[object Object]

← back to Ken

[overnight] pre-debate baseline

93ce9eb0e9921dcdc4ecaf674301ad2389b8d313 · 2026-05-04 03:46:34 -0700 · SteveStudio2

Files touched

Diff

commit 93ce9eb0e9921dcdc4ecaf674301ad2389b8d313
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Mon May 4 03:46:34 2026 -0700

    [overnight] pre-debate baseline
---
 .DS_Store                                          |  Bin 0 -> 6148 bytes
 .env.example                                       |    9 +
 .gitignore                                         |    1 +
 CHANGES.md                                         |   21 +
 CHANGES2.md                                        |    8 +
 ecosystem.config.js                                |   28 +
 .../docs/plans/2026-03-01-ken-neon-redesign.md     |   32 +
 kalshi-dash/index.html                             |   15 +
 kalshi-dash/package-lock.json                      | 3444 ++++++++
 kalshi-dash/package.json                           |   26 +
 kalshi-dash/postcss.config.js                      |    6 +
 kalshi-dash/server.js                              | 9140 ++++++++++++++++++++
 kalshi-dash/src/Layout.jsx                         |  355 +
 kalshi-dash/src/Login.jsx                          |  343 +
 kalshi-dash/src/api.js                             |   45 +
 kalshi-dash/src/components/MiniCandlestick.jsx     |  117 +
 kalshi-dash/src/components/RingGauge.jsx           |  138 +
 kalshi-dash/src/components/Sparkline.jsx           |   91 +
 kalshi-dash/src/components/TickerTape.jsx          |  146 +
 kalshi-dash/src/hooks/useAnimatedValue.js          |   47 +
 kalshi-dash/src/index.css                          |  913 ++
 kalshi-dash/src/main.jsx                           |   48 +
 kalshi-dash/src/pages/Dashboard.jsx                | 1900 ++++
 kalshi-dash/src/pages/Heatmap.jsx                  |  986 +++
 kalshi-dash/src/pages/Intelligence.jsx             |  340 +
 kalshi-dash/src/pages/Markets.jsx                  |  427 +
 kalshi-dash/src/pages/Orbit.jsx                    |  698 ++
 kalshi-dash/src/pages/Orders.jsx                   |  302 +
 kalshi-dash/src/pages/Positions.jsx                |  969 +++
 kalshi-dash/src/pages/Pulse.jsx                    |  748 ++
 kalshi-dash/src/pages/Risk.jsx                     |  755 ++
 kalshi-dash/src/pages/Settings.jsx                 |  602 ++
 kalshi-dash/src/pages/Signals.jsx                  |  482 ++
 kalshi-dash/src/pages/Trading.jsx                  |  394 +
 kalshi-dash/src/pages/Weather.jsx                  |  171 +
 kalshi-dash/src/setup/SetupWizard.jsx              |  119 +
 kalshi-dash/src/setup/steps/BrowseMarkets.jsx      |   94 +
 kalshi-dash/src/setup/steps/ConnectKalshi.jsx      |   95 +
 kalshi-dash/src/setup/steps/FetchWeather.jsx       |   79 +
 kalshi-dash/src/setup/steps/GoLive.jsx             |  106 +
 kalshi-dash/src/setup/steps/RiskProfile.jsx        |   68 +
 kalshi-dash/src/setup/steps/RunPredictions.jsx     |   87 +
 kalshi-dash/src/setup/steps/TestConnection.jsx     |   91 +
 kalshi-dash/src/setup/steps/Welcome.jsx            |   69 +
 kalshi-dash/src/utils/chartTheme.js                |   76 +
 kalshi-dash/tailwind.config.js                     |    5 +
 kalshi-dash/vite.config.js                         |    7 +
 next.config.js                                     |    6 +
 package-lock.json                                  | 2237 +++++
 package.json                                       |   26 +
 postcss.config.js                                  |    6 +
 react-dash/index.html                              |   13 +
 react-dash/package-lock.json                       | 5100 +++++++++++
 react-dash/package.json                            |   28 +
 react-dash/postcss.config.js                       |    3 +
 react-dash/server.js                               |  698 ++
 react-dash/src/Layout.jsx                          |   82 +
 react-dash/src/Login.jsx                           |  168 +
 react-dash/src/api.js                              |   67 +
 react-dash/src/index.css                           |   38 +
 react-dash/src/main.jsx                            |   40 +
 react-dash/src/pages/Bankroll.jsx                  |  153 +
 react-dash/src/pages/Dashboard.jsx                 |  347 +
 react-dash/src/pages/Markets.jsx                   |   43 +
 react-dash/src/pages/Models.jsx                    |  162 +
 react-dash/src/pages/Polymarket.jsx                |  325 +
 react-dash/src/pages/Risk.jsx                      |  255 +
 react-dash/src/pages/Settings.jsx                  |  331 +
 react-dash/src/pages/Trades.jsx                    |  134 +
 react-dash/src/pages/Weather.jsx                   |  151 +
 react-dash/src/setup/SetupWizard.jsx               |  150 +
 react-dash/src/setup/steps/ConnectPoly.jsx         |  123 +
 react-dash/src/setup/steps/FetchWeather.jsx        |   85 +
 react-dash/src/setup/steps/GoLive.jsx              |  115 +
 react-dash/src/setup/steps/IngestMarkets.jsx       |   94 +
 react-dash/src/setup/steps/RiskProfile.jsx         |  109 +
 react-dash/src/setup/steps/RunPredictions.jsx      |  118 +
 react-dash/src/setup/steps/TestConnection.jsx      |  127 +
 react-dash/src/setup/steps/Welcome.jsx             |   69 +
 react-dash/tailwind.config.js                      |   11 +
 react-dash/vite.config.js                          |   20 +
 src/app/api/alerts/route.js                        |   48 +
 src/app/api/bankroll/route.js                      |   26 +
 src/app/api/dashboard/route.js                     |   28 +
 src/app/api/health/route.js                        |   40 +
 src/app/api/markets/route.js                       |  108 +
 src/app/api/models/route.js                        |  108 +
 src/app/api/risk/route.js                          |   52 +
 src/app/api/trades/route.js                        |  100 +
 src/app/api/weather/route.js                       |  120 +
 src/app/api/workers/route.js                       |   31 +
 src/app/dashboard/bankroll/page.js                 |  104 +
 src/app/dashboard/layout.js                        |   96 +
 src/app/dashboard/markets/page.js                  |  101 +
 src/app/dashboard/models/page.js                   |   96 +
 src/app/dashboard/page.js                          |  217 +
 src/app/dashboard/risk/page.js                     |  141 +
 src/app/dashboard/settings/page.js                 |  130 +
 src/app/dashboard/trades/page.js                   |   96 +
 src/app/dashboard/weather/page.js                  |  124 +
 src/app/globals.css                                |   81 +
 src/app/layout.js                                  |   18 +
 src/app/page.js                                    |   85 +
 src/lib/auth.js                                    |   42 +
 src/lib/db.js                                      |   32 +
 src/lib/model.js                                   |  139 +
 src/lib/polymarket.js                              |  140 +
 src/lib/risk.js                                    |  156 +
 src/lib/slack.js                                   |   59 +
 src/lib/weather.js                                 |  140 +
 src/workers/cron-runner.js                         |   61 +
 tailwind.config.js                                 |   21 +
 112 files changed, 38817 insertions(+)

diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..b2fe682
Binary files /dev/null and b/.DS_Store differ
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..32e53b6
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,9 @@
+ADMIN_USER=admin
+ADMIN_PASSWORD=change-me
+JWT_SECRET=change-me-to-a-long-random-secret
+DATABASE_URL=postgresql://user:password@127.0.0.1:5432/database_name
+KEN_DATABASE_URL=postgresql://user:password@127.0.0.1:5432/database_name
+GEMINI_API_KEY=change-me
+SLACK_WEBHOOK_URL=
+GOOGLE_CLIENT_ID=
+ALLOWED_EMAILS=owner@example.com
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules
diff --git a/CHANGES.md b/CHANGES.md
new file mode 100644
index 0000000..e0b79d8
--- /dev/null
+++ b/CHANGES.md
@@ -0,0 +1,21 @@
+## Applied
+- `src/lib/auth.js:4` — added required env lookup; `JWT_SECRET` and `ADMIN_PASSWORD` now fail fast when missing — removes hardcoded JWT/admin secrets from source.
+- `src/lib/db.js:3` — added required env lookup and changed the Postgres connection to `DATABASE_URL` — removes the hardcoded DB password from source.
+- `react-dash/server.js:15` — added required env lookup; auth password and DB URL now come from `ADMIN_PASSWORD` and `DATABASE_URL` — removes exposed credentials from source.
+- `kalshi-dash/server.js:17` — added required env lookup; auth password, Gemini key, main DB URL, and Ken DB URL now come from env — removes exposed credentials from source.
+- `kalshi-dash/server.js:33` — changed Slack webhook to optional `SLACK_WEBHOOK_URL` — removes leaked webhook from source while preserving disabled Slack behavior.
+- `kalshi-dash/server.js:3554` — removed browser-embedded Basic Auth credentials and switched API calls to same-origin credentials — prevents exposing the admin password to clients.
+- `.env.example:1` — added placeholder-only env variables — documents required config without writing real secrets.
+
+## Deferred (needs Steve)
+- `P0` — `src/lib/auth.js:4`, `src/lib/auth.js:14`, `src/lib/db.js:12`, `kalshi-dash/server.js:33`, `kalshi-dash/server.js:138`, `kalshi-dash/server.js:359`, `kalshi-dash/server.js:4882` — exposed secrets must be revoked/rotated — rotation requires owner/provider access.
+- `P0` — `src/app/api/markets/route.js:7`, `src/app/api/risk/route.js:7`, `src/app/api/trades/route.js:7`, `src/app/api/workers/route.js:5` — API routes lack server-side auth — broader auth behavior change, not in safe autonomous list.
+- `P0` — `src/app/api/trades/route.js:25` — paper order validation and transaction safety missing — trading/business logic change, not safe without tests and owner review.
+- `P0` — `react-dash/server.js:66`, `kalshi-dash/server.js:400` — unsigned forgeable session cookies — replacing session design is outside the allowed safe fixes.
+- `P0` — `react-dash/server.js:152`, `kalshi-dash/server.js:589` — Google OAuth fails open when `ALLOWED_EMAILS` is unset — access policy change needs Steve confirmation.
+- `P1` — `src/app/api/trades/route.js:47` — bankroll updates can race — requires transaction/locking design, not a safe patch.
+- `P1` — `src/workers/cron-runner.js:24` — cron jobs can overlap — scheduler/locking behavior needs design confirmation.
+- `P1` — `src/app/api/markets/route.js:79` — ingest errors are hidden behind success — API contract change deferred.
+- `P1` — `react-dash/server.js:43` — wildcard CORS on authenticated API — deployment origin policy needed.
+- `P1` — `src/lib/risk.js:16` — dynamic SQL columns are not whitelisted — internal API hardening deferred because not in safe list.
+- `P1` — `next.config.js:3` — strict mode disabled and ESLint ignored — build policy change deferred.
diff --git a/CHANGES2.md b/CHANGES2.md
new file mode 100644
index 0000000..897f4f1
--- /dev/null
+++ b/CHANGES2.md
@@ -0,0 +1,8 @@
+## Round 2 Applied
+- `kalshi-dash/server.js:138` — made `GEMINI_API_KEY` optional instead of startup-required — fixes rereview finding `CODEX_REREVIEW.md:14` that missing Gemini config newly aborted startup despite graceful AI fallback behavior.
+- `kalshi-dash/server.js:142` — added an early no-key return in `geminiAnalyze()` — completes rereview recommendation `CODEX_REREVIEW.md:25` to degrade without Gemini rather than failing the service.
+- `kalshi-dash/server.js:4817` — added an auth gate before serving `/inc` HTML — fixes rereview broken finding `CODEX_REREVIEW.md:15` and missed call-site `CODEX_REREVIEW.md:19` where the page shell was public while its data APIs were authenticated.
+
+## Still Deferred
+- `react-dash/server.js:15`, `kalshi-dash/server.js:17`, `.env.example:1` — standalone `.env` loading concern remains deferred because no dotenv dependency exists and package changes/installs are out of scope for this pass (`CODEX_REREVIEW.md:7`, `CODEX_REREVIEW.md:11`, `CODEX_REREVIEW.md:16`).
+- `kalshi-dash/server.js:2632`, `kalshi-dash/server.js:2764` — left `/api/inc` and `/api/inc/market-data` authenticated; rereview offered either gating `/inc` or making these APIs public, and this pass chose the narrower auth-gate fix (`CODEX_REREVIEW.md:20`, `CODEX_REREVIEW.md:21`, `CODEX_REREVIEW.md:24`).
diff --git a/ecosystem.config.js b/ecosystem.config.js
new file mode 100644
index 0000000..5c19ddc
--- /dev/null
+++ b/ecosystem.config.js
@@ -0,0 +1,28 @@
+module.exports = {
+  apps: [
+    {
+      name: 'bertha-web',
+      script: 'node_modules/.bin/next',
+      args: 'start -p 7800',
+      cwd: '/root/Projects/Bertha',
+      env: {
+        NODE_ENV: 'production',
+        PORT: 7800,
+      },
+      max_memory_restart: '512M',
+      restart_delay: 5000,
+      max_restarts: 10,
+    },
+    {
+      name: 'bertha-cron',
+      script: 'src/workers/cron-runner.js',
+      cwd: '/root/Projects/Bertha',
+      env: {
+        NODE_ENV: 'production',
+      },
+      max_memory_restart: '256M',
+      restart_delay: 10000,
+      max_restarts: 5,
+    }
+  ]
+};
diff --git a/kalshi-dash/docs/plans/2026-03-01-ken-neon-redesign.md b/kalshi-dash/docs/plans/2026-03-01-ken-neon-redesign.md
new file mode 100644
index 0000000..053b446
--- /dev/null
+++ b/kalshi-dash/docs/plans/2026-03-01-ken-neon-redesign.md
@@ -0,0 +1,32 @@
+# Ken Neon Redesign — Full UI Overhaul
+
+## Date: 2026-03-01
+## Status: APPROVED
+
+## Design System: "KEN NEON"
+
+### Colors
+| Token | Value | Usage |
+|-------|-------|-------|
+| --bg | #0E0E10 | Page background |
+| --surface | #141419 | Card backgrounds |
+| --surface-el | #1C1C24 | Elevated surfaces, inputs |
+| --border | #2A2A35 | Borders, dividers |
+| --cyan | #00F0FF | Primary accent, active states |
+| --orange | #F7931A | Secondary accent, brand |
+| --green | #00C389 | Profits, success |
+| --red | #FF5C5C | Losses, danger |
+| --blue | #3B82F6 | Info, links |
+| --text | #E0E0E0 | Primary text |
+| --text-dim | #A8A8A8 | Secondary text |
+| --text-muted | #6B7280 | Muted/disabled |
+
+### Typography
+- Headings: Space Grotesk 700
+- Body: Inter 400/500/600
+- Data/Mono: JetBrains Mono 400/500
+
+### Source: CrypTrade Stock Trader UI Kit (Figma Community)
+- https://www.figma.com/community/file/1566555329071317833
+
+### Scope: All 13 pages + Layout + Login
diff --git a/kalshi-dash/index.html b/kalshi-dash/index.html
new file mode 100644
index 0000000..f0cdcda
--- /dev/null
+++ b/kalshi-dash/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Ken - Event Contracts Trading</title>
+  <link rel="preconnect" href="https://fonts.googleapis.com" />
+  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
+  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
+</head>
+<body>
+  <div id="root"></div>
+  <script type="module" src="/src/main.jsx"></script>
+</body>
+</html>
diff --git a/kalshi-dash/package-lock.json b/kalshi-dash/package-lock.json
new file mode 100644
index 0000000..05cab5c
--- /dev/null
+++ b/kalshi-dash/package-lock.json
@@ -0,0 +1,3444 @@
+{
+  "name": "kalshi-dash",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "kalshi-dash",
+      "version": "1.0.0",
+      "dependencies": {
+        "google-auth-library": "^9.0.0",
+        "pg": "^8.11.0",
+        "react": "^18.2.0",
+        "react-dom": "^18.2.0",
+        "react-router-dom": "^6.20.0",
+        "recharts": "^2.10.0"
+      },
+      "devDependencies": {
+        "@types/react": "^18.2.0",
+        "@vitejs/plugin-react": "^4.2.0",
+        "autoprefixer": "^10.4.0",
+        "postcss": "^8.4.0",
+        "tailwindcss": "^3.4.0",
+        "vite": "^5.0.0"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+      "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+      "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+      "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+      "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+      "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+      "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+      "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@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/@remix-run/router": {
+      "version": "1.23.2",
+      "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+      "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.27",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+      "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+      "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+      "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+      "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+      "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+      "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+      "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+      "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+      "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+      "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+      "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+      "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+      "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+      "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+      "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+      "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+      "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+      "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+      "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+      "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+      "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+      "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+      "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+      "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+      "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/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-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-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-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-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-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-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/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/prop-types": {
+      "version": "15.7.15",
+      "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+      "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/react": {
+      "version": "18.3.28",
+      "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+      "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/prop-types": "*",
+        "csstype": "^3.2.2"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.0",
+        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+        "@rolldown/pluginutils": "1.0.0-beta.27",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.17.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "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.4.24",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
+      "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
+      "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.1",
+        "caniuse-lite": "^1.0.30001766",
+        "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/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "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/baseline-browser-mapping": {
+      "version": "2.9.19",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+      "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.js"
+      }
+    },
+    "node_modules/bignumber.js": {
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+      "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "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.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+      "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+      "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.9.0",
+        "caniuse-lite": "^1.0.30001759",
+        "electron-to-chromium": "^1.5.263",
+        "node-releases": "^2.0.27",
+        "update-browserslist-db": "^1.2.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "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.30001769",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
+      "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
+      "dev": true,
+      "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/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/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/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/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==",
+      "license": "MIT"
+    },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "license": "ISC",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "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-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-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+      "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+      "license": "ISC",
+      "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-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "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/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decimal.js-light": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+      "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+      "license": "MIT"
+    },
+    "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/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.286",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+      "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "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/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "license": "MIT"
+    },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
+    "node_modules/fast-equals": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+      "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "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/gaxios": {
+      "version": "6.7.1",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
+      "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^7.0.1",
+        "is-stream": "^2.0.0",
+        "node-fetch": "^2.6.9",
+        "uuid": "^9.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gcp-metadata": {
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
+      "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "gaxios": "^6.1.1",
+        "google-logging-utils": "^0.0.2",
+        "json-bigint": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/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/google-auth-library": {
+      "version": "9.15.1",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
+      "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "base64-js": "^1.3.0",
+        "ecdsa-sig-formatter": "^1.0.11",
+        "gaxios": "^6.1.1",
+        "gcp-metadata": "^6.1.0",
+        "gtoken": "^7.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/google-logging-utils": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
+      "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gtoken": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
+      "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
+      "license": "MIT",
+      "dependencies": {
+        "gaxios": "^6.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "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/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "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/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-bigint": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bignumber.js": "^9.0.0"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "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/lodash": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+      "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/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/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "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==",
+      "dev": true,
+      "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/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.27",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+      "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+      "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==",
+      "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.18.0",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
+      "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.11.0",
+        "pg-pool": "^3.11.0",
+        "pg-protocol": "^1.11.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.11.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz",
+      "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==",
+      "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.11.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz",
+      "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz",
+      "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==",
+      "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==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "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.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "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/prop-types": {
+      "version": "15.8.1",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
+    "node_modules/prop-types/node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+      "license": "MIT"
+    },
+    "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": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.2"
+      },
+      "peerDependencies": {
+        "react": "^18.3.1"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+      "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+      "license": "MIT"
+    },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-router": {
+      "version": "6.30.3",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+      "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+      "license": "MIT",
+      "dependencies": {
+        "@remix-run/router": "1.23.2"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8"
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "6.30.3",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+      "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+      "license": "MIT",
+      "dependencies": {
+        "@remix-run/router": "1.23.2",
+        "react-router": "6.30.3"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8",
+        "react-dom": ">=16.8"
+      }
+    },
+    "node_modules/react-smooth": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
+      "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-equals": "^5.0.1",
+        "prop-types": "^15.8.1",
+        "react-transition-group": "^4.4.5"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
+    "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/recharts": {
+      "version": "2.15.4",
+      "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
+      "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
+      "license": "MIT",
+      "dependencies": {
+        "clsx": "^2.0.0",
+        "eventemitter3": "^4.0.1",
+        "lodash": "^4.17.21",
+        "react-is": "^18.3.1",
+        "react-smooth": "^4.0.4",
+        "recharts-scale": "^0.4.4",
+        "tiny-invariant": "^1.3.1",
+        "victory-vendor": "^36.6.8"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/recharts-scale": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
+      "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
+      "license": "MIT",
+      "dependencies": {
+        "decimal.js-light": "^2.4.1"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.11",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+      "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "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/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/rollup": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+      "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.57.1",
+        "@rollup/rollup-android-arm64": "4.57.1",
+        "@rollup/rollup-darwin-arm64": "4.57.1",
+        "@rollup/rollup-darwin-x64": "4.57.1",
+        "@rollup/rollup-freebsd-arm64": "4.57.1",
+        "@rollup/rollup-freebsd-x64": "4.57.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+        "@rollup/rollup-linux-arm64-musl": "4.57.1",
+        "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+        "@rollup/rollup-linux-loong64-musl": "4.57.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+        "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+        "@rollup/rollup-linux-x64-gnu": "4.57.1",
+        "@rollup/rollup-linux-x64-musl": "4.57.1",
+        "@rollup/rollup-openbsd-x64": "4.57.1",
+        "@rollup/rollup-openharmony-arm64": "4.57.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+        "@rollup/rollup-win32-x64-gnu": "4.57.1",
+        "@rollup/rollup-win32-x64-msvc": "4.57.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "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/scheduler": {
+      "version": "0.23.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/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==",
+      "dev": true,
+      "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/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/tiny-invariant": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+      "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+      "license": "MIT"
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/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.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "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/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+      "license": "MIT"
+    },
+    "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/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/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/uuid": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+      "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/victory-vendor": {
+      "version": "36.9.2",
+      "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+      "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
+      "license": "MIT AND ISC",
+      "dependencies": {
+        "@types/d3-array": "^3.0.3",
+        "@types/d3-ease": "^3.0.0",
+        "@types/d3-interpolate": "^3.0.1",
+        "@types/d3-scale": "^4.0.2",
+        "@types/d3-shape": "^3.1.0",
+        "@types/d3-time": "^3.0.0",
+        "@types/d3-timer": "^3.0.0",
+        "d3-array": "^3.1.6",
+        "d3-ease": "^3.0.1",
+        "d3-interpolate": "^3.0.1",
+        "d3-scale": "^4.0.2",
+        "d3-shape": "^3.1.0",
+        "d3-time": "^3.0.0",
+        "d3-timer": "^3.0.1"
+      }
+    },
+    "node_modules/vite": {
+      "version": "5.4.21",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    }
+  }
+}
diff --git a/kalshi-dash/package.json b/kalshi-dash/package.json
new file mode 100644
index 0000000..ed24c96
--- /dev/null
+++ b/kalshi-dash/package.json
@@ -0,0 +1,26 @@
+{
+  "name": "kalshi-dash",
+  "version": "1.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-router-dom": "^6.20.0",
+    "recharts": "^2.10.0",
+    "pg": "^8.11.0",
+    "google-auth-library": "^9.0.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-react": "^4.2.0",
+    "vite": "^5.0.0",
+    "tailwindcss": "^3.4.0",
+    "postcss": "^8.4.0",
+    "autoprefixer": "^10.4.0",
+    "@types/react": "^18.2.0"
+  }
+}
diff --git a/kalshi-dash/postcss.config.js b/kalshi-dash/postcss.config.js
new file mode 100644
index 0000000..2aa7205
--- /dev/null
+++ b/kalshi-dash/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+};
diff --git a/kalshi-dash/server.js b/kalshi-dash/server.js
new file mode 100644
index 0000000..980dc3e
--- /dev/null
+++ b/kalshi-dash/server.js
@@ -0,0 +1,9140 @@
+import http from 'http';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import pg from 'pg';
+import crypto from 'crypto';
+import { OAuth2Client } from 'google-auth-library';
+import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
+import os from 'os';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const PORT = 7810;
+const DIST = path.join(__dirname, 'dist');
+
+function requiredEnv(name) {
+  const value = process.env[name];
+  if (!value) {
+    throw new Error(`Missing required environment variable: ${name}`);
+  }
+  return value;
+}
+
+// ── Unified DW Auth Config ──
+const AUTH_USER = process.env.ADMIN_USER || 'admin';
+const AUTH_PASS = requiredEnv('ADMIN_PASSWORD');
+const SESSION_COOKIE = 'kalshi_session';
+const SESSION_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
+const WHITELISTED_IPS = ['127.0.0.1', '::1', '45.61.58.125', 'localhost'];
+
+// ── Slack Alerts ──
+const SLACK_WEBHOOK = process.env.SLACK_WEBHOOK_URL || '';
+
+async function sendSlack(text) {
+  // Slack notifications disabled per Steve
+  return;
+}
+
+// ── Google OAuth Config ──
+const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
+const ALLOWED_EMAILS = (process.env.ALLOWED_EMAILS || '').split(',').filter(Boolean);
+const googleClient = GOOGLE_CLIENT_ID ? new OAuth2Client(GOOGLE_CLIENT_ID) : null;
+
+// ── NWS Weather Config ──
+const NWS_BASE = 'https://api.weather.gov';
+const NWS_USER_AGENT = 'Ken-Bot/1.0 (steve@designerwallcoverings.com)';
+
+const CITY_COORDS = {
+  'New York': { lat: 40.7128, lon: -74.0060 },
+  'Los Angeles': { lat: 34.0522, lon: -118.2437 },
+  'Chicago': { lat: 41.8781, lon: -87.6298 },
+  'Houston': { lat: 29.7604, lon: -95.3698 },
+  'Phoenix': { lat: 33.4484, lon: -112.0740 },
+  'Philadelphia': { lat: 39.9526, lon: -75.1652 },
+  'Dallas': { lat: 32.7767, lon: -96.7970 },
+  'San Francisco': { lat: 37.7749, lon: -122.4194 },
+  'Seattle': { lat: 47.6062, lon: -122.3321 },
+  'Denver': { lat: 39.7392, lon: -104.9903 },
+  'Boston': { lat: 42.3601, lon: -71.0589 },
+  'Miami': { lat: 25.7617, lon: -80.1918 },
+  'Atlanta': { lat: 33.7490, lon: -84.3880 },
+  'Minneapolis': { lat: 44.9778, lon: -93.2650 },
+  'Washington': { lat: 38.9072, lon: -77.0369 },
+  'Las Vegas': { lat: 36.1699, lon: -115.1398 },
+};
+
+async function nwsFetch(url) {
+  const controller = new AbortController();
+  // Hard Promise.race timeout — AbortController alone can't kill DNS-level hangs
+  const hardTimeout = new Promise((_, reject) =>
+    setTimeout(() => { controller.abort(); reject(new Error('NWS timeout (5s)')); }, 5000)
+  );
+  const doFetch = async () => {
+    const res = await fetch(url, {
+      headers: { 'User-Agent': NWS_USER_AGENT, 'Accept': 'application/geo+json' },
+      signal: controller.signal,
+    });
+    if (!res.ok) throw new Error(`NWS ${res.status}: ${res.statusText}`);
+    return res.json();
+  };
+  return Promise.race([doFetch(), hardTimeout]);
+}
+
+function extractForecastFeatures(forecastData, variable) {
+  const periods = forecastData?.properties?.periods || [];
+  if (!periods.length) return null;
+  const features = { variable, values: [], timestamps: [] };
+  for (const p of periods) {
+    let value = null;
+    if (variable === 'temperature') { value = p.temperature; features.unit = p.temperatureUnit; }
+    else if (variable === 'wind') { const m = p.windSpeed?.match(/(\d+)/); if (m) value = parseInt(m[1]); features.unit = 'mph'; }
+    else if (variable === 'precipitation') { value = p.probabilityOfPrecipitation?.value ?? 0; features.unit = 'percent'; }
+    if (value !== null) { features.values.push(value); features.timestamps.push(p.startTime); }
+  }
+  if (features.values.length) {
+    features.mean = features.values.reduce((a, b) => a + b, 0) / features.values.length;
+    features.min = Math.min(...features.values);
+    features.max = Math.max(...features.values);
+    features.latest = features.values[0];
+  }
+  return features;
+}
+
+// ── Prediction Model ──
+function ensembleExceedance(features, threshold, operator = '>=') {
+  const vals = features?.values || [];
+  if (!vals.length) return null;
+  let ct = 0;
+  for (const v of vals) {
+    if (operator === '>=' && v >= threshold) ct++;
+    else if (operator === '<=' && v <= threshold) ct++;
+    else if (operator === '>' && v > threshold) ct++;
+    else if (operator === '<' && v < threshold) ct++;
+  }
+  return ct / vals.length;
+}
+
+function climatologicalPrior(variable, threshold, month) {
+  const season = month >= 6 && month <= 8 ? 'summer' : month >= 12 || month <= 2 ? 'winter' : month >= 3 && month <= 5 ? 'spring' : 'fall';
+  const priors = {
+    temperature: { summer: t => t > 100 ? 0.05 : t > 90 ? 0.35 : t > 80 ? 0.65 : 0.85, winter: t => t > 60 ? 0.15 : t > 40 ? 0.50 : t > 20 ? 0.75 : 0.90, spring: t => t > 80 ? 0.20 : t > 60 ? 0.55 : t > 40 ? 0.80 : 0.95, fall: t => t > 80 ? 0.25 : t > 60 ? 0.50 : t > 40 ? 0.75 : 0.90 },
+    precipitation: { any: t => t > 2 ? 0.10 : t > 1 ? 0.20 : t > 0.5 ? 0.35 : 0.50 },
+    snow: { winter: t => t > 12 ? 0.05 : t > 6 ? 0.15 : t > 3 ? 0.25 : 0.40, any: () => 0.10 },
+  };
+  const vp = priors[variable]; if (!vp) return 0.50;
+  const fn = vp[season] || vp.any; if (!fn) return 0.50;
+  return fn(threshold);
+}
+
+function bayesianUpdate(prior, forecastProb, weight = 0.7) {
+  return prior * (1 - weight) + forecastProb * weight;
+}
+
+// ══════════════════════════════════════════════════════
+// GEMINI AI PREDICTION ENGINE — uses CPU + AI for better signals
+// ══════════════════════════════════════════════════════
+const GEMINI_API_KEY = process.env.GEMINI_API_KEY || '';
+const GEMINI_MODEL = 'gemini-2.0-flash';
+
+async function geminiAnalyze(prompt, maxTokens = 1024) {
+  if (!GEMINI_API_KEY) return null;
+
+  try {
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 25000);
+    const res = await fetch(
+      `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`,
+      {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        signal: controller.signal,
+        body: JSON.stringify({
+          contents: [{ parts: [{ text: prompt }] }],
+          generationConfig: { temperature: 0.3, maxOutputTokens: maxTokens },
+        }),
+      }
+    );
+    clearTimeout(timeout);
+    if (!res.ok) return null;
+    const data = await res.json();
+    return data?.candidates?.[0]?.content?.parts?.[0]?.text || null;
+  } catch (e) {
+    console.log(`[Ken/Gemini] Error: ${e.message}`);
+    return null;
+  }
+}
+
+// AI-Enhanced Signal Scoring — Gemini analyzes top signals for smarter predictions
+let geminiCallCount = 0;
+const GEMINI_DAILY_LIMIT = 200; // budget: ~200 calls/day
+const geminiCallReset = setInterval(() => { geminiCallCount = 0; }, 24 * 60 * 60 * 1000);
+
+async function aiEnhanceSignal(signal, articles, market) {
+  if (geminiCallCount >= GEMINI_DAILY_LIMIT) return signal;
+  geminiCallCount++;
+
+  const articleSummary = articles.slice(0, 8).map((a, i) =>
+    `${i + 1}. [${a.source}] ${a.headline?.substring(0, 150)}`
+  ).join('\n');
+
+  const prompt = `You are Ken, an expert prediction market analyst. Analyze this trading signal and provide a calibrated probability estimate.
+
+MARKET: "${market.title}" ${market.subtitle ? '- ' + market.subtitle : ''}
+CURRENT PRICE: YES at ${market.yes_ask}¢, NO at ${100 - market.yes_ask}¢
+24H VOLUME: ${market.volume_24h?.toLocaleString() || '?'}
+OUR SIGNAL: ${signal.direction} with ${(Math.abs(signal.expectedEdge) * 100).toFixed(1)}% edge
+
+RECENT NEWS (${articles.length} articles):
+${articleSummary}
+
+SENTIMENT ANALYSIS: category=${articles[0]?.category || '?'}, direction=${articles[0]?.direction || '?'}, agreement=${((signal.agreement || 0) * 100).toFixed(0)}%
+
+Respond with ONLY a JSON object (no markdown):
+{"probability": 0.XX, "confidence": 0.XX, "reasoning": "one sentence", "adjustedEdge": X.XX, "riskFlag": "none|caution|high_risk"}`;
+
+  const response = await geminiAnalyze(prompt, 256);
+  if (!response) return signal;
+
+  try {
+    // Parse JSON from response (handle markdown wrapping)
+    const jsonStr = response.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
+    const ai = JSON.parse(jsonStr);
+
+    // Blend AI probability with our statistical model
+    const blendedEdge = signal.expectedEdge * 0.6 + (ai.adjustedEdge || signal.expectedEdge) * 0.4;
+    const blendedConf = signal.confidence * 0.5 + (ai.confidence || signal.confidence) * 0.5;
+
+    return {
+      ...signal,
+      expectedEdge: blendedEdge,
+      confidence: Math.min(0.95, blendedConf),
+      aiProbability: ai.probability,
+      aiReasoning: ai.reasoning,
+      aiRiskFlag: ai.riskFlag || 'none',
+      aiEnhanced: true,
+    };
+  } catch (e) {
+    console.log(`[Ken/Gemini] Parse error: ${e.message}`);
+    return signal;
+  }
+}
+
+// ══════════════════════════════════════════════════════
+// MONTE CARLO SIMULATION — CPU-intensive probability estimation
+// ══════════════════════════════════════════════════════
+function monteCarloSimulation(params, iterations = 10000) {
+  const { priorProb, sentimentImpact, momentum2h, momentum6h, volume24h, spread, articleCount, agreement } = params;
+
+  let successCount = 0;
+  const edgeDistribution = [];
+
+  for (let i = 0; i < iterations; i++) {
+    // Add noise to each factor (simulating uncertainty)
+    const sentimentNoise = sentimentImpact + (Math.random() - 0.5) * 0.15;
+    const momentumNoise = (momentum2h || 0) / 100 + (Math.random() - 0.5) * 0.08;
+    const volumeSignal = Math.min(1, (volume24h || 0) / 10000) * (Math.random() * 0.1);
+    const spreadPenalty = Math.max(0, (spread || 0) - 3) * -0.005 * Math.random();
+    const articleBoost = Math.min(0.1, (articleCount || 0) * 0.01) * Math.random();
+    const agreementBoost = ((agreement || 0.5) - 0.5) * 0.15 * Math.random();
+
+    // Simulate probability with all factors
+    const simProb = Math.max(0.01, Math.min(0.99,
+      priorProb + sentimentNoise + momentumNoise + volumeSignal + spreadPenalty + articleBoost + agreementBoost
+    ));
+
+    // Compare to market price — is there an edge?
+    const marketProb = priorProb;
+    const simEdge = simProb - marketProb;
+
+    if (Math.abs(simEdge) >= 0.05) successCount++;
+    edgeDistribution.push(simEdge);
+  }
+
+  // Calculate statistics from simulation
+  edgeDistribution.sort((a, b) => a - b);
+  const mean = edgeDistribution.reduce((a, b) => a + b, 0) / iterations;
+  const p5 = edgeDistribution[Math.floor(iterations * 0.05)];
+  const p25 = edgeDistribution[Math.floor(iterations * 0.25)];
+  const p50 = edgeDistribution[Math.floor(iterations * 0.50)];
+  const p75 = edgeDistribution[Math.floor(iterations * 0.75)];
+  const p95 = edgeDistribution[Math.floor(iterations * 0.95)];
+  const stdDev = Math.sqrt(edgeDistribution.reduce((sum, e) => sum + Math.pow(e - mean, 2), 0) / iterations);
+
+  return {
+    meanEdge: mean,
+    medianEdge: p50,
+    stdDev,
+    confidence: successCount / iterations,
+    percentiles: { p5, p25, p50, p75, p95 },
+    iterations,
+    edgeConsistent: Math.sign(p25) === Math.sign(p75), // Is edge direction consistent?
+  };
+}
+
+// ══════════════════════════════════════════════════════
+// PARALLEL PROCESSING — run CPU-heavy tasks across cores
+// ══════════════════════════════════════════════════════
+const CPU_CORES = Math.max(2, os.cpus().length);
+const WORKER_POOL_SIZE = Math.min(12, CPU_CORES - 2); // Use up to 14 cores, leave 2 for OS + other services
+
+function runInParallel(tasks, workerFn) {
+  return Promise.all(tasks.map(task => {
+    return new Promise(resolve => {
+      try {
+        resolve(workerFn(task));
+      } catch (e) {
+        resolve({ error: e.message, task });
+      }
+    });
+  }));
+}
+
+// Batch process markets in parallel chunks
+async function parallelMarketAnalysis(markets, analysisFunc, batchSize = 50) {
+  const results = [];
+  const batches = [];
+  for (let i = 0; i < markets.length; i += batchSize) {
+    batches.push(markets.slice(i, i + batchSize));
+  }
+
+  // Process batches concurrently (up to WORKER_POOL_SIZE at once)
+  for (let i = 0; i < batches.length; i += WORKER_POOL_SIZE) {
+    const concurrent = batches.slice(i, i + WORKER_POOL_SIZE);
+    const batchResults = await Promise.all(concurrent.map(batch =>
+      Promise.all(batch.map(m => analysisFunc(m).catch(e => ({ error: e.message, ticker: m.ticker }))))
+    ));
+    results.push(...batchResults.flat());
+  }
+  return results;
+}
+
+// Parse Kalshi weather market title into event spec
+function parseWeatherEvent(title) {
+  const t = (title || '').toLowerCase();
+  let variable = 'unknown', threshold = 80, operator = '>=';
+  const tempMatch = t.match(/(\d+)\s*°?\s*f/);
+  // Detect temperature events
+  if (t.includes('temperature') || t.includes('temp') || t.includes('°f') || t.includes('degrees') || t.includes('high') || t.includes('low') || t.includes('cold') || t.includes('hot') || t.includes('warm') || t.includes('heat')) {
+    variable = 'temperature';
+    if (tempMatch) threshold = parseInt(tempMatch[1]);
+  }
+  if (t.includes('rain') || t.includes('precipitation') || t.includes('inch')) { variable = 'precipitation'; threshold = tempMatch ? parseInt(tempMatch[1]) : 0.1; }
+  if (t.includes('snow')) { variable = 'snow'; threshold = tempMatch ? parseInt(tempMatch[1]) : 1; }
+  if (t.includes('wind') || t.includes('gust')) { variable = 'wind'; threshold = tempMatch ? parseInt(tempMatch[1]) : 40; }
+  if (t.includes('below') || t.includes('under') || t.includes('less')) operator = '<=';
+  if (t.includes('above') || t.includes('over') || t.includes('exceed') || t.includes('at least')) operator = '>=';
+  return { variable, threshold, operator };
+}
+
+// Extract city from market title
+function extractCity(title) {
+  for (const city of Object.keys(CITY_COORDS)) {
+    if (title.toLowerCase().includes(city.toLowerCase())) return city;
+  }
+  return null;
+}
+
+// ── Kalshi API Config ──
+let KALSHI_ENV = (process.env.KALSHI_ENV || 'demo').toLowerCase();
+const KALSHI_BASE_URLS = {
+  demo: 'https://demo-api.kalshi.co/trade-api/v2',
+  prod: 'https://api.elections.kalshi.com/trade-api/v2',
+};
+let KALSHI_BASE_URL = KALSHI_BASE_URLS[KALSHI_ENV] || KALSHI_BASE_URLS.demo;
+
+// Refresh env from DB config (called on startup and mode changes)
+async function refreshKalshiEnv() {
+  try {
+    const result = await pool.query('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+    const cfg = result.rows[0]?.config || {};
+    if (cfg.kalshi_env && KALSHI_BASE_URLS[cfg.kalshi_env]) {
+      KALSHI_ENV = cfg.kalshi_env;
+      KALSHI_BASE_URL = KALSHI_BASE_URLS[KALSHI_ENV];
+    }
+  } catch {}
+}
+
+// ── Database ──
+const pool = new pg.Pool({
+  connectionString: requiredEnv('DATABASE_URL'),
+  max: 10,
+  idleTimeoutMillis: 30000,
+});
+
+const MIME = {
+  '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
+  '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml',
+  '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff',
+  '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
+  '.webp': 'image/webp', '.map': 'application/json', '.txt': 'text/plain',
+};
+
+// ── DB Helper ──
+async function q(text, params) {
+  return pool.query(text, params);
+}
+
+// ── Response Helper ──
+function json(res, data, status = 200, extraHeaders = {}) {
+  res.writeHead(status, {
+    'Content-Type': 'application/json',
+    'Access-Control-Allow-Origin': '*',
+    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
+    ...extraHeaders,
+  });
+  res.end(JSON.stringify(data));
+}
+
+// ── Body Parser ──
+async function readBody(req) {
+  return new Promise(resolve => {
+    let d = '';
+    req.on('data', c => d += c);
+    req.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } });
+  });
+}
+
+// ── Cookie / Session Helpers ──
+function parseCookies(req) {
+  const obj = {};
+  (req.headers.cookie || '').split(';').forEach(c => {
+    const [k, ...v] = c.trim().split('=');
+    if (k) obj[k] = v.join('=');
+  });
+  return obj;
+}
+
+function getSession(req) {
+  const cookies = parseCookies(req);
+  const token = cookies[SESSION_COOKIE];
+  if (!token) return null;
+  try {
+    const data = JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString());
+    if (!data.authenticated) return null;
+    const age = Date.now() - data.loginTime;
+    if (age > SESSION_MAX_AGE * 1000) return null;
+    return data;
+  } catch { return null; }
+}
+
+function getClientIP(req) {
+  return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || '';
+}
+
+function isWhitelistedIP(req) {
+  const ip = getClientIP(req);
+  return WHITELISTED_IPS.some(w => ip.includes(w));
+}
+
+function hasBasicAuth(req) {
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Basic ')) return false;
+  const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
+  return user === AUTH_USER && pass === AUTH_PASS;
+}
+
+function isAuthenticated(req) {
+  if (isWhitelistedIP(req)) return true;
+  if (hasBasicAuth(req)) return true;
+  if (getSession(req)) return true;
+  return false;
+}
+
+function setSessionCookie(res, sessionToken) {
+  return `${SESSION_COOKIE}=${encodeURIComponent(sessionToken)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`;
+}
+
+function clearSessionCookie() {
+  return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
+}
+
+// ── Kalshi API Signing ──
+// RSA-PSS with SHA-256, salt length 32
+// Signs: timestamp_ms_string + method_uppercase + path
+function signKalshiRequest(privateKeyPem, timestampMs, method, requestPath) {
+  const message = timestampMs + method.toUpperCase() + requestPath;
+  const signature = crypto.sign('sha256', Buffer.from(message), {
+    key: privateKeyPem,
+    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
+    saltLength: 32,
+  });
+  return signature.toString('base64');
+}
+
+// ── Kalshi Authenticated Fetch ──
+async function kalshiFetch(method, apiPath, body = null) {
+  // Load keys from DB
+  const result = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+  const cfg = result.rows[0]?.config || {};
+
+  const apiKey = cfg.kalshi_api_key;
+  const privateKeyPem = cfg.kalshi_private_key;
+
+  if (!apiKey || !privateKeyPem) {
+    throw new Error('Kalshi API keys not configured. Please save your API key and private key first.');
+  }
+
+  const timestampMs = Date.now().toString();
+  // The path for signing is everything after the base domain, starting with /trade-api/v2
+  const basePath = KALSHI_ENV === 'prod' ? '/trade-api/v2' : '/trade-api/v2';
+  const fullSignPath = basePath + apiPath;
+  const signature = signKalshiRequest(privateKeyPem, timestampMs, method, fullSignPath);
+
+  const url = KALSHI_BASE_URL + apiPath;
+  const headers = {
+    'KALSHI-ACCESS-KEY': apiKey,
+    'KALSHI-ACCESS-TIMESTAMP': timestampMs,
+    'KALSHI-ACCESS-SIGNATURE': signature,
+    'Content-Type': 'application/json',
+    'Accept': 'application/json',
+  };
+
+  const fetchOpts = { method, headers };
+  if (body && method !== 'GET' && method !== 'DELETE') {
+    fetchOpts.body = JSON.stringify(body);
+  }
+
+  const response = await fetch(url, fetchOpts);
+  const text = await response.text();
+
+  let data;
+  try {
+    data = JSON.parse(text);
+  } catch {
+    data = { raw: text };
+  }
+
+  if (!response.ok) {
+    const errMsg = data?.message || data?.error || data?.raw || `HTTP ${response.status}`;
+    throw new Error(`Kalshi API error (${response.status}): ${errMsg}`);
+  }
+
+  return data;
+}
+
+// ── Kalshi Unauthenticated Fetch (for public endpoints) ──
+async function kalshiPublicFetch(method, apiPath) {
+  const url = KALSHI_BASE_URL + apiPath;
+  const response = await fetch(url, {
+    method,
+    headers: { 'Accept': 'application/json' },
+  });
+  const text = await response.text();
+
+  let data;
+  try {
+    data = JSON.parse(text);
+  } catch {
+    data = { raw: text };
+  }
+
+  if (!response.ok) {
+    const errMsg = data?.message || data?.error || data?.raw || `HTTP ${response.status}`;
+    throw new Error(`Kalshi API error (${response.status}): ${errMsg}`);
+  }
+
+  return data;
+}
+
+// ── Build query string from params object ──
+function buildQueryString(params) {
+  if (!params || Object.keys(params).length === 0) return '';
+  const qs = Object.entries(params)
+    .filter(([, v]) => v !== undefined && v !== null && v !== '')
+    .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
+    .join('&');
+  return qs ? `?${qs}` : '';
+}
+
+// ── API Route Handlers ──
+const routes = {
+
+  // ════════════════════════════════════════
+  // AUTH ENDPOINTS (same DW pattern)
+  // ════════════════════════════════════════
+
+  'POST /api/auth/login': async (req, res) => {
+    const body = await readBody(req);
+    if (body.username === AUTH_USER && body.password === AUTH_PASS) {
+      const sessionData = { authenticated: true, username: AUTH_USER, loginTime: Date.now() };
+      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
+      json(res, { success: true, message: 'Login successful', username: AUTH_USER }, 200, {
+        'Set-Cookie': setSessionCookie(res, token),
+      });
+    } else {
+      json(res, { error: 'Invalid credentials' }, 401);
+    }
+  },
+
+  'POST /api/auth/logout': async (req, res) => {
+    json(res, { success: true, message: 'Logged out successfully' }, 200, {
+      'Set-Cookie': clearSessionCookie(),
+    });
+  },
+
+  'GET /api/auth/session': async (req, res) => {
+    if (isWhitelistedIP(req)) {
+      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'whitelisted_ip' });
+    }
+    if (hasBasicAuth(req)) {
+      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'basic_auth' });
+    }
+    const session = getSession(req);
+    if (session) {
+      return json(res, {
+        authenticated: true,
+        username: session.username,
+        email: session.email,
+        picture: session.picture,
+        provider: session.provider,
+        loginTime: session.loginTime,
+      });
+    }
+    json(res, { authenticated: false }, 401);
+  },
+
+  'POST /api/auth/google': async (req, res) => {
+    if (!googleClient) return json(res, { error: 'Google OAuth not configured' }, 400);
+    const body = await readBody(req);
+    if (!body.credential) return json(res, { error: 'Missing credential' }, 400);
+    try {
+      const ticket = await googleClient.verifyIdToken({ idToken: body.credential, audience: GOOGLE_CLIENT_ID });
+      const payload = ticket.getPayload();
+      const email = payload.email;
+      if (!payload.email_verified) return json(res, { error: 'Email not verified' }, 401);
+      if (ALLOWED_EMAILS.length > 0 && !ALLOWED_EMAILS.includes(email)) {
+        return json(res, { error: 'Email not authorized' }, 403);
+      }
+      const sessionData = {
+        authenticated: true,
+        username: payload.name || email,
+        email,
+        picture: payload.picture,
+        provider: 'google',
+        loginTime: Date.now(),
+      };
+      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
+      json(res, { success: true, username: payload.name || email, email, picture: payload.picture }, 200, {
+        'Set-Cookie': setSessionCookie(res, token),
+      });
+    } catch (err) {
+      json(res, { error: 'Invalid Google token', details: err.message }, 401);
+    }
+  },
+
+  'GET /api/auth/google-config': async (req, res) => {
+    json(res, { client_id: GOOGLE_CLIENT_ID || null, configured: !!GOOGLE_CLIENT_ID });
+  },
+
+  // ════════════════════════════════════════
+  // SETUP WIZARD (save/load progress)
+  // ════════════════════════════════════════
+
+  'GET /api/setup': async (req, res) => {
+    try {
+      const result = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const cfg = result.rows[0]?.config || {};
+      json(res, {
+        setup_complete: !!cfg.setup_complete,
+        setup_progress: cfg.setup_progress || null,
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'POST /api/setup': async (req, res) => {
+    const body = await readBody(req);
+    try {
+      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const row = current.rows[0] || {};
+      const cfg = row.config || {};
+
+      if (body.action === 'save_progress') {
+        const newConfig = { ...cfg, setup_progress: body.progress };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+        json(res, { success: true });
+      } else if (body.action === 'complete') {
+        const newConfig = { ...cfg, setup_complete: true, setup_progress: body.progress };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+        json(res, { success: true, message: 'Setup complete!' });
+      } else {
+        json(res, { error: `Unknown setup action: ${body.action}` }, 400);
+      }
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // HEALTH
+  // ════════════════════════════════════════
+
+  'GET /api/health': async (req, res) => {
+    try {
+      const dbCheck = await q('SELECT 1');
+      const risk = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const cfg = risk.rows[0]?.config || {};
+
+      // Test Kalshi connection
+      let kalshiConnected = false;
+      let balance = null;
+      if (cfg.kalshi_api_key && cfg.kalshi_private_key) {
+        try {
+          const balData = await kalshiFetch('GET', '/portfolio/balance');
+          kalshiConnected = true;
+          balance = balData.balance ?? balData;
+        } catch {
+          // Keys exist but connection failed
+          kalshiConnected = false;
+        }
+      }
+
+      json(res, {
+        status: 'ok',
+        db_connected: !!dbCheck,
+        kalshi_connected: kalshiConnected,
+        env: KALSHI_ENV,
+        safe_mode: cfg.safe_mode ?? true,
+        balance,
+      });
+    } catch (err) {
+      json(res, { status: 'error', error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // KALSHI PROXY (all actions via POST)
+  // ════════════════════════════════════════
+
+  'POST /api/kalshi': async (req, res) => {
+    const body = await readBody(req);
+    const { action } = body;
+
+    if (!action) {
+      return json(res, { error: 'Missing action field' }, 400);
+    }
+
+    try {
+      switch (action) {
+
+        // ── Save API Keys ──
+        case 'save_keys': {
+          if (!body.api_key || !body.private_key) {
+            return json(res, { error: 'api_key and private_key (PEM) required' }, 400);
+          }
+          const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+          const row = current.rows[0] || {};
+          const newConfig = {
+            ...row.config,
+            kalshi_api_key: body.api_key,
+            kalshi_private_key: body.private_key,
+            kalshi_connected: true,
+            kalshi_connected_at: new Date().toISOString(),
+            kalshi_env: KALSHI_ENV,
+          };
+          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
+            JSON.stringify(newConfig), row.id,
+          ]);
+          json(res, {
+            success: true,
+            api_key: body.api_key.substring(0, 12) + '...',
+            env: KALSHI_ENV,
+            message: 'Kalshi API keys saved',
+          });
+          break;
+        }
+
+        // ── Test Connection ──
+        case 'test_connection': {
+          try {
+            const data = await kalshiPublicFetch('GET', '/exchange/status');
+            json(res, { success: true, exchange_status: data, env: KALSHI_ENV });
+          } catch (err) {
+            json(res, { success: false, error: err.message }, 500);
+          }
+          break;
+        }
+
+        // ── Get Balance ──
+        case 'get_balance': {
+          const data = await kalshiFetch('GET', '/portfolio/balance');
+          json(res, { success: true, balance: data });
+          break;
+        }
+
+        // ── Get Markets (public) ──
+        case 'get_markets': {
+          const params = {};
+          if (body.category) params.category = body.category;
+          if (body.status) params.status = body.status;
+          if (body.limit) params.limit = body.limit;
+          if (body.cursor) params.cursor = body.cursor;
+          if (body.series_ticker) params.series_ticker = body.series_ticker;
+          if (body.event_ticker) params.event_ticker = body.event_ticker;
+          const qs = buildQueryString(params);
+          const data = await kalshiPublicFetch('GET', `/markets${qs}`);
+          json(res, { success: true, ...data });
+          break;
+        }
+
+        // ── Get Single Market ──
+        case 'get_market': {
+          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
+          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}`);
+          json(res, { success: true, market: data });
+          break;
+        }
+
+        // ── Get Orderbook ──
+        case 'get_orderbook': {
+          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
+          const qs = body.depth ? `?depth=${body.depth}` : '';
+          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}/orderbook${qs}`);
+          json(res, { success: true, orderbook: data });
+          break;
+        }
+
+        // ── Get Trades (public market trades) ──
+        case 'get_trades': {
+          if (!body.ticker) return json(res, { error: 'ticker required' }, 400);
+          const params = {};
+          if (body.limit) params.limit = body.limit;
+          if (body.cursor) params.cursor = body.cursor;
+          const qs = buildQueryString(params);
+          const data = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(body.ticker)}/trades${qs}`);
+          json(res, { success: true, ...data });
+          break;
+        }
+
+        // ── Get Events ──
+        case 'get_events': {
+          // Accept flat fields or nested in body.params (BrowseMarkets sends params wrapper)
+          const src = body.params || body;
+          const params = {};
+          if (src.status) params.status = src.status;
+          if (src.limit) params.limit = src.limit;
+          if (src.cursor) params.cursor = src.cursor;
+          if (src.series_ticker) params.series_ticker = src.series_ticker;
+          if (src.with_nested_markets !== undefined) params.with_nested_markets = src.with_nested_markets;
+          const qs = buildQueryString(params);
+          const data = await kalshiPublicFetch('GET', `/events${qs}`);
+          json(res, { success: true, ...data });
+          break;
+        }
+
+        // ── Get Single Event ──
+        case 'get_event': {
+          if (!body.event_ticker) return json(res, { error: 'event_ticker required' }, 400);
+          const qs = body.with_nested_markets ? '?with_nested_markets=true' : '';
+          const data = await kalshiPublicFetch('GET', `/events/${encodeURIComponent(body.event_ticker)}${qs}`);
+          json(res, { success: true, event: data });
+          break;
+        }
+
+        // ── Get Series ──
+        case 'get_series': {
+          if (!body.series_ticker) return json(res, { error: 'series_ticker required' }, 400);
+          const data = await kalshiPublicFetch('GET', `/series/${encodeURIComponent(body.series_ticker)}`);
+          json(res, { success: true, series: data });
+          break;
+        }
+
+        // ── Get Positions (from portfolio trades DB, or real Kalshi API if keys configured) ──
+        case 'get_positions': {
+          {
+            // No API keys — return portfolio trades from DB
+            const { rows: posRows } = await kenQ(`
+              SELECT pt.market_id as ticker, pt.market_title, pt.direction,
+                COUNT(*) FILTER (WHERE pt.status = 'open') as open_count,
+                SUM(pt.contracts) FILTER (WHERE pt.status = 'open') as open_contracts,
+                SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') as open_cost,
+                ROUND(AVG(pt.entry_price_cents) FILTER (WHERE pt.status = 'open')) as avg_price,
+                COUNT(*) as total_trades,
+                SUM(COALESCE(pt.pnl_cents, 0)) FILTER (WHERE pt.status != 'open') as realized_pnl
+              FROM ken_portfolio_trades pt
+              WHERE pt.created_at > NOW() - INTERVAL '24 hours'
+              GROUP BY pt.market_id, pt.market_title, pt.direction
+              HAVING COUNT(*) FILTER (WHERE pt.status = 'open') > 0
+              ORDER BY SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') DESC NULLS LAST
+            `);
+            const positions = posRows.map(r => ({
+              ticker: r.ticker,
+              market_ticker: r.ticker,
+              title: r.market_title || r.ticker,
+              market_exposure: r.direction === 'BUY_YES' ? parseInt(r.open_contracts || 0) : -parseInt(r.open_contracts || 0),
+              position: r.direction === 'BUY_YES' ? parseInt(r.open_contracts || 0) : -parseInt(r.open_contracts || 0),
+              position_cost: parseInt(r.open_cost || 0) * 100,
+              realized_pnl: parseInt(r.realized_pnl || 0) * 100,
+              fees_paid: 0,
+              avg_price_cents: parseInt(r.avg_price || 0),
+              open_trades: parseInt(r.open_count || 0),
+              total_trades: parseInt(r.total_trades || 0),
+            }));
+            json(res, { success: true, positions, market_positions: positions, is_paper: true });
+          }
+          break;
+        }
+
+        // ── Get Fills (auth required) ──
+        case 'get_fills': {
+          const params = {};
+          if (body.limit) params.limit = body.limit;
+          if (body.cursor) params.cursor = body.cursor;
+          if (body.ticker) params.ticker = body.ticker;
+          if (body.order_id) params.order_id = body.order_id;
+          const qs = buildQueryString(params);
+          const data = await kalshiFetch('GET', `/portfolio/fills${qs}`);
+          json(res, { success: true, ...data });
+          break;
+        }
+
+        // ── Get Orders (auth required) ──
+        case 'get_orders': {
+          const params = {};
+          if (body.limit) params.limit = body.limit;
+          if (body.cursor) params.cursor = body.cursor;
+          if (body.ticker) params.ticker = body.ticker;
+          if (body.status) params.status = body.status;
+          const qs = buildQueryString(params);
+          const data = await kalshiFetch('GET', `/portfolio/orders${qs}`);
+          json(res, { success: true, ...data });
+          break;
+        }
+
+        // ── Create Order (auth required) ──
+        case 'create_order': {
+          if (!body.ticker || !body.side || !body.action || !body.type || !body.count) {
+            return json(res, { error: 'ticker, side, action, type, and count are required' }, 400);
+          }
+          const orderPayload = {
+            ticker: body.ticker,
+            side: body.side,       // 'yes' or 'no'
+            action: body.action,   // 'buy' or 'sell'
+            type: body.type,       // 'limit' or 'market'
+            count: body.count,
+          };
+          if (body.yes_price !== undefined) orderPayload.yes_price = body.yes_price;
+          if (body.no_price !== undefined) orderPayload.no_price = body.no_price;
+          if (body.client_order_id) orderPayload.client_order_id = body.client_order_id;
+          if (body.time_in_force) orderPayload.time_in_force = body.time_in_force;
+          if (body.expiration_ts) orderPayload.expiration_ts = body.expiration_ts;
+
+          const data = await kalshiFetch('POST', '/portfolio/orders', orderPayload);
+          json(res, { success: true, order: data });
+          break;
+        }
+
+        // ── Cancel Order (auth required) ──
+        case 'cancel_order': {
+          if (!body.order_id) return json(res, { error: 'order_id required' }, 400);
+          const data = await kalshiFetch('DELETE', `/portfolio/orders/${encodeURIComponent(body.order_id)}`);
+          json(res, { success: true, result: data });
+          break;
+        }
+
+        // ── Get Single Order (auth required) ──
+        case 'get_order': {
+          if (!body.order_id) return json(res, { error: 'order_id required' }, 400);
+          const data = await kalshiFetch('GET', `/portfolio/orders/${encodeURIComponent(body.order_id)}`);
+          json(res, { success: true, order: data });
+          break;
+        }
+
+        // ── Auto-Trader Status ──
+        case 'trader_status': {
+          const state = await getTraderState();
+          const allTrades = await kenQ('SELECT * FROM ken_trades ORDER BY created_at DESC LIMIT 50');
+          const closedTrades = (allTrades.rows || []).filter(t => t.status !== 'open');
+          const totalPnL = closedTrades.reduce((sum, t) => sum + (t.pnl_cents || 0), 0);
+          const wins = closedTrades.filter(t => (t.pnl_cents || 0) > 0).length;
+          const losses = closedTrades.filter(t => (t.pnl_cents || 0) < 0).length;
+          json(res, {
+            success: true,
+            config: TRADE_CONFIG,
+            state: {
+              open_positions: state.openCount,
+              total_invested: state.totalInvested,
+              available_budget: state.available,
+              daily_loss: state.dailyLoss,
+            },
+            positions: state.openPositions,
+            stats: {
+              total_trades: closedTrades.length,
+              wins, losses,
+              win_rate: closedTrades.length > 0 ? Math.round(wins / closedTrades.length * 100) : 0,
+              total_pnl_cents: totalPnL,
+              total_pnl_dollars: (totalPnL / 100).toFixed(2),
+            },
+            recent_trades: (allTrades.rows || []).slice(0, 20),
+          });
+          break;
+        }
+
+        // ── Toggle Auto-Trader ──
+        case 'trader_toggle': {
+          TRADE_CONFIG.enabled = !TRADE_CONFIG.enabled;
+          saveTradeConfig();
+          json(res, { success: true, enabled: TRADE_CONFIG.enabled, message: `Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'}` });
+          console.log(`[Ken] Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'} via API`);
+          break;
+        }
+
+        // ── Run Predictions (weather model pipeline) ──
+        case 'run_predictions': {
+          // Step 1: Get weather events from Kalshi
+          let events = [];
+          try {
+            const evData = await kalshiPublicFetch('GET', '/events?status=open&limit=100');
+            events = (evData.events || []).filter(e => {
+              const t = ((e.category || '') + ' ' + (e.title || '')).toLowerCase();
+              return t.includes('weather') || t.includes('temperature') || t.includes('temp ') ||
+                     t.includes('rain') || t.includes('snow') || t.includes('precipitation') ||
+                     t.includes('hurricane') || t.includes('wind') || t.includes('°f');
+            });
+          } catch { /* no events available - use weather cache */ }
+
+          // Step 2: Load cached weather data
+          const riskRow = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+          const rCfg = riskRow.rows[0]?.config || {};
+          const weatherCache = rCfg.weather_cache?.results || [];
+          const month = new Date().getMonth() + 1;
+
+          // Step 3: Generate predictions (only for real weather events with a city match)
+          const predictions = [];
+          for (const evt of events) {
+            const city = extractCity(evt.title);
+            if (!city) continue; // Skip non-geographic events (climate policy, earthquakes, etc.)
+            const spec = parseWeatherEvent(evt.title);
+            if (!spec || spec.variable === 'unknown') continue; // Skip if we can't parse a weather variable
+            const cityWeather = weatherCache.find(w => w.city === city);
+            if (!cityWeather) continue; // Skip if no weather data for this city
+            const features = cityWeather[spec.variable === 'snow' ? 'precipitation' : spec.variable] || cityWeather.temperature;
+
+            if (features && features.values?.length) {
+              const rawProb = ensembleExceedance(features, spec.threshold, spec.operator);
+              const prior = climatologicalPrior(spec.variable, spec.threshold, month);
+              const combined = bayesianUpdate(prior, rawProb ?? 0.5);
+              const pYes = Math.max(0.01, Math.min(0.99, combined));
+
+              // Get market price for edge calc
+              let marketPrice = 50;
+              try {
+                const mkts = await kalshiPublicFetch('GET', `/events/${encodeURIComponent(evt.event_ticker)}?with_nested_markets=true`);
+                const mkt = mkts.event?.markets?.[0] || mkts.markets?.[0];
+                if (mkt) {
+                  const p = mkt.yes_ask || mkt.last_price || 50;
+                  if (p > 0 && p < 100) marketPrice = p; // Skip fully priced markets
+                }
+              } catch {}
+
+              const edgeBps = Math.round((pYes * 100 - marketPrice) * 100);
+              predictions.push({
+                event_ticker: evt.event_ticker,
+                ticker: evt.title?.substring(0, 60),
+                market: evt.title,
+                city,
+                side: edgeBps > 0 ? 'yes' : 'no',
+                p_yes: Math.round(pYes * 1000) / 10,
+                price_cents: marketPrice,
+                edge_bps: edgeBps,
+                method: 'ensemble_bayesian_v1',
+                variable: spec.variable,
+                threshold: spec.threshold,
+                data_points: features.values.length,
+              });
+            }
+          }
+
+          // If no Kalshi events but we have weather data, generate synthetic predictions
+          if (predictions.length === 0 && weatherCache.length > 0) {
+            for (const wd of weatherCache.filter(w => !w.error)) {
+              if (wd.temperature?.values?.length) {
+                const maxT = wd.temperature.max;
+                const threshold = Math.round(maxT / 5) * 5;
+                const rawProb = ensembleExceedance(wd.temperature, threshold, '>=');
+                const prior = climatologicalPrior('temperature', threshold, month);
+                const combined = bayesianUpdate(prior, rawProb ?? 0.5);
+                const pYes = Math.max(0.01, Math.min(0.99, combined));
+                predictions.push({
+                  ticker: `${wd.city} temp >= ${threshold}°F`,
+                  market: `Will the high temperature in ${wd.city} reach ${threshold}°F?`,
+                  city: wd.city,
+                  side: pYes > 0.5 ? 'yes' : 'no',
+                  p_yes: Math.round(pYes * 1000) / 10,
+                  price_cents: 50,
+                  edge_bps: Math.round((pYes * 100 - 50) * 100),
+                  method: 'synthetic_forecast',
+                  variable: 'temperature',
+                  threshold,
+                  data_points: wd.temperature.values.length,
+                });
+              }
+            }
+          }
+
+          predictions.sort((a, b) => Math.abs(b.edge_bps) - Math.abs(a.edge_bps));
+          const buySignals = predictions.filter(p => p.edge_bps > 0);
+          const avgEdge = predictions.length ? Math.round(predictions.reduce((s, p) => s + p.edge_bps, 0) / predictions.length) : 0;
+
+          json(res, {
+            success: true,
+            predictions,
+            buy_signals: buySignals.length,
+            avg_edge_bps: avgEdge,
+            total_events_scanned: events.length,
+            weather_cities: weatherCache.filter(w => !w.error).length,
+          });
+          break;
+        }
+
+        // ── Disconnect: remove keys from config ──
+        case 'disconnect': {
+          const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+          const row = current.rows[0] || {};
+          const newConfig = {
+            ...row.config,
+            kalshi_api_key: null,
+            kalshi_private_key: null,
+            kalshi_connected: false,
+            kalshi_connected_at: null,
+          };
+          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
+            JSON.stringify(newConfig), row.id,
+          ]);
+          json(res, { success: true, message: 'Kalshi keys removed, disconnected' });
+          break;
+        }
+
+        // ── Status: show current connection config (masked) ──
+        case 'status': {
+          const current = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+          const cfg = current.rows[0]?.config || {};
+          json(res, {
+            connected: !!cfg.kalshi_connected,
+            api_key: cfg.kalshi_api_key ? cfg.kalshi_api_key.substring(0, 12) + '...' : null,
+            has_private_key: !!cfg.kalshi_private_key,
+            connected_at: cfg.kalshi_connected_at || null,
+            env: KALSHI_ENV,
+          });
+          break;
+        }
+
+        default:
+          json(res, {
+            error: `Unknown action: ${action}`,
+            available_actions: [
+              'save_keys', 'test_connection', 'get_balance',
+              'get_markets', 'get_market', 'get_orderbook', 'get_trades',
+              'get_events', 'get_event', 'get_series',
+              'get_positions', 'get_fills', 'get_orders',
+              'create_order', 'cancel_order', 'get_order',
+              'run_predictions', 'disconnect', 'status',
+            ],
+          }, 400);
+      }
+    } catch (err) {
+      json(res, { success: false, error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // WEATHER ENDPOINTS
+  // ════════════════════════════════════════
+
+  'POST /api/weather': async (req, res) => {
+    const body = await readBody(req);
+
+    if (body.action === 'fetch_all') {
+      try {
+        const cities = body.cities || ['New York', 'Chicago', 'Miami', 'Denver', 'Los Angeles', 'Seattle'];
+        const useNws = body.force_nws === true; // opt-in to real NWS (flaky from this server)
+
+        // ── Generate synthetic seasonal forecast data (fast, reliable) ──
+        function generateSyntheticCities(cityList) {
+          const month = new Date().getMonth();
+          const results = [];
+          let totalDP = 0;
+          for (const city of cityList) {
+            const coords = CITY_COORDS[city]; if (!coords) continue;
+            const baseTemp = [35,38,48,58,68,78,85,83,75,62,48,38][month] + (coords.lat < 35 ? 15 : coords.lat > 42 ? -8 : 0);
+            const temps = Array.from({length: 14}, (_, i) => baseTemp + Math.round((Math.random() - 0.5) * 12) + (i < 7 ? 3 : -2));
+            const precips = Array.from({length: 14}, () => Math.round(Math.random() * 60));
+            const winds = Array.from({length: 14}, () => 5 + Math.round(Math.random() * 15));
+            const mkStat = (arr) => ({ mean: arr.reduce((a,b)=>a+b)/arr.length, min: Math.min(...arr), max: Math.max(...arr), latest: arr[0] });
+            results.push({
+              city, synthetic: true,
+              temperature: { variable: 'temperature', unit: 'F', values: temps, ...mkStat(temps) },
+              precipitation: { variable: 'precipitation', unit: 'percent', values: precips, ...mkStat(precips) },
+              wind: { variable: 'wind', unit: 'mph', values: winds, ...mkStat(winds) },
+              data_points: 42,
+            });
+            totalDP += 42;
+          }
+          return { results, totalDP };
+        }
+
+        let responseData;
+
+        if (useNws) {
+          // Real NWS path — wrapped in 15s global timeout, falls back to synthetic
+          const nwsResult = await Promise.race([
+            (async () => {
+              const results = [];
+              let dp = 0;
+              const mets = new Set();
+              for (const city of cities) {
+                const coords = CITY_COORDS[city]; if (!coords) continue;
+                try {
+                  const grid = await nwsFetch(`${NWS_BASE}/points/${coords.lat},${coords.lon}`);
+                  const forecast = await nwsFetch(`${NWS_BASE}/gridpoints/${grid.properties.gridId}/${grid.properties.gridX},${grid.properties.gridY}/forecast`);
+                  const tempF = extractForecastFeatures(forecast, 'temperature');
+                  const precip = extractForecastFeatures(forecast, 'precipitation');
+                  const wind = extractForecastFeatures(forecast, 'wind');
+                  const cdp = (tempF?.values?.length||0) + (precip?.values?.length||0) + (wind?.values?.length||0);
+                  dp += cdp;
+                  if (tempF) mets.add('temperature'); if (precip) mets.add('precipitation'); if (wind) mets.add('wind');
+                  results.push({ city, temperature: tempF, precipitation: precip, wind, data_points: cdp });
+                } catch (e) { results.push({ city, error: e.message }); }
+              }
+              return { results, totalDP: dp, metrics: [...mets], synthetic: false };
+            })(),
+            new Promise(resolve => setTimeout(() => resolve(null), 15000)),
+          ]);
+
+          if (nwsResult && nwsResult.results.some(r => !r.error)) {
+            responseData = nwsResult;
+          } else {
+            const syn = generateSyntheticCities(cities);
+            responseData = { ...syn, metrics: ['temperature','precipitation','wind'], synthetic: true, note: 'NWS timed out — using synthetic' };
+          }
+        } else {
+          // Default: synthetic data (instant, always works)
+          const syn = generateSyntheticCities(cities);
+          responseData = { ...syn, metrics: ['temperature','precipitation','wind'], synthetic: true, note: 'Using seasonal synthetic forecasts (pass force_nws:true for real NWS data)' };
+        }
+
+        // Cache in DB
+        const row = (await q('SELECT id, config FROM risk_state ORDER BY updated_at DESC LIMIT 1')).rows[0];
+        if (row) {
+          const newCfg = { ...(row.config || {}), weather_cache: { results: responseData.results, fetched_at: new Date().toISOString(), synthetic: responseData.synthetic } };
+          await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newCfg), row.id]);
+        }
+
+        json(res, {
+          success: true,
+          synthetic: responseData.synthetic,
+          stations: responseData.results.length,
+          data_points: responseData.totalDP,
+          metrics: responseData.metrics,
+          cities: responseData.results,
+          fetched_at: new Date().toISOString(),
+          note: responseData.note || undefined,
+        });
+      } catch (err) {
+        json(res, { success: false, error: err.message }, 500);
+      }
+    } else {
+      json(res, { error: `Unknown weather action: ${body.action}` }, 400);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // RISK ENDPOINTS
+  // ════════════════════════════════════════
+
+  'POST /api/risk': async (req, res) => {
+    const body = await readBody(req);
+
+    try {
+      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const row = current.rows[0] || {};
+      const cfg = row.config || {};
+
+      if (body.action === 'get_config') {
+        json(res, {
+          success: true,
+          config: {
+            safe_mode: cfg.safe_mode ?? true,
+            risk_profile: cfg.risk_profile ?? 'conservative',
+            max_position_size: cfg.max_position_size ?? 100,
+            max_contracts_per_market: cfg.max_contracts_per_market ?? 25,
+            max_dollars_at_risk: cfg.max_dollars_at_risk ?? 250,
+            max_daily_loss: cfg.max_daily_loss ?? cfg.daily_loss_cap ?? 50,
+            daily_loss_cap: cfg.daily_loss_cap ?? 50,
+            max_open_orders: cfg.max_open_orders ?? 10,
+            max_exposure: cfg.max_exposure ?? 500,
+            stop_loss_pct: cfg.stop_loss_pct ?? 20,
+            auto_hedge: cfg.auto_hedge ?? false,
+            allowed_categories: cfg.allowed_categories ?? [],
+            kill_switch_active: row.kill_switch_active || false,
+            kill_reason: row.kill_reason || null,
+            bankroll: parseFloat(row.bankroll || 0),
+            daily_pnl: parseFloat(row.daily_pnl || 0),
+            open_exposure: parseFloat(row.open_exposure || 0),
+          },
+        });
+      } else if (body.action === 'update_config') {
+        // Accept flat fields (from RiskProfile step) or wrapped in config object
+        const updates = body.config || {};
+        if (body.max_contracts_per_market !== undefined) updates.max_contracts_per_market = body.max_contracts_per_market;
+        if (body.max_dollars_at_risk !== undefined) updates.max_dollars_at_risk = body.max_dollars_at_risk;
+        if (body.daily_loss_cap !== undefined) updates.daily_loss_cap = body.daily_loss_cap;
+        if (body.risk_profile !== undefined) updates.risk_profile = body.risk_profile;
+        if (body.safe_mode !== undefined) updates.safe_mode = body.safe_mode;
+        const newConfig = { ...cfg, ...updates };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
+          JSON.stringify(newConfig), row.id,
+        ]);
+
+        // Also update top-level risk fields if provided
+        if (body.config?.kill_switch_active !== undefined) {
+          await q('UPDATE risk_state SET kill_switch_active = $1, kill_reason = $2, updated_at = NOW() WHERE id = $3', [
+            body.config.kill_switch_active,
+            body.config.kill_reason || null,
+            row.id,
+          ]);
+        }
+
+        const updated = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+        const u = updated.rows[0] || {};
+        json(res, {
+          success: true,
+          message: 'Risk config updated',
+          config: u.config || {},
+        });
+      } else {
+        json(res, { error: `Unknown risk action: ${body.action}` }, 400);
+      }
+    } catch (err) {
+      json(res, { success: false, error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // TRADING MODE CONTROL
+  // ════════════════════════════════════════
+
+  'POST /api/trading/mode': async (req, res) => {
+    const body = await readBody(req);
+    try {
+      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const row = current.rows[0] || {};
+      const cfg = row.config || {};
+
+      if (body.action === 'get') {
+        // Get current trading mode
+        const traderState = await getTraderState().catch(() => ({ openPositions: [], openCount: 0, totalInvested: 0, dailyLoss: 0, available: 0 }));
+        return json(res, {
+          env: KALSHI_ENV,
+          safe_mode: cfg.safe_mode ?? true,
+          kalshi_connected: !!(cfg.kalshi_api_key && cfg.kalshi_private_key),
+          trade_config: TRADE_CONFIG,
+          trader_state: traderState,
+          auto_trader_enabled: TRADE_CONFIG.enabled,
+        });
+      }
+
+      if (body.action === 'switch_env') {
+        const newEnv = body.env === 'prod' ? 'prod' : 'demo';
+        const newConfig = { ...cfg, kalshi_env: newEnv };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+        KALSHI_ENV = newEnv;
+        KALSHI_BASE_URL = KALSHI_BASE_URLS[newEnv];
+        console.log(`[Ken] Environment switched to ${newEnv.toUpperCase()} (${KALSHI_BASE_URL})`);
+        return json(res, { success: true, env: newEnv, base_url: KALSHI_BASE_URL });
+      }
+
+      if (body.action === 'toggle_safe_mode') {
+        const newSafe = !(cfg.safe_mode ?? true);
+        const newConfig = { ...cfg, safe_mode: newSafe };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+        console.log(`[Ken] Safe mode ${newSafe ? 'ENABLED' : 'DISABLED'}`);
+        return json(res, { success: true, safe_mode: newSafe });
+      }
+
+      if (body.action === 'toggle_auto_trader') {
+        TRADE_CONFIG.enabled = !TRADE_CONFIG.enabled;
+        saveTradeConfig();
+        console.log(`[Ken] Auto-trader ${TRADE_CONFIG.enabled ? 'ENABLED' : 'DISABLED'}`);
+        return json(res, { success: true, auto_trader_enabled: TRADE_CONFIG.enabled });
+      }
+
+      if (body.action === 'update_trade_config') {
+        if (body.budget_cents !== undefined) TRADE_CONFIG.budget_cents = Math.max(100, Math.min(100000, body.budget_cents));
+        if (body.max_position_cents !== undefined) TRADE_CONFIG.max_position_cents = Math.max(50, Math.min(10000, body.max_position_cents));
+        if (body.max_open_positions !== undefined) TRADE_CONFIG.max_open_positions = Math.max(1, Math.min(20, body.max_open_positions));
+        if (body.min_confidence !== undefined) TRADE_CONFIG.min_confidence = Math.max(50, Math.min(99, body.min_confidence));
+        if (body.daily_loss_limit_cents !== undefined) TRADE_CONFIG.daily_loss_limit_cents = Math.max(100, Math.min(50000, body.daily_loss_limit_cents));
+        saveTradeConfig();
+        return json(res, { success: true, trade_config: TRADE_CONFIG });
+      }
+
+      json(res, { error: `Unknown trading mode action: ${body.action}` }, 400);
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // TRADING HISTORY & P&L
+  // ════════════════════════════════════════
+
+  'GET /api/trading/history': async (req, res) => {
+    try {
+      const { rows: trades } = await kenQ('SELECT * FROM ken_trades ORDER BY created_at DESC LIMIT 100');
+      const { rows: [summary] } = await kenQ(`
+        SELECT
+          COUNT(*) as total_trades,
+          COUNT(*) FILTER (WHERE status = 'open') as open_trades,
+          COUNT(*) FILTER (WHERE pnl_cents > 0) as winning_trades,
+          COUNT(*) FILTER (WHERE pnl_cents < 0) as losing_trades,
+          COALESCE(SUM(pnl_cents), 0) as total_pnl,
+          COALESCE(SUM(cost_cents) FILTER (WHERE status = 'open'), 0) as open_exposure
+        FROM ken_trades
+      `);
+      json(res, { trades, summary });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // PAPER TRADING
+  // ════════════════════════════════════════
+
+  'POST /api/paper': async (req, res) => {
+    const body = await readBody(req);
+
+    try {
+      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const row = current.rows[0] || {};
+      const cfg = row.config || {};
+      const paperTrades = cfg.paper_trades || [];
+
+      if (body.action === 'submit_order') {
+        if (!body.ticker || !body.side || !body.count) {
+          return json(res, { error: 'ticker, side, and count required' }, 400);
+        }
+
+        const orderId = `paper_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
+        const fillPrice = body.yes_price || body.no_price || body.price || 50; // cents
+
+        const paperOrder = {
+          order_id: orderId,
+          ticker: body.ticker,
+          side: body.side,           // 'yes' or 'no'
+          action: body.action_type || 'buy',
+          type: body.type || 'market',
+          count: body.count,
+          price: fillPrice,
+          cost: fillPrice * body.count, // in cents
+          status: 'filled',
+          created_at: new Date().toISOString(),
+          filled_at: new Date().toISOString(),
+          is_paper: true,
+        };
+
+        paperTrades.push(paperOrder);
+
+        const newConfig = { ...cfg, paper_trades: paperTrades };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
+          JSON.stringify(newConfig), row.id,
+        ]);
+
+        json(res, { success: true, order: paperOrder, message: 'Paper order filled' });
+
+      } else if (body.action === 'get_positions') {
+        // Aggregate from ken_portfolio_trades (live hyper-trading data)
+        try {
+          const { rows } = await kenQ(`
+            SELECT pt.market_id as ticker, pt.market_title, pt.direction,
+              SUM(pt.contracts) as total_contracts,
+              SUM(pt.cost_cents) as position_cost,
+              ROUND(AVG(pt.entry_price_cents)) as avg_price,
+              COUNT(*) as trades,
+              SUM(CASE WHEN pt.status != 'open' THEN pt.pnl_cents ELSE 0 END) as realized_pnl
+            FROM ken_portfolio_trades pt
+            WHERE pt.created_at > NOW() - INTERVAL '24 hours'
+            GROUP BY pt.market_id, pt.market_title, pt.direction
+            ORDER BY SUM(pt.cost_cents) DESC
+          `);
+          const positions = rows.map(r => ({
+            ticker: r.ticker,
+            market_ticker: r.ticker,
+            title: r.market_title,
+            side: r.direction === 'BUY_YES' ? 'yes' : 'no',
+            market_exposure: r.direction === 'BUY_YES' ? parseInt(r.total_contracts) : -parseInt(r.total_contracts),
+            position: r.direction === 'BUY_YES' ? parseInt(r.total_contracts) : -parseInt(r.total_contracts),
+            position_cost: parseInt(r.position_cost) * 100,
+            avg_price_cents: parseInt(r.avg_price),
+            realized_pnl: parseInt(r.realized_pnl || 0) * 100,
+            fees_paid: 0,
+            trades: parseInt(r.trades),
+          }));
+          json(res, { success: true, positions, is_paper: true });
+        } catch (e) {
+          // Fallback to old paper trades
+          const positionMap = {};
+          for (const trade of paperTrades) {
+            const key = `${trade.ticker}_${trade.side}`;
+            if (!positionMap[key]) positionMap[key] = { ticker: trade.ticker, side: trade.side, total_contracts: 0, avg_price: 0, total_cost: 0, trades: 0 };
+            const pos = positionMap[key];
+            if (trade.action === 'buy') { pos.total_cost += trade.price * trade.count; pos.total_contracts += trade.count; }
+            else { pos.total_cost -= trade.price * trade.count; pos.total_contracts -= trade.count; }
+            pos.trades++;
+            pos.avg_price = pos.total_contracts > 0 ? Math.round(pos.total_cost / pos.total_contracts) : 0;
+          }
+          const positions = Object.values(positionMap).filter(p => p.total_contracts !== 0);
+          json(res, { success: true, positions, is_paper: true });
+        }
+
+      } else if (body.action === 'get_fills') {
+        // Return paper trade history
+        const limit = body.limit || 50;
+        const fills = paperTrades.slice(-limit).reverse();
+        json(res, { success: true, fills, total: paperTrades.length, is_paper: true });
+
+      } else if (body.action === 'clear') {
+        // Clear all paper trades
+        const newConfig = { ...cfg, paper_trades: [] };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [
+          JSON.stringify(newConfig), row.id,
+        ]);
+        json(res, { success: true, message: 'Paper trades cleared' });
+
+      } else {
+        json(res, { error: `Unknown paper action: ${body.action}` }, 400);
+      }
+    } catch (err) {
+      json(res, { success: false, error: err.message }, 500);
+    }
+  },
+
+  // ── Positions Detail API (collapsible tables) ──
+  'GET /api/positions': async (req, res) => {
+    try {
+      // All positions with full stats
+      const { rows: positions } = await kenQ(`
+        SELECT
+          pt.market_id AS ticker,
+          pt.direction,
+          pt.market_title AS title,
+          COUNT(*) FILTER (WHERE pt.status = 'open') AS open_trades,
+          SUM(pt.contracts) FILTER (WHERE pt.status = 'open') AS open_contracts,
+          SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') AS open_cost_cents,
+          ROUND(AVG(pt.entry_price_cents) FILTER (WHERE pt.status = 'open')) AS avg_entry_price,
+          SUM(COALESCE(pt.pnl_cents, 0)) FILTER (WHERE pt.status != 'open') AS realized_pnl_cents,
+          COUNT(*) AS total_trades,
+          COUNT(*) FILTER (WHERE pt.status != 'open') AS closed_trades,
+          MIN(pt.created_at) AS first_trade_at,
+          MAX(pt.created_at) AS last_trade_at,
+          MIN(pt.entry_price_cents) FILTER (WHERE pt.status = 'open') AS min_entry,
+          MAX(pt.entry_price_cents) FILTER (WHERE pt.status = 'open') AS max_entry
+        FROM ken_portfolio_trades pt
+        GROUP BY pt.market_id, pt.direction, pt.market_title
+        HAVING COUNT(*) FILTER (WHERE pt.status = 'open') > 0
+        ORDER BY SUM(pt.cost_cents) FILTER (WHERE pt.status = 'open') DESC NULLS LAST
+      `);
+
+      // Category grouping by market event prefix
+      const grouped = {};
+      for (const p of positions) {
+        const prefix = (p.ticker || '').split('-')[0];
+        if (!grouped[prefix]) grouped[prefix] = { event: prefix, title: p.title, positions: [], totalCost: 0, totalPnl: 0, totalContracts: 0 };
+        grouped[prefix].positions.push(p);
+        grouped[prefix].totalCost += parseInt(p.open_cost_cents || 0);
+        grouped[prefix].totalPnl += parseInt(p.realized_pnl_cents || 0);
+        grouped[prefix].totalContracts += parseInt(p.open_contracts || 0);
+      }
+
+      // Summary stats
+      const totalCost = positions.reduce((s, p) => s + parseInt(p.open_cost_cents || 0), 0);
+      const totalPnl = positions.reduce((s, p) => s + parseInt(p.realized_pnl_cents || 0), 0);
+      const totalContracts = positions.reduce((s, p) => s + parseInt(p.open_contracts || 0), 0);
+      const totalTrades = positions.reduce((s, p) => s + parseInt(p.total_trades || 0), 0);
+
+      json(res, {
+        success: true,
+        positions: positions.map(p => ({
+          ...p,
+          open_trades: parseInt(p.open_trades || 0),
+          open_contracts: parseInt(p.open_contracts || 0),
+          open_cost_cents: parseInt(p.open_cost_cents || 0),
+          avg_entry_price: parseInt(p.avg_entry_price || 0),
+          realized_pnl_cents: parseInt(p.realized_pnl_cents || 0),
+          total_trades: parseInt(p.total_trades || 0),
+          closed_trades: parseInt(p.closed_trades || 0),
+          min_entry: parseInt(p.min_entry || 0),
+          max_entry: parseInt(p.max_entry || 0),
+        })),
+        grouped: Object.values(grouped).sort((a, b) => b.totalCost - a.totalCost),
+        summary: { totalCost, totalPnl, totalContracts, totalTrades, positionCount: positions.length },
+      });
+    } catch (err) {
+      json(res, { success: false, error: err.message }, 500);
+    }
+  },
+
+  // Get trade history for a specific position
+  'POST /api/positions/trades': async (req, res) => {
+    try {
+      const body = await readBody(req);
+      const { ticker, direction, limit: lim } = body;
+      if (!ticker) return json(res, { error: 'ticker required' }, 400);
+
+      let sql = `SELECT * FROM ken_portfolio_trades WHERE market_id = $1`;
+      const params = [ticker];
+      if (direction) { sql += ` AND direction = $2`; params.push(direction); }
+      sql += ` ORDER BY created_at DESC LIMIT ${Math.min(parseInt(lim) || 100, 500)}`;
+
+      const { rows } = await kenQ(sql, params);
+      json(res, { success: true, trades: rows });
+    } catch (err) {
+      json(res, { success: false, error: err.message }, 500);
+    }
+  },
+
+  // ── Signal Pipeline API ──
+  'GET /api/signals': async (req, res) => {
+    try {
+      const { rows: signals } = await kenQ(`
+        SELECT id, market_id, direction, expected_edge, size_recommendation, confidence, reasoning, sources, risk_approved, executed, created_at
+        FROM ken_strategy_signals
+        ORDER BY created_at DESC
+        LIMIT 50
+      `);
+      const { rows: shifts } = await kenQ(`
+        SELECT market_id, prior_probability, sentiment_impact, adjusted_probability, market_probability, edge, num_articles, created_at
+        FROM ken_probability_shifts
+        ORDER BY created_at DESC
+        LIMIT 50
+      `);
+      const { rows: recentSentiment } = await kenQ(`
+        SELECT event_tag, market_ticker, AVG(sentiment) as avg_sentiment, AVG(confidence) as avg_confidence, COUNT(*) as count
+        FROM ken_event_sentiment
+        WHERE created_at > NOW() - INTERVAL '1 hour'
+        GROUP BY event_tag, market_ticker
+        ORDER BY count DESC
+        LIMIT 30
+      `);
+      // Enrich with Kalshi URLs
+      const enrichWithUrl = (rows) => rows.map(r => {
+        const ticker = r.market_id || '';
+        const eventTicker = ticker.replace(/-[^-]+$/, '') || ticker;
+        const seriesTicker = eventTicker.replace(/-[^-]+$/, '') || eventTicker;
+        return { ...r, event_ticker: eventTicker, kalshi_url: `https://kalshi.com/markets/${seriesTicker.toLowerCase()}/${eventTicker.toLowerCase()}` };
+      });
+      json(res, {
+        signals: enrichWithUrl(signals),
+        probabilityShifts: enrichWithUrl(shifts),
+        eventSentiment: recentSentiment,
+        pipeline: {
+          scanInterval: '15 min',
+          signalInterval: '5 min',
+          intelligenceInterval: '30 min',
+          cachedMarkets: cachedActiveMarkets.length,
+          riskLimits: RISK_LIMITS,
+        },
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'GET /api/signals/dashboard': async (req, res) => {
+    try {
+      // Summary stats for the signal pipeline
+      const { rows: [stats] } = await kenQ(`
+        SELECT
+          COUNT(*) as total_signals,
+          COUNT(CASE WHEN risk_approved THEN 1 END) as approved_signals,
+          COUNT(CASE WHEN direction = 'BUY_YES' THEN 1 END) as buy_yes,
+          COUNT(CASE WHEN direction = 'BUY_NO' THEN 1 END) as buy_no,
+          AVG(expected_edge) as avg_edge,
+          AVG(confidence) as avg_confidence,
+          MAX(created_at) as latest_signal
+        FROM ken_strategy_signals
+        WHERE created_at > NOW() - INTERVAL '24 hours'
+      `);
+      const { rows: topSignals } = await kenQ(`
+        SELECT market_id, direction, expected_edge, confidence, reasoning, risk_approved, created_at
+        FROM ken_strategy_signals
+        WHERE created_at > NOW() - INTERVAL '4 hours' AND risk_approved = true
+        ORDER BY ABS(expected_edge) DESC
+        LIMIT 10
+      `);
+      const { rows: sourceBreakdown } = await kenQ(`
+        SELECT source, COUNT(*) as cnt, AVG(sentiment_score) as avg_sent
+        FROM ken_sentiment
+        WHERE captured_at > NOW() - INTERVAL '1 hour'
+        GROUP BY source
+        ORDER BY cnt DESC
+        LIMIT 20
+      `);
+      json(res, { stats, topSignals, sourceBreakdown, cachedMarkets: cachedActiveMarkets.length });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // INTELLIGENCE FEED
+  // ════════════════════════════════════════
+
+  'GET /api/intelligence': async (req, res) => {
+    try {
+      const { rows: recent } = await kenQ(`
+        SELECT source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, captured_at
+        FROM ken_sentiment
+        ORDER BY captured_at DESC
+        LIMIT 100
+      `);
+      const { rows: sourceCounts } = await kenQ(`
+        SELECT source, COUNT(*) as cnt, AVG(sentiment_score) as avg_sent,
+               MAX(captured_at) as latest
+        FROM ken_sentiment
+        WHERE captured_at > NOW() - INTERVAL '4 hours'
+        GROUP BY source ORDER BY cnt DESC
+      `);
+      const { rows: [totals] } = await kenQ(`
+        SELECT
+          COUNT(*) as total_items,
+          COUNT(*) FILTER (WHERE captured_at > NOW() - INTERVAL '1 hour') as last_hour,
+          COUNT(*) FILTER (WHERE captured_at > NOW() - INTERVAL '4 hours') as last_4h,
+          AVG(sentiment_score) FILTER (WHERE captured_at > NOW() - INTERVAL '1 hour') as avg_sentiment_1h
+        FROM ken_sentiment
+      `);
+      json(res, { recent, sourceCounts, totals });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // PREDICTIONS FEED
+  // ════════════════════════════════════════
+
+  'GET /api/predictions': async (req, res) => {
+    try {
+      const { rows } = await kenQ(`
+        SELECT ticker, event_ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources, created_at
+        FROM ken_predictions
+        ORDER BY created_at DESC
+        LIMIT 100
+      `);
+      json(res, { predictions: rows });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ── Live Markets (all active with snapshots) ──
+  'GET /api/markets': async (req, res) => {
+    try {
+      const { rows } = await kenQ(`
+        SELECT DISTINCT ON (ticker) ticker, event_ticker, title, subtitle, category,
+          yes_bid, yes_ask, no_bid, no_ask, last_price, volume, volume_24h, open_interest, spread,
+          captured_at
+        FROM ken_market_snapshots
+        WHERE captured_at > NOW() - INTERVAL '1 hour'
+        ORDER BY ticker, captured_at DESC
+      `);
+      json(res, { markets: rows, count: rows.length, cached: cachedActiveMarkets.length });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ── Energy/LNG Arbitrage Opportunities ──
+  'GET /api/energy-arb': async (req, res) => {
+    try {
+      const arbs = await energyArbitrageScan(cachedActiveMarkets);
+      // Also get recent energy-related signals
+      const { rows: energySignals } = await kenQ(`
+        SELECT ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources, created_at
+        FROM ken_predictions
+        WHERE signal_type IN ('energy_arb', 'polymarket_arb')
+           OR reasoning ILIKE '%energy%' OR reasoning ILIKE '%oil%' OR reasoning ILIKE '%gas%' OR reasoning ILIKE '%lng%'
+        ORDER BY created_at DESC LIMIT 50
+      `);
+      json(res, {
+        arbitrage_opportunities: arbs,
+        energy_signals: energySignals,
+        scan_time: new Date().toISOString(),
+        markets_scanned: cachedActiveMarkets.length,
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ── All Active Signals (for trading) ──
+  'GET /api/signals': async (req, res) => {
+    try {
+      const { rows } = await kenQ(`
+        SELECT market_id as ticker, direction, expected_edge, confidence, reasoning, sources, risk_approved, created_at
+        FROM ken_strategy_signals
+        WHERE created_at > NOW() - INTERVAL '2 hours'
+        ORDER BY created_at DESC LIMIT 200
+      `);
+      json(res, { signals: rows, count: rows.length });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // DASHBOARD — aggregated paper portfolio stats
+  // ════════════════════════════════════════
+
+  'GET /api/dashboard': async (req, res) => {
+    try {
+      // Aggregate balance across all portfolios
+      const { rows: [balRow] } = await kenQ(`
+        SELECT SUM(balance_cents) as total_balance,
+               SUM(total_invested_cents) as total_invested,
+               SUM(total_returned_cents) as total_returned
+        FROM ken_portfolios
+      `);
+
+      // Seed money + economics
+      const totalBalance = Number(balRow?.total_balance || 0);
+      const totalInvested = Number(balRow?.total_invested || 0);
+      const totalReturned = Number(balRow?.total_returned || 0);
+      const seedMoney = totalBalance + totalInvested - totalReturned; // always 5000000 cents ($50K)
+      const netPnl = totalBalance - seedMoney; // how much above/below seed
+      const roi = seedMoney > 0 ? ((totalBalance - seedMoney) / seedMoney * 100) : 0;
+
+      // Daily spend breakdown (last 7 days)
+      const { rows: spendRows } = await kenQ(`
+        SELECT resolved_at::date as day,
+               COUNT(*) as trades,
+               SUM(CASE WHEN pnl_cents > 0 THEN 1 ELSE 0 END) as wins,
+               SUM(CASE WHEN pnl_cents < 0 THEN 1 ELSE 0 END) as losses,
+               SUM(pnl_cents) as net_pnl,
+               SUM(CASE WHEN pnl_cents < 0 THEN ABS(pnl_cents) ELSE 0 END) as gross_losses,
+               SUM(CASE WHEN pnl_cents > 0 THEN pnl_cents ELSE 0 END) as gross_wins,
+               SUM(cost_cents) as capital_deployed
+        FROM ken_portfolio_trades
+        WHERE status != 'open' AND resolved_at IS NOT NULL
+        GROUP BY resolved_at::date ORDER BY day DESC LIMIT 7
+      `);
+
+      // Daily budget across all portfolios
+      const { rows: [budgetRow] } = await kenQ(`
+        SELECT SUM(daily_budget_cents) as total_daily_budget,
+               COUNT(*) as num_portfolios
+        FROM ken_portfolios
+      `);
+
+      // Open position cost (capital currently at risk)
+      const { rows: [riskRow] } = await kenQ(`
+        SELECT COALESCE(SUM(cost_cents), 0) as capital_at_risk,
+               COUNT(*) as open_positions
+        FROM ken_portfolio_trades WHERE status = 'open'
+      `);
+
+      // Open position count + today's P&L
+      const { rows: [posRow] } = await kenQ(`
+        SELECT
+          COUNT(*) FILTER (WHERE status = 'open') as open_count,
+          COUNT(DISTINCT market_id) FILTER (WHERE status = 'open') as unique_markets,
+          COALESCE(SUM(pnl_cents) FILTER (WHERE status != 'open' AND resolved_at::date = CURRENT_DATE), 0) as daily_pnl,
+          COALESCE(SUM(pnl_cents) FILTER (WHERE status != 'open'), 0) as total_pnl
+        FROM ken_portfolio_trades
+      `);
+
+      // Recent fills (last 10 resolved trades)
+      const { rows: fills } = await kenQ(`
+        SELECT market_id as ticker, market_title, direction,
+               contracts, entry_price_cents, pnl_cents, status, resolved_at
+        FROM ken_portfolio_trades
+        WHERE status != 'open'
+        ORDER BY resolved_at DESC LIMIT 10
+      `);
+
+      // P&L history — daily totals for last 14 days
+      const { rows: pnlHistory } = await kenQ(`
+        SELECT resolved_at::date as day, SUM(pnl_cents) as daily_pnl
+        FROM ken_portfolio_trades
+        WHERE status != 'open' AND resolved_at > NOW() - INTERVAL '14 days'
+        GROUP BY resolved_at::date
+        ORDER BY day ASC
+      `);
+
+      // Top winning markets
+      const { rows: topMarkets } = await kenQ(`
+        SELECT market_id, market_title,
+               COUNT(*) as total_trades,
+               SUM(CASE WHEN pnl_cents > 0 THEN 1 ELSE 0 END) as wins,
+               SUM(CASE WHEN pnl_cents < 0 THEN 1 ELSE 0 END) as losses,
+               SUM(pnl_cents) as pnl_cents,
+               MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
+               ROUND(AVG(entry_price_cents)) as avg_entry
+        FROM ken_portfolio_trades
+        WHERE status != 'open'
+        GROUP BY market_id, market_title
+        ORDER BY SUM(pnl_cents) DESC
+        LIMIT 10
+      `);
+
+      // Bottom losing markets
+      const { rows: worstMarkets } = await kenQ(`
+        SELECT market_id, market_title,
+               COUNT(*) as total_trades,
+               SUM(CASE WHEN pnl_cents > 0 THEN 1 ELSE 0 END) as wins,
+               SUM(CASE WHEN pnl_cents < 0 THEN 1 ELSE 0 END) as losses,
+               SUM(pnl_cents) as pnl_cents,
+               MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
+               ROUND(AVG(entry_price_cents)) as avg_entry
+        FROM ken_portfolio_trades
+        WHERE status != 'open'
+        GROUP BY market_id, market_title
+        HAVING SUM(pnl_cents) < 0
+        ORDER BY SUM(pnl_cents) ASC
+        LIMIT 5
+      `);
+
+      // Top/bottom strategies
+      const { rows: strategies } = await kenQ(`
+        SELECT p.strategy, p.name,
+               p.trades_won, p.trades_lost,
+               (p.total_returned_cents - p.total_invested_cents) as net_pnl_cents,
+               p.balance_cents,
+               ROUND(100.0 * p.trades_won / NULLIF(p.trades_won + p.trades_lost, 0), 1) as win_rate
+        FROM ken_portfolios p
+        WHERE p.total_invested_cents > 0
+        ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC
+      `);
+
+      // Today's stats
+      const { rows: [todayRow] } = await kenQ(`
+        SELECT COUNT(*) as trades,
+               SUM(CASE WHEN pnl_cents > 0 THEN 1 ELSE 0 END) as wins,
+               SUM(CASE WHEN pnl_cents < 0 THEN 1 ELSE 0 END) as losses
+        FROM ken_portfolio_trades
+        WHERE status != 'open' AND resolved_at::date = CURRENT_DATE
+      `);
+
+      // ── Top 3 Best Opportunities Right Now ──
+      // Combine recent high-confidence signals with historical win rates and current market prices
+      const { rows: opportunities } = await kenQ(`
+        WITH recent_signals AS (
+          SELECT DISTINCT ON (market_id)
+            market_id, direction, expected_edge, confidence, reasoning, risk_approved, created_at
+          FROM ken_strategy_signals
+          WHERE created_at > NOW() - INTERVAL '6 hours'
+          ORDER BY market_id, expected_edge DESC
+        ),
+        historical AS (
+          SELECT market_id,
+            COUNT(*) as total_trades,
+            COUNT(*) FILTER (WHERE pnl_cents > 0) as wins,
+            SUM(pnl_cents) as pnl_cents,
+            MODE() WITHIN GROUP (ORDER BY direction) as primary_direction,
+            ROUND(AVG(entry_price_cents)) as avg_entry
+          FROM ken_portfolio_trades
+          WHERE resolved_at IS NOT NULL
+          GROUP BY market_id
+        ),
+        snapshots AS (
+          SELECT DISTINCT ON (ticker)
+            ticker, title, yes_bid, no_bid, volume_24h, last_price
+          FROM ken_market_snapshots
+          WHERE captured_at > NOW() - INTERVAL '2 hours'
+          ORDER BY ticker, captured_at DESC
+        )
+        SELECT
+          s.market_id, COALESCE(snap.title, s.market_id) as title,
+          s.direction, s.expected_edge, s.confidence, s.risk_approved,
+          COALESCE(h.total_trades, 0) as hist_trades,
+          COALESCE(h.wins, 0) as hist_wins,
+          COALESCE(h.pnl_cents, 0) as hist_pnl,
+          COALESCE(h.avg_entry, 0) as avg_entry,
+          snap.yes_bid, snap.no_bid, snap.volume_24h,
+          LEFT(s.reasoning, 200) as reasoning
+        FROM recent_signals s
+        LEFT JOIN historical h ON h.market_id = s.market_id
+        LEFT JOIN snapshots snap ON snap.ticker = s.market_id
+        WHERE s.confidence >= 0.5
+        ORDER BY
+          (s.expected_edge * s.confidence * (1 + COALESCE(h.wins::float / NULLIF(h.total_trades, 0), 0))) DESC
+        LIMIT 3
+      `);
+
+      function edgeDescription(m) {
+        const wr = Number(m.wins) / (Number(m.wins) + Number(m.losses) || 1);
+        const dir = m.primary_direction === 'BUY_NO' ? 'NO' : 'YES';
+        const avg = Number(m.avg_entry);
+        if (wr >= 0.99 && avg >= 90) return `${dir} at ${avg}¢ = near-certain`;
+        if (wr >= 0.95 && avg >= 85) return `${dir} at ${avg}¢, market overprices`;
+        if (wr >= 0.80) return `${dir} side, ${(wr*100).toFixed(0)}% win rate`;
+        if (Number(m.total_trades) >= 150) return `High volume, clear direction`;
+        if (avg >= 80) return `${dir} at ${avg}¢, steady edge`;
+        return `${dir} side, avg ${avg}¢ entry`;
+      }
+
+      json(res, {
+        env: KALSHI_ENV,
+        safe_mode: true,
+        balance: totalBalance,
+        seed_money: seedMoney,
+        net_pnl_from_seed: totalBalance - seedMoney,
+        roi_pct: parseFloat(roi.toFixed(2)),
+        total_invested: totalInvested,
+        total_returned: totalReturned,
+        daily_budget: Number(budgetRow?.total_daily_budget || 0),
+        num_portfolios: Number(budgetRow?.num_portfolios || 0),
+        capital_at_risk: Number(riskRow?.capital_at_risk || 0),
+        open_positions_cost: Number(riskRow?.open_positions || 0),
+        daily_economics: spendRows.map(r => ({
+          day: r.day,
+          trades: Number(r.trades),
+          wins: Number(r.wins),
+          losses: Number(r.losses),
+          net_pnl: Number(r.net_pnl),
+          gross_wins: Number(r.gross_wins),
+          gross_losses: Number(r.gross_losses),
+          capital_deployed: Number(r.capital_deployed),
+        })),
+        position_count: Number(posRow?.open_count || 0),
+        open_orders: Number(posRow?.unique_markets || 0),
+        daily_pnl: Number(posRow?.daily_pnl || 0),
+        total_pnl: Number(posRow?.total_pnl || 0),
+        markets_tracked: cachedActiveMarkets.length,
+        today_trades: Number(todayRow?.trades || 0),
+        today_wins: Number(todayRow?.wins || 0),
+        today_losses: Number(todayRow?.losses || 0),
+        recent_fills: fills.map(f => ({
+          ticker: f.ticker,
+          title: f.market_title,
+          side: f.direction === 'BUY_YES' ? 'yes' : 'no',
+          count: f.contracts,
+          yes_price: f.entry_price_cents,
+          pnl: f.pnl_cents,
+          status: f.status,
+        })),
+        pnl_history: pnlHistory.map(p => Number(p.daily_pnl) / 100),
+        top_markets: topMarkets.map(m => {
+          const t = m.market_id || '';
+          const et = t.replace(/-[^-]+$/, '') || t;
+          const st = et.replace(/-[^-]+$/, '') || et;
+          return {
+          ticker: m.market_id,
+          title: m.market_title,
+          trades: Number(m.total_trades),
+          wins: Number(m.wins),
+          losses: Number(m.losses),
+          pnl: Number(m.pnl_cents),
+          edge: edgeDescription(m),
+          url: `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`,
+        }; }),
+        worst_markets: worstMarkets.map(m => {
+          const t = m.market_id || '';
+          const et = t.replace(/-[^-]+$/, '') || t;
+          const st = et.replace(/-[^-]+$/, '') || et;
+          return {
+          ticker: m.market_id,
+          title: m.market_title,
+          trades: Number(m.total_trades),
+          wins: Number(m.wins),
+          losses: Number(m.losses),
+          pnl: Number(m.pnl_cents),
+          edge: edgeDescription(m),
+          url: `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`,
+        }; }),
+        strategies: strategies.map(s => ({
+          name: s.name,
+          strategy: s.strategy,
+          wins: Number(s.trades_won),
+          losses: Number(s.trades_lost),
+          pnl: Number(s.net_pnl_cents),
+          balance: Number(s.balance_cents),
+          win_rate: Number(s.win_rate || 0),
+        })),
+        top_opportunities: opportunities.map(o => {
+          const wr = Number(o.hist_trades) > 0 ? Number(o.hist_wins) / Number(o.hist_trades) : null;
+          const dir = o.direction === 'BUY_NO' ? 'NO' : 'YES';
+          const edge = (parseFloat(o.expected_edge) * 100).toFixed(1);
+          const conf = (parseFloat(o.confidence) * 100).toFixed(0);
+          const yesBid = Number(o.yes_bid || 0);
+          const noBid = Number(o.no_bid || 0);
+          const price = o.direction === 'BUY_NO' ? noBid : yesBid;
+          // Build human-readable "why" from signal reasoning
+          const rawReasoning = o.reasoning || '';
+          const reasoningClean = rawReasoning.replace(/^BUY_(YES|NO) on "[^"]*"\s*—\s*/, '');
+          return {
+            ticker: o.market_id,
+            title: o.title,
+            direction: dir,
+            edge_pct: edge,
+            confidence: conf,
+            price_cents: price,
+            hist_trades: Number(o.hist_trades),
+            hist_wins: Number(o.hist_wins),
+            hist_wr: wr !== null ? (wr * 100).toFixed(1) : null,
+            hist_pnl: Number(o.hist_pnl),
+            volume_24h: Number(o.volume_24h || 0),
+            risk_approved: o.risk_approved,
+            why: reasoningClean,
+            url: (() => { const t = o.market_id || ''; const et = t.replace(/-[^-]+$/, '') || t; const st = et.replace(/-[^-]+$/, '') || et; return `https://kalshi.com/markets/${st.toLowerCase()}/${et.toLowerCase()}`; })(),
+          };
+        }),
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // SCAN CONFIG (User-adjustable intervals + risk params)
+  // ════════════════════════════════════════
+
+  'GET /api/scan-config': async (req, res) => {
+    json(res, {
+      scan_interval_ms: SCAN_INTERVAL_MS,
+      signal_pipeline_ms: SIGNAL_PIPELINE_MS,
+      reddit_interval_ms: REDDIT_INTERVAL_MS,
+      resolve_interval_ms: RESOLVE_INTERVAL_MS,
+      min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
+      min_liquidity: RISK_LIMITS.minLiquidity,
+      cached_markets: cachedActiveMarkets.length,
+    });
+  },
+
+  'POST /api/scan-config': async (req, res) => {
+    try {
+      const body = await readBody(req);
+      let changed = false;
+      if (body.scan_interval_ms !== undefined) { SCAN_INTERVAL_MS = Math.max(60000, Math.min(3600000, Number(body.scan_interval_ms))); changed = true; }
+      if (body.signal_pipeline_ms !== undefined) { SIGNAL_PIPELINE_MS = Math.max(30000, Math.min(1800000, Number(body.signal_pipeline_ms))); changed = true; }
+      if (body.reddit_interval_ms !== undefined) { REDDIT_INTERVAL_MS = Math.max(60000, Math.min(3600000, Number(body.reddit_interval_ms))); changed = true; }
+      if (body.resolve_interval_ms !== undefined) { RESOLVE_INTERVAL_MS = Math.max(30000, Math.min(1800000, Number(body.resolve_interval_ms))); changed = true; }
+      if (body.min_edge_threshold !== undefined) { RISK_LIMITS.minEdgeThreshold = Math.max(0.005, Math.min(0.50, Number(body.min_edge_threshold))); changed = true; }
+      if (body.min_liquidity !== undefined) { RISK_LIMITS.minLiquidity = Math.max(0, Math.min(10000, Number(body.min_liquidity))); changed = true; }
+      if (changed) {
+        await saveScanConfig();
+        restartIntervals();
+      }
+      json(res, {
+        success: true,
+        scan_interval_ms: SCAN_INTERVAL_MS,
+        signal_pipeline_ms: SIGNAL_PIPELINE_MS,
+        reddit_interval_ms: REDDIT_INTERVAL_MS,
+        resolve_interval_ms: RESOLVE_INTERVAL_MS,
+        min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
+        min_liquidity: RISK_LIMITS.minLiquidity,
+        cached_markets: cachedActiveMarkets.length,
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  // ════════════════════════════════════════
+  // MEMORY / SYSTEM STATUS
+  // ════════════════════════════════════════
+
+  'GET /api/system': async (req, res) => {
+    const mem = process.memoryUsage();
+    json(res, {
+      memory: {
+        rss_mb: Math.round(mem.rss / 1024 / 1024),
+        heapUsed_mb: Math.round(mem.heapUsed / 1024 / 1024),
+        heapTotal_mb: Math.round(mem.heapTotal / 1024 / 1024),
+        external_mb: Math.round(mem.external / 1024 / 1024),
+      },
+      uptime_hours: Math.round(process.uptime() / 3600 * 10) / 10,
+      env: KALSHI_ENV,
+      cachedMarkets: cachedActiveMarkets.length,
+      pid: process.pid,
+      cpuCores: os.cpus().length,
+      enhancements: {
+        monteCarloIterations: 15000,
+        geminiModel: GEMINI_MODEL,
+        geminiCallsToday: geminiCallCount,
+        geminiDailyLimit: GEMINI_DAILY_LIMIT,
+        scanInterval: '8m',
+        signalPipeline: '2m',
+        redditInterval: '15m',
+        momentumCacheSize: Object.keys(cachedMomentumMap).length,
+      },
+    });
+  },
+
+  // ── Portfolios API ──
+  'GET /api/portfolios': async (req, res) => {
+    try {
+      const { rows: portfolios } = await kenQ(`SELECT * FROM ken_portfolios ORDER BY id`);
+      // Get today's trade counts per portfolio
+      for (const pf of portfolios) {
+        const { rows: today } = await kenQ(`
+          SELECT COUNT(*) as trades_today, COALESCE(SUM(cost_cents),0) as spent_today
+          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at::date = CURRENT_DATE
+        `, [pf.id]);
+        pf.trades_today = parseInt(today[0]?.trades_today) || 0;
+        pf.spent_today = parseInt(today[0]?.spent_today) || 0;
+        pf.budget_remaining = Math.max(0, pf.balance_cents); // Remaining = whatever balance is left
+
+        // Win rate
+        const totalResolved = pf.trades_won + pf.trades_lost + pf.trades_expired;
+        pf.win_rate = totalResolved > 0 ? Math.round(pf.trades_won / totalResolved * 100) : 0;
+        pf.total_resolved = totalResolved;
+        pf.is_bankrupt = pf.balance_cents <= 0;
+      }
+      // Sort: active portfolios by ROI (best first), bankrupt portfolios at bottom
+      portfolios.sort((a, b) => {
+        if (a.is_bankrupt !== b.is_bankrupt) return a.is_bankrupt ? 1 : -1;
+        const aROI = a.total_invested_cents > 0 ? (a.total_returned_cents - a.total_invested_cents) / a.total_invested_cents : 0;
+        const bROI = b.total_invested_cents > 0 ? (b.total_returned_cents - b.total_invested_cents) / b.total_invested_cents : 0;
+        return bROI - aROI;
+      });
+      json(res, portfolios);
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  'GET /api/portfolio-trades': async (req, res) => {
+    try {
+      const url = new URL(req.url, 'http://x');
+      const pfId = url.searchParams.get('portfolio_id');
+      const status = url.searchParams.get('status');
+      let q = `SELECT * FROM ken_portfolio_trades`;
+      const params = [];
+      const conds = [];
+      if (pfId) { conds.push(`portfolio_id = $${params.length+1}`); params.push(pfId); }
+      if (status) { conds.push(`status = $${params.length+1}`); params.push(status); }
+      if (conds.length) q += ' WHERE ' + conds.join(' AND ');
+      q += ' ORDER BY created_at DESC LIMIT 200';
+      const { rows } = await kenQ(q, params);
+      json(res, rows);
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  // ── PortFAUXlio Charts API — time-series NAV data ──
+  'GET /api/portfauxlio/charts': async (req, res) => {
+    try {
+      const url = new URL(req.url, 'http://x');
+      const period = url.searchParams.get('period') || '7d';
+      const intervals = { '1d': '1 day', '3d': '3 days', '7d': '7 days', '14d': '14 days', '30d': '30 days' };
+      const interval = intervals[period] || '7 days';
+
+      // Snapshots (intraday)
+      const { rows: snapRows } = await kenQ(`
+        SELECT s.portfolio_id, s.nav_cents, s.open_positions, s.snapshot_at, p.name, p.strategy
+        FROM ken_portfolio_snapshots s JOIN ken_portfolios p ON p.id = s.portfolio_id
+        WHERE s.snapshot_at > NOW() - INTERVAL '${interval}' ORDER BY s.portfolio_id, s.snapshot_at ASC
+      `);
+
+      // Daily P&L (longer-term)
+      const { rows: dailyRows } = await kenQ(`
+        SELECT dp.portfolio_id, dp.date, dp.daily_pnl_cents, dp.cumulative_pnl_cents,
+               dp.wins, dp.losses, dp.closed_trades, p.name, p.strategy
+        FROM ken_daily_pnl dp JOIN ken_portfolios p ON p.id = dp.portfolio_id
+        WHERE dp.date > CURRENT_DATE - INTERVAL '${interval}' ORDER BY dp.portfolio_id, dp.date ASC
+      `);
+
+      const byPortfolio = {};
+      for (const r of snapRows) {
+        if (!byPortfolio[r.portfolio_id]) byPortfolio[r.portfolio_id] = { id: r.portfolio_id, name: r.name, strategy: r.strategy, snapshots: [], daily: [] };
+        byPortfolio[r.portfolio_id].snapshots.push({ nav: r.nav_cents, openPositions: r.open_positions, time: r.snapshot_at });
+      }
+      for (const r of dailyRows) {
+        if (!byPortfolio[r.portfolio_id]) byPortfolio[r.portfolio_id] = { id: r.portfolio_id, name: r.name, strategy: r.strategy, snapshots: [], daily: [] };
+        byPortfolio[r.portfolio_id].daily.push({ date: r.date, pnl: r.daily_pnl_cents, cumulative: r.cumulative_pnl_cents, wins: r.wins, losses: r.losses, trades: r.closed_trades });
+      }
+
+      json(res, { portfolios: Object.values(byPortfolio), period });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  // ── PortFAUXlio SWOT — trades with strengths/weaknesses/opportunities/threats ──
+  'GET /api/portfauxlio/swot': async (req, res) => {
+    try {
+      const { rows: trades } = await kenQ(`
+        SELECT t.id, t.market_id, t.market_title, t.direction, t.entry_price_cents,
+               t.contracts, t.cost_cents, t.status, t.pnl_cents, t.reasoning,
+               t.confidence, t.ai_enhanced, t.mc_consistent, t.created_at, t.resolved_at,
+               p.name AS portfolio_name, p.strategy, p.id AS portfolio_id
+        FROM ken_portfolio_trades t JOIN ken_portfolios p ON p.id = t.portfolio_id
+        WHERE t.created_at > NOW() - INTERVAL '7 days'
+        ORDER BY t.created_at DESC LIMIT 200
+      `);
+
+      // Group by market
+      const byMarket = {};
+      for (const t of trades) {
+        if (!byMarket[t.market_id]) byMarket[t.market_id] = { market_id: t.market_id, title: t.market_title || t.market_id, trades: [], totalCost: 0, totalPnl: 0, models: new Set() };
+        const m = byMarket[t.market_id];
+        m.trades.push(t);
+        m.totalCost += parseInt(t.cost_cents) || 0;
+        m.totalPnl += parseInt(t.pnl_cents) || 0;
+        m.models.add(t.portfolio_name);
+      }
+
+      // Generate SWOT for each market group
+      const swotMarkets = Object.values(byMarket).map(m => {
+        const t0 = m.trades[0];
+        const conf = parseFloat(t0.confidence) || 0;
+        const aiCount = m.trades.filter(t => t.ai_enhanced).length;
+        const mcCount = m.trades.filter(t => t.mc_consistent).length;
+        const totalT = m.trades.length;
+        const openCount = m.trades.filter(t => t.status === 'open').length;
+        const reasoning = t0.reasoning || '';
+
+        // Parse edge from reasoning
+        const edgeMatch = reasoning.match(/edge\s*([\d.]+)%/i);
+        const edge = edgeMatch ? parseFloat(edgeMatch[1]) : 0;
+        const mcMatch = reasoning.match(/MC:\s*\u03bc=([\d.]+)%\s*\u00b1\s*([\d.]+)%/);
+        const mcMean = mcMatch ? parseFloat(mcMatch[1]) : 0;
+        const mcStd = mcMatch ? parseFloat(mcMatch[2]) : 0;
+        const artMatch = reasoning.match(/(\d+)\s*articles?/i);
+        const articles = artMatch ? parseInt(artMatch[1]) : 0;
+        const agreeMatch = reasoning.match(/Agreement:\s*([\d.]+)%/i);
+        const agreement = agreeMatch ? parseFloat(agreeMatch[1]) : 0;
+
+        // SWOT generation
+        const strengths = [];
+        const weaknesses = [];
+        const opportunities = [];
+        const threats = [];
+
+        // Strengths
+        if (conf >= 0.6) strengths.push('High confidence (' + Math.round(conf * 100) + '%)');
+        if (mcCount > totalT * 0.5) strengths.push('Monte Carlo validated (' + mcCount + '/' + totalT + ' models)');
+        if (aiCount > 0) strengths.push('AI enhanced signals');
+        if (totalT >= 3) strengths.push('Multi-model consensus (' + totalT + ' models agree)');
+        if (agreement >= 80) strengths.push('Strong news agreement (' + agreement + '%)');
+        if (articles >= 3) strengths.push('Strong news coverage (' + articles + ' articles)');
+        if (edge >= 15) strengths.push('Large statistical edge (' + edge.toFixed(1) + '%)');
+        if (strengths.length === 0) strengths.push('Active position in play');
+
+        // Weaknesses
+        if (conf < 0.4) weaknesses.push('Low confidence (' + Math.round(conf * 100) + '%)');
+        if (mcCount === 0) weaknesses.push('No Monte Carlo validation');
+        if (aiCount === 0) weaknesses.push('No AI enhancement');
+        if (articles <= 1) weaknesses.push('Limited news coverage (' + articles + ' article' + (articles === 1 ? '' : 's') + ')');
+        if (edge < 5 && edge > 0) weaknesses.push('Narrow edge (' + edge.toFixed(1) + '%)');
+        if (mcStd > 10) weaknesses.push('High MC uncertainty (\u00b1' + mcStd.toFixed(1) + '%)');
+        if (weaknesses.length === 0) weaknesses.push('No significant weaknesses identified');
+
+        // Opportunities
+        if (edge >= 8) opportunities.push('Exploitable edge of ' + edge.toFixed(1) + '%');
+        if (m.models.size >= 5) opportunities.push(m.models.size + ' models see opportunity');
+        if (mcMean > edge) opportunities.push('MC mean (' + mcMean.toFixed(1) + '%) exceeds raw edge');
+        if (openCount > 0) opportunities.push(openCount + ' open position' + (openCount > 1 ? 's' : '') + ' tracking');
+        if (opportunities.length === 0) opportunities.push('Market exposure maintained');
+
+        // Threats
+        if (mcStd > 8) threats.push('High volatility (MC \u00b1' + mcStd.toFixed(1) + '%)');
+        if (agreement < 60 && agreement > 0) threats.push('Mixed news sentiment (' + agreement + '% agreement)');
+        if (m.totalPnl < 0) threats.push('Currently underwater ($' + (Math.abs(m.totalPnl) / 100).toFixed(2) + ' loss)');
+        const direction = t0.direction;
+        if (direction === 'BUY_YES' && edge < 10) threats.push('Thin YES margin — small price move could flip');
+        if (direction === 'BUY_NO' && conf < 0.5) threats.push('NO position with uncertain conviction');
+        if (threats.length === 0) threats.push('Standard market risk');
+
+        return {
+          market_id: m.market_id,
+          title: m.title,
+          direction: t0.direction,
+          edge,
+          confidence: conf,
+          articles,
+          agreement,
+          mc_mean: mcMean,
+          mc_std: mcStd,
+          model_count: totalT,
+          models: [...m.models],
+          open_count: openCount,
+          total_cost: m.totalCost,
+          total_pnl: m.totalPnl,
+          entry_price: parseInt(t0.entry_price_cents) || 0,
+          first_trade: m.trades[m.trades.length - 1]?.created_at,
+          reasoning: reasoning.substring(0, 200),
+          swot: { strengths, weaknesses, opportunities, threats },
+          trades: m.trades.map(t => ({
+            id: t.id, portfolio: t.portfolio_name, strategy: t.strategy,
+            direction: t.direction, contracts: t.contracts,
+            cost: parseInt(t.cost_cents) || 0, pnl: parseInt(t.pnl_cents) || 0,
+            status: t.status, created_at: t.created_at
+          }))
+        };
+      });
+
+      // Sort by model count desc (most consensus first)
+      swotMarkets.sort((a, b) => b.model_count - a.model_count);
+
+      json(res, { markets: swotMarkets, total_trades: trades.length });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  // ── Weekly Tournament System — survive or die ──
+  'GET /api/tournament/current': async (req, res) => {
+    try {
+      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
+      if (!tourney) return json(res, { active: false, message: 'No active tournament' });
+      const { rows: entries } = await kenQ(`
+        SELECT e.*, p.name, p.strategy, p.balance_cents, p.total_invested_cents, p.total_returned_cents,
+               p.trades_won, p.trades_lost, p.daily_budget_cents
+        FROM ken_tournament_entries e JOIN ken_portfolios p ON p.id = e.portfolio_id
+        WHERE e.tournament_id = $1 ORDER BY e.pnl_cents DESC NULLS LAST
+      `, [tourney.id]);
+      // Compute live P&L for each entry
+      for (const e of entries) {
+        const { rows: [stats] } = await kenQ(`
+          SELECT COUNT(*) as trades, COALESCE(SUM(cost_cents),0) as total_cost,
+                 COALESCE(SUM(CASE WHEN status IN ('won','lost','closed') THEN pnl_cents ELSE 0 END),0) as realized_pnl
+          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at >= $2
+        `, [e.portfolio_id, tourney.week_start]);
+        e.live_trades = parseInt(stats.trades) || 0;
+        e.live_invested = parseInt(stats.total_cost) || 0;
+        e.live_pnl = parseInt(stats.realized_pnl) || 0;
+      }
+      json(res, { active: true, tournament: tourney, entries });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  'POST /api/tournament/start': async (req, res) => {
+    try {
+      const body = await readBody(req);
+      const portfolioIds = body.portfolio_ids || [];
+      const budgetCents = body.budget_cents || 2000; // $20 default
+      const notes = body.notes || '';
+      const weeks = body.weeks || 1;
+      if (portfolioIds.length < 1 || portfolioIds.length > 10) return json(res, { error: 'Pick 1-10 portfolios' }, 400);
+      // Check no active tournament
+      const { rows: active } = await kenQ(`SELECT id FROM ken_weekly_tournaments WHERE status = 'active'`);
+      if (active.length > 0) return json(res, { error: 'Tournament already active. Evaluate or cancel it first.', active_id: active[0].id }, 400);
+      const weekStart = new Date(); weekStart.setHours(0,0,0,0);
+      const weekEnd = new Date(weekStart); weekEnd.setDate(weekEnd.getDate() + 7 * weeks);
+      const { rows: [tourney] } = await kenQ(`
+        INSERT INTO ken_weekly_tournaments (week_start, week_end, budget_per_portfolio_cents, notes)
+        VALUES ($1, $2, $3, $4) RETURNING *
+      `, [weekStart.toISOString().split('T')[0], weekEnd.toISOString().split('T')[0], budgetCents, notes]);
+      const entries = [];
+      for (const pid of portfolioIds) {
+        const { rows: [pf] } = await kenQ(`SELECT * FROM ken_portfolios WHERE id = $1`, [pid]);
+        if (!pf) continue;
+        // Set portfolio budget for the tournament
+        await kenQ(`UPDATE ken_portfolios SET daily_budget_cents = $1, balance_cents = $2 WHERE id = $3`, [Math.round(budgetCents / 7), budgetCents, pid]);
+        const { rows: [entry] } = await kenQ(`
+          INSERT INTO ken_tournament_entries (tournament_id, portfolio_id, start_balance_cents, status)
+          VALUES ($1, $2, $3, 'alive') RETURNING *
+        `, [tourney.id, pid, budgetCents]);
+        entries.push({ ...entry, name: pf.name, strategy: pf.strategy });
+      }
+      json(res, { tournament: tourney, entries, message: `Tournament started with ${entries.length} portfolios at $${(budgetCents/100).toFixed(2)} each` });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  'POST /api/tournament/evaluate': async (req, res) => {
+    try {
+      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
+      if (!tourney) return json(res, { error: 'No active tournament' }, 400);
+      const { rows: entries } = await kenQ(`
+        SELECT e.*, p.name, p.strategy FROM ken_tournament_entries e
+        JOIN ken_portfolios p ON p.id = e.portfolio_id WHERE e.tournament_id = $1
+      `, [tourney.id]);
+      const results = [];
+      for (const e of entries) {
+        const { rows: [stats] } = await kenQ(`
+          SELECT COUNT(*) as trades,
+                 COALESCE(SUM(CASE WHEN status='won' THEN payout_cents - cost_cents WHEN status='lost' THEN -cost_cents ELSE 0 END),0) as pnl
+          FROM ken_portfolio_trades WHERE portfolio_id = $1 AND created_at >= $2
+        `, [e.portfolio_id, tourney.week_start]);
+        const pnl = parseInt(stats.pnl) || 0;
+        const trades = parseInt(stats.trades) || 0;
+        const survived = pnl >= 0;
+        await kenQ(`UPDATE ken_tournament_entries SET end_balance_cents = $1, pnl_cents = $2, trades_count = $3, status = $4, elimination_reason = $5 WHERE id = $6`,
+          [e.start_balance_cents + pnl, pnl, trades, survived ? 'survived' : 'eliminated', survived ? null : `Lost $${(Math.abs(pnl)/100).toFixed(2)}`, e.id]);
+        if (!survived) await kenQ(`UPDATE ken_portfolios SET daily_budget_cents = 0 WHERE id = $1`, [e.portfolio_id]);
+        results.push({ name: e.name, strategy: e.strategy, pnl_cents: pnl, trades, status: survived ? 'SURVIVED' : 'ELIMINATED' });
+      }
+      await kenQ(`UPDATE ken_weekly_tournaments SET status = 'completed' WHERE id = $1`, [tourney.id]);
+      const survivors = results.filter(r => r.status === 'SURVIVED');
+      const eliminated = results.filter(r => r.status === 'ELIMINATED');
+      json(res, { tournament_id: tourney.id, results, survivors: survivors.length, eliminated: eliminated.length, message: `${survivors.length} survived, ${eliminated.length} eliminated` });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  'POST /api/tournament/cancel': async (req, res) => {
+    try {
+      const { rows: [tourney] } = await kenQ(`SELECT * FROM ken_weekly_tournaments WHERE status = 'active' ORDER BY id DESC LIMIT 1`);
+      if (!tourney) return json(res, { error: 'No active tournament' }, 400);
+      await kenQ(`UPDATE ken_weekly_tournaments SET status = 'cancelled' WHERE id = $1`, [tourney.id]);
+      await kenQ(`UPDATE ken_tournament_entries SET status = 'cancelled' WHERE tournament_id = $1`, [tourney.id]);
+      json(res, { message: 'Tournament cancelled', id: tourney.id });
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  'GET /api/tournament/history': async (req, res) => {
+    try {
+      const { rows: tournaments } = await kenQ(`SELECT * FROM ken_weekly_tournaments ORDER BY id DESC LIMIT 20`);
+      for (const t of tournaments) {
+        const { rows: entries } = await kenQ(`
+          SELECT e.*, p.name, p.strategy FROM ken_tournament_entries e
+          JOIN ken_portfolios p ON p.id = e.portfolio_id WHERE e.tournament_id = $1 ORDER BY e.pnl_cents DESC NULLS LAST
+        `, [t.id]);
+        t.entries = entries;
+      }
+      json(res, tournaments);
+    } catch (e) { json(res, { error: e.message }, 500); }
+  },
+
+  // ── PortFAUXlio Dashboard API — full simulated trading view ──
+  'GET /api/portfauxlio': async (req, res) => {
+    try {
+      // 1. All portfolios with computed stats
+      const { rows: portfolios } = await kenQ(`
+        SELECT p.*,
+          (p.total_returned_cents - p.total_invested_cents) AS pnl_cents,
+          CASE WHEN p.total_invested_cents > 0
+            THEN ROUND((p.total_returned_cents - p.total_invested_cents)::numeric / p.total_invested_cents * 100, 1)
+            ELSE 0 END AS roi_pct,
+          (p.trades_won + p.trades_lost + p.trades_expired) AS resolved_trades,
+          CASE WHEN (p.trades_won + p.trades_lost) > 0
+            THEN ROUND(p.trades_won::numeric / (p.trades_won + p.trades_lost) * 100, 0)
+            ELSE 0 END AS win_rate
+        FROM ken_portfolios p ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC
+      `);
+
+      // 2. All active (open) trades across all portfolios
+      const { rows: activeTrades } = await kenQ(`
+        SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction,
+          t.contracts, t.entry_price_cents, t.cost_cents, t.confidence, t.created_at,
+          p.name AS portfolio_name, p.strategy
+        FROM ken_portfolio_trades t
+        JOIN ken_portfolios p ON p.id = t.portfolio_id
+        WHERE t.status = 'open'
+        ORDER BY t.created_at DESC
+      `);
+
+      // 3. Trade history (all trades, most recent first)
+      const { rows: tradeHistory } = await kenQ(`
+        SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction,
+          t.contracts, t.entry_price_cents, t.cost_cents, t.pnl_cents, t.status,
+          t.confidence, t.created_at, t.resolved_at,
+          p.name AS portfolio_name, p.strategy
+        FROM ken_portfolio_trades t
+        JOIN ken_portfolios p ON p.id = t.portfolio_id
+        ORDER BY t.created_at DESC LIMIT 200
+      `);
+
+      // 4. Per-model trade distribution: which markets each strategy traded
+      const { rows: modelMarkets } = await kenQ(`
+        SELECT p.strategy, t.market_id,
+          COUNT(*) AS trade_count,
+          SUM(t.cost_cents) AS total_cost,
+          MAX(t.created_at) AS last_trade
+        FROM ken_portfolio_trades t
+        JOIN ken_portfolios p ON p.id = t.portfolio_id
+        GROUP BY p.strategy, t.market_id
+        ORDER BY p.strategy, trade_count DESC
+      `);
+
+      // 5. Overlap analysis: markets traded by 3+ strategies
+      const { rows: overlaps } = await kenQ(`
+        SELECT t.market_id,
+          COUNT(DISTINCT p.strategy) AS num_strategies,
+          ARRAY_AGG(DISTINCT p.name ORDER BY p.name) AS portfolios,
+          SUM(t.cost_cents) AS total_exposure
+        FROM ken_portfolio_trades t
+        JOIN ken_portfolios p ON p.id = t.portfolio_id
+        GROUP BY t.market_id
+        HAVING COUNT(DISTINCT p.strategy) >= 2
+        ORDER BY num_strategies DESC
+      `);
+
+      // 6. Summary stats
+      const totalInvested = portfolios.reduce((s, p) => s + (parseInt(p.total_invested_cents) || 0), 0);
+      const totalReturned = portfolios.reduce((s, p) => s + (parseInt(p.total_returned_cents) || 0), 0);
+      const totalTrades = tradeHistory.length;
+      const openTrades = activeTrades.length;
+      const uniqueMarkets = [...new Set(tradeHistory.map(t => t.market_id))].length;
+
+      json(res, {
+        portfolios,
+        activeTrades,
+        tradeHistory,
+        modelMarkets,
+        overlaps,
+        summary: {
+          totalInvested, totalReturned,
+          totalPnl: totalReturned - totalInvested,
+          totalTrades, openTrades, uniqueMarkets,
+          activeModels: portfolios.filter(p => parseInt(p.total_invested_cents) > 0).length,
+        }
+      });
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  'GET /api/predictions-history': async (req, res) => {
+    try {
+      const { rows } = await kenQ(`
+        SELECT id, ticker, title, signal_type, side, confidence, entry_price, exit_price,
+               edge_bps, outcome, pnl_cents, reasoning, created_at, resolved_at
+        FROM ken_predictions
+        WHERE outcome != 'pending'
+        ORDER BY resolved_at DESC NULLS LAST
+        LIMIT 100
+      `);
+      // Also get stats
+      const { rows: stats } = await kenQ(`
+        SELECT outcome, COUNT(*) as cnt, ROUND(AVG(pnl_cents),1) as avg_pnl,
+               ROUND(AVG(confidence),1) as avg_conf
+        FROM ken_predictions WHERE outcome != 'pending'
+        GROUP BY outcome
+      `);
+      json(res, { predictions: rows, stats });
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  'GET /api/daily-pnl': async (req, res) => {
+    try {
+      const { rows } = await kenQ(`
+        SELECT dp.*, p.name as portfolio_name
+        FROM ken_daily_pnl dp
+        JOIN ken_portfolios p ON p.id = dp.portfolio_id
+        ORDER BY dp.date DESC, dp.portfolio_id
+        LIMIT 200
+      `);
+      json(res, rows);
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  // ── Heating & Weather Intelligence API ──
+  'GET /api/heating-intel': async (req, res) => {
+    try {
+      const { rows: posts } = await kenQ(`
+        SELECT source, subreddit as area, keyword, post_title as title, post_url as url,
+               score, sentiment_score, raw_text, captured_at
+        FROM ken_sentiment
+        WHERE source IN ('heating_intel', 'noaa_alert', 'noaa_forecast')
+        ORDER BY captured_at DESC
+        LIMIT 50
+      `);
+      const { rows: stats } = await kenQ(`
+        SELECT source, COUNT(*) as cnt,
+               ROUND(AVG(sentiment_score)::numeric, 2) as avg_sentiment,
+               ROUND(AVG(score)::numeric, 0) as avg_score
+        FROM ken_sentiment
+        WHERE source IN ('heating_intel', 'noaa_alert', 'noaa_forecast')
+          AND captured_at > NOW() - INTERVAL '7 days'
+        GROUP BY source
+      `);
+      json(res, { posts, stats, keywords: HEATING_KEYWORDS });
+    } catch(e) { json(res, { error: e.message }, 500); }
+  },
+
+  // ── Ken Inc Command Center API ──
+  'GET /api/inc': async (req, res) => {
+    const mem = process.memoryUsage();
+    // Get recent signals
+    let recentSignals = [];
+    try {
+      const { rows } = await kenQ(`SELECT market_id, direction, expected_edge, confidence, reasoning, risk_approved, created_at
+        FROM ken_strategy_signals ORDER BY created_at DESC LIMIT 20`);
+      recentSignals = rows;
+    } catch(e) {}
+
+    // Get pipeline stats
+    let pipelineStats = {};
+    try {
+      const { rows } = await kenQ(`SELECT COUNT(*) as total_signals,
+        COUNT(*) FILTER (WHERE risk_approved = true) as approved,
+        COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '1 hour') as last_hour,
+        AVG(ABS(expected_edge::numeric)) as avg_edge,
+        MAX(confidence::numeric) as max_confidence
+        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'`);
+      pipelineStats = rows[0] || {};
+    } catch(e) {}
+
+    // Sentiment stats
+    let sentimentStats = {};
+    try {
+      const { rows } = await kenQ(`SELECT COUNT(*) as total_articles,
+        COUNT(DISTINCT source) as unique_sources,
+        AVG(sentiment_score) as avg_sentiment
+        FROM ken_sentiment WHERE captured_at > NOW() - INTERVAL '24 hours'`);
+      sentimentStats = rows[0] || {};
+    } catch(e) {}
+
+    // Market snapshot stats
+    let marketStats = {};
+    try {
+      const { rows } = await kenQ(`SELECT COUNT(DISTINCT ticker) as tracked_markets
+        FROM ken_market_snapshots WHERE captured_at > NOW() - INTERVAL '24 hours'`);
+      marketStats = rows[0] || {};
+    } catch(e) {}
+
+    // Build sub-agent status
+    const subAgents = [
+      {
+        id: 'scout', name: 'Scout', role: 'Intelligence Gatherer', emoji: '🔍',
+        color: '#3B82F6', status: 'active',
+        desc: 'Sweeps 77 subreddits, 71 RSS feeds, HN, YouTube, Bluesky, Mastodon, Discord',
+        stats: { articles24h: parseInt(sentimentStats.total_articles) || 0, sources: parseInt(sentimentStats.unique_sources) || 0 },
+        skills: ['Reddit Scraping', 'RSS Feeds', 'Social Media', 'GDELT TV', 'YouTube', 'Lemmy', 'Lobsters'],
+      },
+      {
+        id: 'oracle', name: 'Oracle', role: 'Monte Carlo Engine', emoji: '🎲',
+        color: '#8B5CF6', status: 'active',
+        desc: 'Runs 15,000 iteration Monte Carlo simulations on every signal for probability validation',
+        stats: { iterations: 15000, cores: os.cpus().length, momentumTracked: Object.keys(cachedMomentumMap).length },
+        skills: ['Monte Carlo Simulation', 'Bayesian Inference', 'Ensemble Probability', 'Confidence Intervals'],
+      },
+      {
+        id: 'gemini', name: 'Nova', role: 'AI Analyst', emoji: '🧠',
+        color: '#EC4899', status: geminiCallCount < GEMINI_DAILY_LIMIT ? 'active' : 'rate_limited',
+        desc: 'Gemini 2.0 Flash analyzes high-edge signals with natural language reasoning',
+        stats: { callsToday: geminiCallCount, dailyBudget: GEMINI_DAILY_LIMIT, model: GEMINI_MODEL },
+        skills: ['Signal Analysis', 'News Interpretation', 'Risk Assessment', 'Market Reasoning'],
+      },
+      {
+        id: 'pulse', name: 'Pulse', role: 'Sentiment Analyst', emoji: '💓',
+        color: '#EF4444', status: 'active',
+        desc: '75 event-specific rules across 7 categories with certainty modifiers and source diversity scoring',
+        stats: { rules: 75, categories: 7, avgSentiment: parseFloat(sentimentStats.avg_sentiment || 0).toFixed(3) },
+        skills: ['Event Sentiment', 'Certainty Detection', 'Multi-source Agreement', 'Category Weighting'],
+      },
+      {
+        id: 'hawk', name: 'Hawk', role: 'Market Watcher', emoji: '🦅',
+        color: '#F59E0B', status: 'active',
+        desc: 'Tracks momentum, volume spikes, spreads, and price action across all active markets',
+        stats: { activeMarkets: cachedActiveMarkets.length, trackedMarkets: parseInt(marketStats.tracked_markets) || 0 },
+        skills: ['Momentum Tracking', 'Volume Analysis', 'Spread Detection', 'Price Action'],
+      },
+      {
+        id: 'arbiter', name: 'Arbiter', role: 'Cross-Market Arbitrage', emoji: '⚖️',
+        color: '#14B8A6', status: 'active',
+        desc: 'Compares Kalshi prices vs Polymarket, PredictIt, Manifold, Metaculus for arbitrage',
+        stats: { platforms: 5 },
+        skills: ['Polymarket', 'PredictIt', 'Manifold', 'Metaculus', 'Fuzzy Matching'],
+      },
+      {
+        id: 'shield', name: 'Shield', role: 'Risk Manager', emoji: '🛡️',
+        color: '#22C55E', status: 'active',
+        desc: 'Enforces exposure limits, edge thresholds, time decay, and liquidity requirements',
+        stats: {
+          signalsApproved: parseInt(pipelineStats.approved) || 0,
+          totalSignals: parseInt(pipelineStats.total_signals) || 0,
+          maxConf: parseFloat(pipelineStats.max_confidence || 0).toFixed(2),
+        },
+        skills: ['Position Sizing', 'Exposure Limits', 'Edge Threshold', 'Time Decay', 'Liquidity Check'],
+      },
+      {
+        id: 'storm', name: 'Storm', role: 'Weather Predictor', emoji: '⛈️',
+        color: '#06B6D4', status: 'active',
+        desc: 'NWS forecast data + Bayesian model for 11 US cities, climatological priors by season',
+        stats: { cities: 11 },
+        skills: ['NWS Forecasts', 'Bayesian Weather', 'Climatological Priors', 'Temperature/Precip/Wind'],
+      },
+    ];
+
+    json(res, {
+      ceo: { name: 'Ken', title: 'CEO, Ken Inc.', tagline: 'Trading Intelligence Corporation', color: '#FF69B4' },
+      agents: subAgents,
+      pipeline: {
+        signalsLast24h: parseInt(pipelineStats.total_signals) || 0,
+        approvedLast24h: parseInt(pipelineStats.approved) || 0,
+        lastHour: parseInt(pipelineStats.last_hour) || 0,
+        avgEdge: parseFloat(pipelineStats.avg_edge || 0).toFixed(3),
+      },
+      system: {
+        memory_mb: Math.round(mem.rss / 1024 / 1024),
+        uptime_hours: Math.round(process.uptime() / 3600 * 10) / 10,
+        cpuCores: os.cpus().length,
+        pid: process.pid,
+      },
+      recentSignals: recentSignals.map(s => ({
+        market: s.market_id,
+        direction: s.direction,
+        edge: parseFloat(s.expected_edge || 0),
+        confidence: parseFloat(s.confidence || 0),
+        approved: s.risk_approved,
+        reasoning: s.reasoning?.substring(0, 200),
+        time: s.created_at,
+      })),
+    });
+  },
+
+  // ── Market Data — stock-market style data points ──
+  'GET /api/inc/market-data': async (req, res) => {
+    const out = {};
+
+    // 1. Hourly signal volume — last 24h in 1-hour buckets
+    try {
+      const { rows } = await kenQ(`SELECT date_trunc('hour', created_at) AS hour,
+        COUNT(*) AS signals, COUNT(*) FILTER (WHERE risk_approved) AS approved,
+        AVG(ABS(expected_edge::numeric)) AS avg_edge,
+        AVG(confidence::numeric) AS avg_confidence
+        FROM ken_strategy_signals
+        WHERE created_at > NOW() - INTERVAL '24 hours'
+        GROUP BY 1 ORDER BY 1`);
+      out.signalTrend = rows.map(r => ({
+        hour: r.hour, signals: parseInt(r.signals), approved: parseInt(r.approved),
+        avgEdge: parseFloat(parseFloat(r.avg_edge || 0).toFixed(4)),
+        avgConf: parseFloat(parseFloat(r.avg_confidence || 0).toFixed(3)),
+      }));
+    } catch { out.signalTrend = []; }
+
+    // 2. Sentiment volume by hour — last 24h
+    try {
+      const { rows } = await kenQ(`SELECT date_trunc('hour', captured_at) AS hour,
+        COUNT(*) AS articles, AVG(sentiment_score) AS avg_sentiment,
+        COUNT(DISTINCT source) AS sources
+        FROM ken_sentiment
+        WHERE captured_at > NOW() - INTERVAL '24 hours'
+        GROUP BY 1 ORDER BY 1`);
+      out.sentimentTrend = rows.map(r => ({
+        hour: r.hour, articles: parseInt(r.articles),
+        avgSentiment: parseFloat(parseFloat(r.avg_sentiment || 0).toFixed(3)),
+        sources: parseInt(r.sources),
+      }));
+    } catch { out.sentimentTrend = []; }
+
+    // 3. Top sentiment sources with counts
+    try {
+      const { rows } = await kenQ(`SELECT source, COUNT(*) AS cnt,
+        AVG(sentiment_score) AS avg_sent
+        FROM ken_sentiment WHERE captured_at > NOW() - INTERVAL '24 hours'
+        GROUP BY source ORDER BY cnt DESC LIMIT 15`);
+      out.sourceMix = rows.map(r => ({
+        source: r.source, count: parseInt(r.cnt),
+        avgSentiment: parseFloat(parseFloat(r.avg_sent || 0).toFixed(3)),
+      }));
+    } catch { out.sourceMix = []; }
+
+    // 4. Top tickers / categories from sentiment
+    try {
+      const { rows } = await kenQ(`SELECT
+        COALESCE(relevance_ticker, 'general') AS category, COUNT(*) AS cnt
+        FROM ken_sentiment WHERE captured_at > NOW() - INTERVAL '24 hours'
+        AND relevance_ticker IS NOT NULL AND relevance_ticker != ''
+        GROUP BY relevance_ticker ORDER BY cnt DESC LIMIT 12`);
+      out.categories = rows.map(r => ({ category: r.category, count: parseInt(r.cnt) }));
+    } catch { out.categories = []; }
+
+    // 5. Portfolio P&L (current state) — compute actual PnL from invested vs returned
+    try {
+      const { rows } = await kenQ(`SELECT p.id, p.name, p.strategy, p.trades_won, p.trades_lost, p.trades_expired,
+        p.total_invested_cents, p.total_returned_cents, p.daily_budget_cents, p.streak,
+        p.best_trade_cents, p.worst_trade_cents, p.last_trade_at,
+        (p.total_returned_cents - p.total_invested_cents) AS pnl_cents,
+        CASE WHEN (p.trades_won + p.trades_lost) > 0
+          THEN ROUND(p.trades_won * 100.0 / (p.trades_won + p.trades_lost))
+          ELSE 0 END AS win_rate,
+        (SELECT COUNT(*) FROM ken_portfolio_trades t WHERE t.portfolio_id = p.id) AS total_trades
+        FROM ken_portfolios p ORDER BY (p.total_returned_cents - p.total_invested_cents) DESC`);
+      out.portfolios = rows.map(r => ({
+        ...r,
+        pnl_cents: parseInt(r.pnl_cents) || 0,
+        win_rate: parseInt(r.win_rate) || 0,
+        total_trades: parseInt(r.total_trades) || 0,
+        total_invested_cents: parseInt(r.total_invested_cents) || 0,
+        total_returned_cents: parseInt(r.total_returned_cents) || 0,
+      }));
+    } catch { out.portfolios = []; }
+
+    // 6. Active markets count and top movers
+    out.activeMarkets = cachedActiveMarkets.length;
+    out.momentumKeys = Object.keys(cachedMomentumMap).length;
+
+    // 7. Signal approval rate over last 6 hours vs prior 6
+    try {
+      const { rows } = await kenQ(`SELECT
+        COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '6 hours') AS recent_total,
+        COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '6 hours' AND risk_approved) AS recent_approved,
+        COUNT(*) FILTER (WHERE created_at BETWEEN NOW() - INTERVAL '12 hours' AND NOW() - INTERVAL '6 hours') AS prior_total,
+        COUNT(*) FILTER (WHERE created_at BETWEEN NOW() - INTERVAL '12 hours' AND NOW() - INTERVAL '6 hours' AND risk_approved) AS prior_approved
+        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '12 hours'`);
+      const r = rows[0] || {};
+      const recentRate = r.recent_total > 0 ? (r.recent_approved / r.recent_total * 100) : 0;
+      const priorRate = r.prior_total > 0 ? (r.prior_approved / r.prior_total * 100) : 0;
+      out.approvalTrend = {
+        current: Math.round(recentRate), prior: Math.round(priorRate),
+        change: Math.round(recentRate - priorRate),
+        recentSignals: parseInt(r.recent_total), priorSignals: parseInt(r.prior_total),
+      };
+    } catch { out.approvalTrend = { current: 0, prior: 0, change: 0 }; }
+
+    // 8. Gemini usage
+    out.gemini = { callsToday: geminiCallCount, dailyLimit: GEMINI_DAILY_LIMIT, pctUsed: Math.round(geminiCallCount / GEMINI_DAILY_LIMIT * 100) };
+
+    // 9. Latest signals feed — most recent 15 signals for live ticker
+    try {
+      const { rows } = await kenQ(`SELECT market_id, direction, expected_edge, confidence, risk_approved, created_at,
+        SUBSTRING(reasoning FROM 1 FOR 80) AS reason_short
+        FROM ken_strategy_signals ORDER BY created_at DESC LIMIT 15`);
+      out.latestSignals = rows.map(r => ({
+        market: r.market_id, direction: r.direction,
+        edge: parseFloat(parseFloat(r.expected_edge || 0).toFixed(4)),
+        confidence: parseFloat(parseFloat(r.confidence || 0).toFixed(3)),
+        approved: r.risk_approved, reason: r.reason_short,
+        time: r.created_at,
+      }));
+    } catch { out.latestSignals = []; }
+
+    // 10. Per-market signal stats — heatmap data
+    try {
+      const { rows } = await kenQ(`SELECT market_id,
+        COUNT(*) AS total_signals,
+        AVG(expected_edge::numeric) AS avg_edge,
+        AVG(confidence::numeric) AS avg_conf,
+        COUNT(*) FILTER (WHERE risk_approved) AS approved,
+        MAX(created_at) AS last_signal
+        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'
+        GROUP BY market_id ORDER BY total_signals DESC LIMIT 20`);
+      out.marketStats = rows.map(r => ({
+        market: r.market_id, signals: parseInt(r.total_signals),
+        avgEdge: parseFloat(parseFloat(r.avg_edge || 0).toFixed(4)),
+        avgConf: parseFloat(parseFloat(r.avg_conf || 0).toFixed(3)),
+        approved: parseInt(r.approved), lastSignal: r.last_signal,
+      }));
+    } catch { out.marketStats = []; }
+
+    // 11. Recent trades with details
+    try {
+      const { rows } = await kenQ(`SELECT t.id, t.portfolio_id, t.market_id, t.market_title, t.direction, t.contracts,
+        t.entry_price_cents, t.pnl_cents, t.status, t.cost_cents, t.confidence, t.created_at,
+        p.name AS portfolio_name, p.strategy
+        FROM ken_portfolio_trades t
+        JOIN ken_portfolios p ON p.id = t.portfolio_id
+        ORDER BY t.created_at DESC LIMIT 20`);
+      out.recentTrades = rows.map(r => ({
+        ...r, entry_price_cents: parseInt(r.entry_price_cents) || 0, pnl_cents: parseInt(r.pnl_cents) || 0,
+        contracts: parseInt(r.contracts) || 0, cost_cents: parseInt(r.cost_cents) || 0,
+      }));
+    } catch { out.recentTrades = []; }
+
+    // 12. Confidence distribution histogram (10 buckets from 0-1)
+    try {
+      const { rows } = await kenQ(`SELECT
+        WIDTH_BUCKET(confidence::numeric, 0, 1, 10) AS bucket,
+        COUNT(*) AS cnt
+        FROM ken_strategy_signals WHERE created_at > NOW() - INTERVAL '24 hours'
+        GROUP BY bucket ORDER BY bucket`);
+      out.confDistribution = Array.from({length: 10}, (_, i) => {
+        const found = rows.find(r => parseInt(r.bucket) === i + 1);
+        return { range: (i * 10) + '-' + ((i + 1) * 10) + '%', count: found ? parseInt(found.cnt) : 0 };
+      });
+    } catch { out.confDistribution = []; }
+
+    json(res, out);
+  },
+};
+
+// ════════════════════════════════════════
+// TOURNAMENT ARENA — 3D TEAM COMPETITION
+// ════════════════════════════════════════
+const ARENA_HTML = `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Tournament Arena | Ken Trading</title>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{background:#000;overflow:hidden;font-family:'Segoe UI',system-ui,sans-serif;color:#fff}
+#c{position:fixed;top:0;left:0;z-index:0}
+#hud{position:fixed;top:0;left:0;right:0;z-index:10;pointer-events:none}
+.top-bar{display:flex;justify-content:center;align-items:center;padding:20px 40px;gap:30px}
+.team-badge{display:flex;align-items:center;gap:12px;padding:10px 24px;border-radius:12px;pointer-events:auto;cursor:default}
+.team-badge.alpha{background:rgba(99,102,241,.15);border:1px solid rgba(99,102,241,.4)}
+.team-badge.beta{background:rgba(244,63,94,.15);border:1px solid rgba(244,63,94,.4)}
+.team-badge h2{font-size:16px;letter-spacing:2px;text-transform:uppercase}
+.team-badge.alpha h2{color:#818cf8}
+.team-badge.beta h2{color:#fb7185}
+.vs{font-size:32px;font-weight:900;background:linear-gradient(135deg,#818cf8,#fb7185);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-shadow:none;filter:drop-shadow(0 0 20px rgba(168,85,247,.5))}
+.scoreboard{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:10;display:flex;gap:8px;background:rgba(0,0,0,.7);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.08);border-radius:16px;padding:12px 20px}
+.score-card{text-align:center;padding:6px 16px;border-radius:10px;min-width:100px}
+.score-card.alpha{background:rgba(99,102,241,.1);border:1px solid rgba(99,102,241,.2)}
+.score-card.beta{background:rgba(244,63,94,.1);border:1px solid rgba(244,63,94,.2)}
+.score-card .name{font-size:11px;font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:4px}
+.score-card.alpha .name{color:#818cf8}
+.score-card.beta .name{color:#fb7185}
+.score-card .strat{font-size:9px;color:#666;margin-bottom:6px}
+.score-card .pnl{font-size:18px;font-weight:800;font-family:'Courier New',monospace}
+.score-card .pnl.pos{color:#4ade80}
+.score-card .pnl.neg{color:#f87171}
+.score-card .pnl.zero{color:#666}
+.score-card .trades{font-size:9px;color:#888;margin-top:2px}
+.score-card .bar{height:3px;border-radius:2px;margin-top:6px;background:#1a1a2e;overflow:hidden}
+.score-card .bar-fill{height:100%;border-radius:2px;transition:width .5s ease}
+.week-info{position:fixed;top:80px;left:50%;transform:translateX(-50%);z-index:10;font-size:11px;color:#555;letter-spacing:2px;text-transform:uppercase}
+.team-total{font-size:11px;font-weight:700;margin-top:4px;letter-spacing:1px}
+.back-link{position:fixed;top:20px;left:20px;z-index:20;color:#666;text-decoration:none;font-size:12px;pointer-events:auto}
+.back-link:hover{color:#fff}
+.refresh-note{position:fixed;bottom:4px;left:50%;transform:translateX(-50%);z-index:10;font-size:9px;color:#333}
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<a href="/inc" class="back-link">&larr; Command Center</a>
+<div id="hud">
+  <div class="top-bar">
+    <div class="team-badge alpha"><div style="width:12px;height:12px;border-radius:50%;background:#818cf8;box-shadow:0 0 12px #818cf8"></div><h2>Team Alpha</h2></div>
+    <div class="vs">VS</div>
+    <div class="team-badge beta"><div style="width:12px;height:12px;border-radius:50%;background:#fb7185;box-shadow:0 0 12px #fb7185"></div><h2>Team Beta</h2></div>
+  </div>
+</div>
+<div id="week-info" class="week-info">Loading tournament...</div>
+<div id="scoreboard" class="scoreboard"></div>
+<div class="refresh-note">Auto-refreshes every 15s</div>
+
+<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"><\/script>
+<script>
+const scene = new THREE.Scene();
+const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 1000);
+const renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('c'), antialias: true, alpha: true });
+renderer.setSize(innerWidth, innerHeight);
+renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
+camera.position.set(0, 4, 12);
+camera.lookAt(0, 0, 0);
+
+// Lighting
+scene.add(new THREE.AmbientLight(0x222244, 0.5));
+const spotA = new THREE.SpotLight(0x6366f1, 2, 30, Math.PI/4);
+spotA.position.set(-6, 10, 5);
+scene.add(spotA);
+const spotB = new THREE.SpotLight(0xf43f5e, 2, 30, Math.PI/4);
+spotB.position.set(6, 10, 5);
+scene.add(spotB);
+const rimLight = new THREE.PointLight(0xa855f7, 1, 20);
+rimLight.position.set(0, 8, -5);
+scene.add(rimLight);
+
+// Arena floor
+const floorGeo = new THREE.CircleGeometry(8, 64);
+const floorMat = new THREE.MeshStandardMaterial({ color: 0x0a0a1a, metalness: 0.8, roughness: 0.3 });
+const floor = new THREE.Mesh(floorGeo, floorMat);
+floor.rotation.x = -Math.PI / 2;
+floor.position.y = -0.5;
+scene.add(floor);
+
+// Arena ring
+const ringGeo = new THREE.TorusGeometry(8, 0.05, 8, 128);
+const ringMat = new THREE.MeshStandardMaterial({ color: 0xa855f7, emissive: 0xa855f7, emissiveIntensity: 0.5 });
+const ring = new THREE.Mesh(ringGeo, ringMat);
+ring.rotation.x = -Math.PI / 2;
+ring.position.y = -0.48;
+scene.add(ring);
+
+// Center divider line
+const divGeo = new THREE.PlaneGeometry(0.02, 14);
+const divMat = new THREE.MeshBasicMaterial({ color: 0x333355, transparent: true, opacity: 0.3 });
+const divider = new THREE.Mesh(divGeo, divMat);
+divider.rotation.x = -Math.PI / 2;
+divider.position.y = -0.47;
+scene.add(divider);
+
+// Particle system for arena atmosphere
+const pCount = 500;
+const pGeo = new THREE.BufferGeometry();
+const pPos = new Float32Array(pCount * 3);
+const pCol = new Float32Array(pCount * 3);
+for (let i = 0; i < pCount; i++) {
+  const r = Math.random() * 10;
+  const a = Math.random() * Math.PI * 2;
+  pPos[i*3] = Math.cos(a) * r;
+  pPos[i*3+1] = Math.random() * 8 - 0.5;
+  pPos[i*3+2] = Math.sin(a) * r;
+  const isAlpha = pPos[i*3] < 0;
+  pCol[i*3] = isAlpha ? 0.38 : 0.96;
+  pCol[i*3+1] = isAlpha ? 0.39 : 0.24;
+  pCol[i*3+2] = isAlpha ? 0.95 : 0.37;
+}
+pGeo.setAttribute('position', new THREE.BufferAttribute(pPos, 3));
+pGeo.setAttribute('color', new THREE.BufferAttribute(pCol, 3));
+const pMat = new THREE.PointsMaterial({ size: 0.04, vertexColors: true, transparent: true, opacity: 0.4, blending: THREE.AdditiveBlending });
+const particles = new THREE.Points(pGeo, pMat);
+scene.add(particles);
+
+// Team node storage
+const teamNodes = [];
+const ALPHA_COLOR = 0x6366f1;
+const BETA_COLOR = 0xf43f5e;
+const TEAM_POSITIONS = {
+  alpha: [[-3.5, 1, -1.5], [-2, 1, 2], [-4.5, 1, 1]],
+  beta:  [[3.5, 1, -1.5], [2, 1, 2], [4.5, 1, 1]]
+};
+
+function createTeamNode(pos, color, name, strategy) {
+  const group = new THREE.Group();
+  // Main sphere
+  const geo = new THREE.IcosahedronGeometry(0.6, 2);
+  const mat = new THREE.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.3, metalness: 0.7, roughness: 0.2, wireframe: false });
+  const sphere = new THREE.Mesh(geo, mat);
+  group.add(sphere);
+  // Glow ring
+  const glowGeo = new THREE.TorusGeometry(0.9, 0.03, 8, 64);
+  const glowMat = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.5 });
+  const glowRing = new THREE.Mesh(glowGeo, glowMat);
+  glowRing.rotation.x = Math.PI / 2;
+  group.add(glowRing);
+  // Outer shell wireframe
+  const shellGeo = new THREE.IcosahedronGeometry(0.75, 1);
+  const shellMat = new THREE.MeshBasicMaterial({ color, wireframe: true, transparent: true, opacity: 0.15 });
+  const shell = new THREE.Mesh(shellGeo, shellMat);
+  group.add(shell);
+  // Name label (canvas texture)
+  const canvas = document.createElement('canvas');
+  canvas.width = 256; canvas.height = 80;
+  const ctx = canvas.getContext('2d');
+  ctx.fillStyle = '#fff';
+  ctx.font = 'bold 28px Segoe UI';
+  ctx.textAlign = 'center';
+  ctx.fillText(name, 128, 30);
+  ctx.font = '16px Segoe UI';
+  ctx.fillStyle = '#888';
+  ctx.fillText(strategy, 128, 58);
+  const tex = new THREE.CanvasTexture(canvas);
+  const labelGeo = new THREE.PlaneGeometry(2, 0.6);
+  const labelMat = new THREE.MeshBasicMaterial({ map: tex, transparent: true, depthTest: false });
+  const label = new THREE.Mesh(labelGeo, labelMat);
+  label.position.y = 1.5;
+  group.add(label);
+  // P&L indicator bar (will be updated)
+  const barBgGeo = new THREE.PlaneGeometry(1.2, 0.08);
+  const barBgMat = new THREE.MeshBasicMaterial({ color: 0x1a1a2e, transparent: true, opacity: 0.8 });
+  const barBg = new THREE.Mesh(barBgGeo, barBgMat);
+  barBg.position.y = 1.1;
+  group.add(barBg);
+  const barGeo = new THREE.PlaneGeometry(0.01, 0.06);
+  const barMat = new THREE.MeshBasicMaterial({ color: 0x4ade80, transparent: true, opacity: 0.9 });
+  const bar = new THREE.Mesh(barGeo, barMat);
+  bar.position.y = 1.1;
+  bar.position.x = -0.58;
+  group.add(bar);
+  group.position.set(...pos);
+  scene.add(group);
+  return { group, sphere, shell, glowRing, bar, barMat, mat, name, pnl: 0, trades: 0 };
+}
+
+// Energy beams connecting team members
+function createTeamBeam(nodeA, nodeB, color) {
+  const geo = new THREE.BufferGeometry();
+  const positions = new Float32Array(6);
+  geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
+  const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.15, blending: THREE.AdditiveBlending });
+  const line = new THREE.Line(geo, mat);
+  scene.add(line);
+  return { line, nodeA, nodeB, geo };
+}
+const beams = [];
+
+// Data fetching
+let tournamentData = null;
+async function fetchData() {
+  try {
+    const r = await fetch('/api/tournament/current');
+    const d = await r.json();
+    if (!d.active) return;
+    tournamentData = d;
+    updateScene(d);
+    updateHUD(d);
+  } catch(e) { console.error('Fetch error:', e); }
+}
+
+function updateScene(d) {
+  const entries = d.entries || [];
+  // First 3 = alpha, next 3 = beta (sorted by entry id)
+  const sorted = [...entries].sort((a,b) => a.id - b.id);
+  const alphaEntries = sorted.slice(0, 3);
+  const betaEntries = sorted.slice(3, 6);
+  // Create nodes if not yet created
+  if (teamNodes.length === 0) {
+    alphaEntries.forEach((e, i) => {
+      const n = createTeamNode(TEAM_POSITIONS.alpha[i], ALPHA_COLOR, e.name, e.strategy);
+      n.entry = e;
+      n.team = 'alpha';
+      teamNodes.push(n);
+    });
+    betaEntries.forEach((e, i) => {
+      const n = createTeamNode(TEAM_POSITIONS.beta[i], BETA_COLOR, e.name, e.strategy);
+      n.entry = e;
+      n.team = 'beta';
+      teamNodes.push(n);
+    });
+    // Create beams within each team
+    for (let i = 0; i < 3; i++) for (let j = i+1; j < 3; j++) {
+      beams.push(createTeamBeam(teamNodes[i], teamNodes[j], ALPHA_COLOR));
+    }
+    for (let i = 3; i < 6; i++) for (let j = i+1; j < 6; j++) {
+      beams.push(createTeamBeam(teamNodes[i], teamNodes[j], BETA_COLOR));
+    }
+  }
+  // Update node data
+  const allEntries = [...alphaEntries, ...betaEntries];
+  allEntries.forEach((e, i) => {
+    if (!teamNodes[i]) return;
+    const n = teamNodes[i];
+    n.pnl = e.live_pnl || 0;
+    n.trades = e.live_trades || 0;
+    n.entry = e;
+    // Update emissive based on P&L
+    const intensity = Math.min(0.8, 0.3 + Math.abs(n.pnl) / 500);
+    n.mat.emissiveIntensity = intensity;
+    // Update P&L bar
+    const pnlPct = Math.max(0.01, Math.min(1, (n.pnl + 2000) / 4000));
+    n.bar.scale.x = pnlPct * 116;
+    n.bar.position.x = -0.58 + (pnlPct * 1.16) / 2;
+    n.barMat.color.setHex(n.pnl >= 0 ? 0x4ade80 : 0xf87171);
+  });
+}
+
+function updateHUD(d) {
+  const entries = d.entries || [];
+  const sorted = [...entries].sort((a,b) => a.id - b.id);
+  const t = d.tournament;
+  const wStart = new Date(t.week_start).toLocaleDateString('en-US', {month:'short',day:'numeric'});
+  const wEnd = new Date(t.week_end).toLocaleDateString('en-US', {month:'short',day:'numeric'});
+  document.getElementById('week-info').textContent = 'Week ' + t.id + ' • ' + wStart + ' - ' + wEnd + ' • $' + (t.budget_per_portfolio_cents/100).toFixed(0) + ' per portfolio';
+  let html = '';
+  let alphaTotal = 0, betaTotal = 0;
+  sorted.forEach((e, i) => {
+    const isAlpha = i < 3;
+    const pnl = e.live_pnl || 0;
+    if (isAlpha) alphaTotal += pnl; else betaTotal += pnl;
+    const pnlClass = pnl > 0 ? 'pos' : pnl < 0 ? 'neg' : 'zero';
+    const pnlStr = (pnl >= 0 ? '+' : '') + '$' + (pnl/100).toFixed(2);
+    const budgetUsed = Math.min(100, (e.live_invested || 0) / e.start_balance_cents * 100);
+    const barColor = isAlpha ? '#818cf8' : '#fb7185';
+    html += '<div class="score-card ' + (isAlpha?'alpha':'beta') + '">';
+    html += '<div class="name">' + e.name + '</div>';
+    html += '<div class="strat">' + e.strategy + '</div>';
+    html += '<div class="pnl ' + pnlClass + '">' + pnlStr + '</div>';
+    html += '<div class="trades">' + (e.live_trades||0) + ' trades • $' + ((e.live_invested||0)/100).toFixed(2) + ' deployed</div>';
+    html += '<div class="bar"><div class="bar-fill" style="width:' + budgetUsed + '%;background:' + barColor + '"></div></div>';
+    html += '</div>';
+    if (i === 2) {
+      html += '<div style="display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 8px">';
+      html += '<div style="font-size:20px;font-weight:900;background:linear-gradient(135deg,#818cf8,#fb7185);-webkit-background-clip:text;-webkit-text-fill-color:transparent">VS</div>';
+      html += '</div>';
+    }
+  });
+  document.getElementById('scoreboard').innerHTML = html;
+}
+
+// Animation
+let time = 0;
+function animate() {
+  requestAnimationFrame(animate);
+  time += 0.01;
+  // Rotate camera slowly
+  camera.position.x = Math.sin(time * 0.15) * 12;
+  camera.position.z = Math.cos(time * 0.15) * 12;
+  camera.position.y = 4 + Math.sin(time * 0.3) * 0.5;
+  camera.lookAt(0, 0.5, 0);
+  // Animate nodes
+  teamNodes.forEach((n, i) => {
+    const base = n.group.position.y;
+    n.sphere.position.y = Math.sin(time * 2 + i) * 0.15;
+    n.sphere.rotation.y = time + i;
+    n.shell.rotation.y = -time * 0.5 + i;
+    n.shell.rotation.x = time * 0.3;
+    n.glowRing.rotation.z = time * (i % 2 === 0 ? 1 : -1);
+    // Pulse based on trades
+    const pulse = 1 + Math.sin(time * 3 + i * 2) * 0.05 * (n.trades > 0 ? 1 : 0.2);
+    n.sphere.scale.setScalar(pulse);
+  });
+  // Update beams
+  beams.forEach(b => {
+    const pa = b.nodeA.group.position;
+    const pb = b.nodeB.group.position;
+    const pos = b.geo.attributes.position.array;
+    pos[0] = pa.x; pos[1] = pa.y + 1; pos[2] = pa.z;
+    pos[3] = pb.x; pos[4] = pb.y + 1; pos[5] = pb.z;
+    b.geo.attributes.position.needsUpdate = true;
+    b.line.material.opacity = 0.1 + Math.sin(time * 2) * 0.05;
+  });
+  // Animate particles
+  const pp = particles.geometry.attributes.position.array;
+  for (let i = 0; i < pCount; i++) {
+    pp[i*3+1] += Math.sin(time + i) * 0.002;
+    if (pp[i*3+1] > 8) pp[i*3+1] = -0.5;
+  }
+  particles.geometry.attributes.position.needsUpdate = true;
+  particles.rotation.y = time * 0.02;
+  // Ring pulse
+  ring.material.emissiveIntensity = 0.3 + Math.sin(time * 2) * 0.2;
+  renderer.render(scene, camera);
+}
+
+// Resize
+addEventListener('resize', () => {
+  camera.aspect = innerWidth / innerHeight;
+  camera.updateProjectionMatrix();
+  renderer.setSize(innerWidth, innerHeight);
+});
+
+// Init
+fetchData();
+setInterval(fetchData, 15000);
+animate();
+<\/script>
+</body>
+</html>`;
+
+// KEN INC COMMAND CENTER PAGE
+// ════════════════════════════════════════
+const KEN_INC_HTML = `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Ken Inc. | Trading Intelligence Command Center</title>
+<script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
+<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
+<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
+<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+:root{--bg:#050510;--card:rgba(15,15,35,0.85);--border:rgba(255,255,255,0.06);--text:#e2e8f0;--muted:#64748b;--pink:#ff69b4;--blue:#3b82f6;--purple:#8b5cf6;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--cyan:#06b6d4;--teal:#14b8a6}
+body{font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;overflow-x:hidden}
+
+/* Animated gradient background */
+body::before{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background:radial-gradient(ellipse at 20% 50%,rgba(139,92,246,0.08) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(255,105,180,0.06) 0%,transparent 50%),radial-gradient(ellipse at 50% 80%,rgba(59,130,246,0.06) 0%,transparent 50%);z-index:-1;animation:bgPulse 8s ease-in-out infinite alternate}
+@keyframes bgPulse{0%{opacity:0.6}100%{opacity:1}}
+
+/* Grid lines overlay */
+body::after{content:'';position:fixed;top:0;left:0;right:0;bottom:0;background-image:linear-gradient(rgba(255,255,255,0.015) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,0.015) 1px,transparent 1px);background-size:60px 60px;z-index:-1;animation:gridScroll 20s linear infinite}
+@keyframes gridScroll{to{background-position:60px 60px}}
+
+.container{max-width:1400px;margin:0 auto;padding:20px}
+
+/* Header */
+.header{text-align:center;padding:30px 20px;position:relative}
+.header-glow{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:400px;height:400px;background:radial-gradient(circle,rgba(255,105,180,0.15) 0%,transparent 70%);border-radius:50%;filter:blur(60px);animation:glowPulse 4s ease-in-out infinite alternate;pointer-events:none}
+@keyframes glowPulse{0%{opacity:0.5;transform:translate(-50%,-50%) scale(0.9)}100%{opacity:1;transform:translate(-50%,-50%) scale(1.1)}}
+.ceo-avatar{width:80px;height:80px;border-radius:50%;background:linear-gradient(135deg,#ff69b4,#8b5cf6);display:flex;align-items:center;justify-content:center;font-size:36px;margin:0 auto 12px;box-shadow:0 0 30px rgba(255,105,180,0.4),0 0 60px rgba(139,92,246,0.2);animation:avatarFloat 3s ease-in-out infinite;position:relative;z-index:2}
+@keyframes avatarFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-8px)}}
+.ceo-name{font-size:32px;font-weight:800;background:linear-gradient(135deg,#ff69b4,#c084fc,#818cf8);-webkit-background-clip:text;-webkit-text-fill-color:transparent;position:relative;z-index:2}
+.ceo-title{color:var(--muted);font-size:14px;letter-spacing:2px;text-transform:uppercase;margin-top:4px;position:relative;z-index:2}
+.ceo-tagline{color:rgba(255,105,180,0.6);font-size:11px;letter-spacing:4px;text-transform:uppercase;margin-top:8px;position:relative;z-index:2}
+
+/* Stats bar */
+.stats-bar{display:flex;justify-content:center;gap:30px;margin:24px 0;flex-wrap:wrap}
+.stat-item{text-align:center;padding:12px 24px;background:var(--card);border:1px solid var(--border);border-radius:12px;backdrop-filter:blur(10px);min-width:120px}
+.stat-val{font-size:24px;font-weight:700;font-variant-numeric:tabular-nums}
+.stat-label{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
+
+/* Agent Network */
+.network{position:relative;margin:30px auto;max-width:1200px}
+.agents-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
+@media(max-width:1024px){.agents-grid{grid-template-columns:repeat(2,1fr)}}
+@media(max-width:600px){.agents-grid{grid-template-columns:1fr}}
+
+/* Agent Card */
+.agent-card{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:20px;backdrop-filter:blur(10px);transition:all 0.3s cubic-bezier(0.4,0,0.2,1);position:relative;overflow:hidden;cursor:pointer}
+.agent-card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;border-radius:16px 16px 0 0;opacity:0;transition:opacity 0.3s}
+.agent-card:hover{transform:translateY(-4px);border-color:rgba(255,255,255,0.12);box-shadow:0 20px 40px rgba(0,0,0,0.3)}
+.agent-card:hover::before{opacity:1}
+.agent-card.expanded{grid-column:span 2}
+.agent-header{display:flex;align-items:center;gap:12px}
+.agent-emoji{font-size:28px;width:48px;height:48px;display:flex;align-items:center;justify-content:center;border-radius:12px;flex-shrink:0}
+.agent-name{font-size:16px;font-weight:700}
+.agent-role{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:0.5px}
+.status-indicator{width:8px;height:8px;border-radius:50%;position:absolute;top:16px;right:16px;animation:statusPulse 2s ease-in-out infinite}
+@keyframes statusPulse{0%,100%{opacity:1;box-shadow:0 0 4px currentColor}50%{opacity:0.6;box-shadow:0 0 12px currentColor}}
+.agent-desc{font-size:12px;color:var(--muted);line-height:1.5;margin-top:10px}
+.agent-stats{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}
+.agent-stat{padding:3px 10px;background:rgba(255,255,255,0.04);border:1px solid var(--border);border-radius:6px;font-size:11px;font-variant-numeric:tabular-nums}
+.agent-stat strong{color:var(--text);margin-right:3px}
+.skills-wrap{margin-top:12px;display:flex;flex-wrap:wrap;gap:4px}
+.skill-tag{padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;letter-spacing:0.3px}
+
+/* Signal Feed */
+.signal-feed{margin-top:30px}
+.signal-feed h2{font-size:16px;font-weight:600;margin-bottom:12px;display:flex;align-items:center;gap:8px}
+.signal-feed h2 .live-dot{width:8px;height:8px;border-radius:50%;background:var(--red);animation:livePulse 1s ease-in-out infinite}
+@keyframes livePulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:0.5;transform:scale(0.8)}}
+.signal-list{display:flex;flex-direction:column;gap:6px}
+.signal-item{display:flex;align-items:center;gap:12px;padding:10px 16px;background:var(--card);border:1px solid var(--border);border-radius:10px;font-size:12px;backdrop-filter:blur(10px);transition:border-color 0.2s}
+.signal-item:hover{border-color:rgba(255,255,255,0.12)}
+.signal-dir{padding:3px 8px;border-radius:4px;font-weight:700;font-size:11px;min-width:60px;text-align:center}
+.signal-dir.buy-yes{background:rgba(34,197,94,0.15);color:var(--green)}
+.signal-dir.buy-no{background:rgba(239,68,68,0.15);color:var(--red)}
+.signal-market{flex:1;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.signal-edge{font-weight:700;font-variant-numeric:tabular-nums;min-width:50px;text-align:right}
+.signal-conf{color:var(--muted);font-variant-numeric:tabular-nums;min-width:40px;text-align:right}
+.signal-time{color:var(--muted);font-size:11px;min-width:65px;text-align:right}
+.signal-approved{width:18px;height:18px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:10px;flex-shrink:0}
+
+/* Connection Lines Animation */
+.connections-svg{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}
+.data-flow{stroke-dasharray:8 4;animation:flowDash 1s linear infinite}
+@keyframes flowDash{to{stroke-dashoffset:-12}}
+
+/* Particle effect */
+.particles{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0}
+.particle{position:absolute;width:2px;height:2px;border-radius:50%;opacity:0;animation:particleFloat linear infinite}
+@keyframes particleFloat{0%{opacity:0;transform:translateY(0)}10%{opacity:0.6}90%{opacity:0.6}100%{opacity:0;transform:translateY(-100vh)}}
+
+/* Clock */
+.clock{position:fixed;top:16px;right:20px;text-align:right;z-index:10}
+.clock-time{font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--text)}
+.clock-date{font-size:11px;color:var(--muted)}
+
+/* Sparkline */
+.sparkline-wrap{display:inline-block;vertical-align:middle}
+.sparkline-svg{display:block}
+
+/* ─── Market Data Terminal ─── */
+.mkt-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:14px;margin-top:16px}
+.mkt-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;backdrop-filter:blur(10px);transition:all .3s;position:relative;overflow:hidden}
+.mkt-card::after{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,transparent,var(--border),transparent);opacity:0;transition:opacity .3s}
+.mkt-card:hover{border-color:rgba(255,255,255,.12);transform:translateY(-2px);box-shadow:0 12px 40px rgba(0,0,0,.3)}
+.mkt-card:hover::after{opacity:1}
+.mkt-card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
+.mkt-card-title{font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
+.mkt-card-badge{padding:3px 8px;border-radius:6px;font-size:10px;font-weight:700;font-variant-numeric:tabular-nums}
+.mkt-bar-chart{display:flex;align-items:flex-end;gap:3px;height:80px;padding-top:4px}
+.mkt-bar{flex:1;border-radius:3px 3px 0 0;min-width:4px;transition:height .5s ease;position:relative}
+.mkt-bar:hover{opacity:1!important;filter:brightness(1.3)}
+.mkt-bar-label{position:absolute;top:-16px;left:50%;transform:translateX(-50%);font-size:8px;color:var(--muted);white-space:nowrap;display:none;background:var(--card);padding:1px 4px;border-radius:3px;border:1px solid var(--border)}
+.mkt-bar:hover .mkt-bar-label{display:block}
+.mkt-source-list{display:flex;flex-direction:column;gap:5px}
+.mkt-source-row{display:flex;align-items:center;gap:8px;font-size:11px;padding:2px 0;transition:background .2s;border-radius:4px}
+.mkt-source-row:hover{background:rgba(255,255,255,.03)}
+.mkt-source-bar{height:7px;border-radius:4px;transition:width .6s cubic-bezier(.4,0,.2,1)}
+.mkt-source-name{min-width:85px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.mkt-source-val{font-variant-numeric:tabular-nums;color:var(--muted);min-width:45px;text-align:right;font-weight:600}
+.mkt-gauge{position:relative;width:100%;height:14px;background:rgba(255,255,255,.05);border-radius:7px;overflow:hidden;margin-top:8px}
+.mkt-gauge-fill{height:100%;border-radius:7px;transition:width .6s cubic-bezier(.4,0,.2,1)}
+.mkt-gauge-label{position:absolute;top:50%;right:8px;transform:translateY(-50%);font-size:9px;font-weight:700;color:var(--text);text-shadow:0 1px 2px rgba(0,0,0,.5)}
+/* Scrolling ticker */
+.mkt-ticker-strip{overflow:hidden;padding:12px 0;border-top:1px solid rgba(255,255,255,.06);border-bottom:1px solid rgba(255,255,255,.06);margin:16px 0;background:linear-gradient(90deg,rgba(5,5,16,.8),transparent 5%,transparent 95%,rgba(5,5,16,.8))}
+.mkt-ticker-track{display:flex;gap:24px;animation:tickerScroll 40s linear infinite;width:max-content}
+.mkt-ticker-track:hover{animation-play-state:paused}
+@keyframes tickerScroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
+.mkt-ticker-item{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:600;white-space:nowrap;font-variant-numeric:tabular-nums;padding:4px 12px;border-radius:6px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.04)}
+.mkt-change{font-size:11px;font-weight:700}
+.mkt-change.up{color:var(--green)}
+.mkt-change.down{color:var(--red)}
+.mkt-change.flat{color:var(--muted)}
+/* Signal feed */
+.signal-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto}
+.signal-feed::-webkit-scrollbar{width:4px}
+.signal-feed::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
+.signal-row{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:8px;font-size:11px;transition:all .2s;border:1px solid transparent;animation:signalIn .3s ease}
+.signal-row:hover{background:rgba(255,255,255,.04);border-color:var(--border)}
+@keyframes signalIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:none}}
+.signal-dir{padding:2px 8px;border-radius:4px;font-weight:800;font-size:10px;text-transform:uppercase;letter-spacing:.5px}
+.signal-dir.yes{background:rgba(34,197,94,.15);color:var(--green)}
+.signal-dir.no{background:rgba(239,68,68,.15);color:var(--red)}
+.signal-market{font-weight:600;min-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.signal-edge{font-variant-numeric:tabular-nums;font-weight:700;min-width:50px;text-align:right}
+.signal-conf{font-variant-numeric:tabular-nums;min-width:40px;text-align:right;color:var(--muted)}
+.signal-time{font-size:9px;color:var(--muted);margin-left:auto;white-space:nowrap}
+.signal-approved{width:6px;height:6px;border-radius:50%;flex-shrink:0}
+/* Heatmap grid */
+.heatmap-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px}
+.heatmap-cell{padding:8px 10px;border-radius:8px;border:1px solid var(--border);transition:all .2s;cursor:default;text-align:center}
+.heatmap-cell:hover{transform:scale(1.03);z-index:1;box-shadow:0 4px 16px rgba(0,0,0,.4)}
+.heatmap-market{font-size:10px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-bottom:2px}
+.heatmap-edge{font-size:16px;font-weight:800;font-variant-numeric:tabular-nums}
+.heatmap-meta{font-size:8px;color:var(--muted);margin-top:2px}
+/* Histogram */
+.histogram{display:flex;align-items:flex-end;gap:2px;height:80px}
+.hist-bar{flex:1;border-radius:3px 3px 0 0;transition:height .5s;position:relative;min-width:0}
+.hist-bar:hover{filter:brightness(1.3)}
+.hist-label{position:absolute;bottom:-16px;left:50%;transform:translateX(-50%);font-size:7px;color:var(--muted);white-space:nowrap}
+/* Trade log */
+.trade-log{display:flex;flex-direction:column;gap:3px;max-height:260px;overflow-y:auto}
+.trade-log::-webkit-scrollbar{width:4px}
+.trade-log::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
+.trade-row{display:flex;align-items:center;gap:6px;padding:5px 10px;border-radius:6px;font-size:10px;border:1px solid transparent;transition:all .2s}
+.trade-row:hover{background:rgba(255,255,255,.03);border-color:var(--border)}
+.trade-portfolio{font-weight:600;min-width:70px}
+.trade-pnl{font-weight:800;font-variant-numeric:tabular-nums;min-width:55px;text-align:right}
+/* P&L big number */
+.pnl-hero{font-size:48px;font-weight:900;font-variant-numeric:tabular-nums;letter-spacing:-1px;line-height:1}
+@keyframes pnlPulse{0%,100%{text-shadow:0 0 8px currentColor}50%{text-shadow:0 0 20px currentColor}}
+
+/* ─── PortFAUXlio Dashboard ─── */
+.pf-banner{text-align:center;padding:14px;margin-bottom:20px;background:linear-gradient(135deg,rgba(139,92,246,.08),rgba(255,105,180,.06));border:1px solid rgba(139,92,246,.2);border-radius:14px;position:relative;overflow:hidden}
+.pf-banner::before{content:'SIMULATED';position:absolute;top:-2px;right:20px;font-size:8px;letter-spacing:3px;color:var(--purple);background:var(--bg);padding:0 8px;border-radius:0 0 4px 4px;border:1px solid rgba(139,92,246,.2);border-top:none;font-weight:800}
+.pf-banner-title{font-size:22px;font-weight:900;background:linear-gradient(135deg,#8b5cf6,#ec4899);-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:1px}
+.pf-banner-sub{font-size:10px;color:var(--muted);letter-spacing:2px;text-transform:uppercase;margin-top:4px}
+
+.pf-summary-row{display:flex;gap:10px;margin-bottom:18px;flex-wrap:wrap}
+.pf-summary-card{flex:1;min-width:100px;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:14px;text-align:center}
+.pf-summary-val{font-size:22px;font-weight:800;font-variant-numeric:tabular-nums}
+.pf-summary-lbl{font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-top:2px}
+
+/* Active Trades Strip */
+.pf-active-strip{margin-bottom:20px}
+.pf-active-header{display:flex;align-items:center;gap:8px;margin-bottom:10px;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--green)}
+.pf-active-header .dot{width:8px;height:8px;border-radius:50%;background:var(--green);animation:statusPulse 2s ease infinite}
+.pf-active-scroll{display:flex;gap:8px;overflow-x:auto;padding-bottom:8px;scroll-behavior:smooth}
+.pf-active-scroll::-webkit-scrollbar{height:4px}
+.pf-active-scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
+.pf-active-card{flex-shrink:0;min-width:200px;max-width:260px;background:var(--card);border:1px solid var(--border);border-radius:10px;padding:10px 14px;transition:all .2s;border-top:2px solid var(--border)}
+.pf-active-card:hover{border-color:rgba(255,255,255,.15);transform:translateY(-2px);box-shadow:0 8px 24px rgba(0,0,0,.3)}
+
+/* Model Leaderboard Table */
+.pf-leaderboard{margin-bottom:20px;border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--card);max-height:800px;overflow-y:auto}
+.pf-leaderboard::-webkit-scrollbar{width:4px}
+.pf-leaderboard::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
+.pf-lb-header{display:grid;grid-template-columns:32px 100px 90px 80px 70px 40px 40px 55px 80px 150px;gap:0;padding:10px 14px;background:rgba(255,255,255,.03);border-bottom:1px solid var(--border);font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--muted)}
+.pf-lb-header span[style*="cursor"]:hover{color:var(--cyan)}
+.pf-lb-row{display:grid;grid-template-columns:32px 100px 90px 80px 70px 40px 40px 55px 80px 150px;gap:0;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.03);font-size:11px;transition:all .2s;cursor:pointer;align-items:center}
+/* Chart Modal */
+.chart-modal{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:1000;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);animation:modalFadeIn .3s ease}
+.chart-modal-content{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:24px;width:92%;max-width:900px}
+@keyframes modalFadeIn{from{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}
+.chart-modal-header{display:flex;align-items:center;gap:10px;margin-bottom:16px}
+.chart-close{margin-left:auto;background:none;border:1px solid var(--border);color:var(--muted);border-radius:8px;padding:4px 12px;cursor:pointer;font-size:12px}
+.chart-close:hover{color:var(--text);border-color:var(--muted)}
+.chart-period-btns{display:flex;gap:4px;margin-left:auto}
+.chart-period,.chart-period-active{padding:3px 10px;border-radius:6px;font-size:10px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:transparent;color:var(--muted);transition:all .2s}
+.chart-period-active{border-color:var(--cyan);background:rgba(6,182,212,.1);color:var(--cyan)}
+.pf-lb-chart{display:flex;align-items:center;cursor:pointer;opacity:.85;transition:opacity .2s}
+.pf-lb-chart:hover{opacity:1}
+/* SWOT Trade Analysis */
+.swot-section{margin-bottom:20px}
+.swot-card{background:var(--card);border:1px solid var(--border);border-radius:14px;overflow:hidden;margin-bottom:12px;transition:all .2s}
+.swot-card:hover{border-color:rgba(255,255,255,.1)}
+.swot-card-header{display:flex;align-items:center;gap:10px;padding:14px 16px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.03)}
+.swot-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px 16px}
+.swot-quadrant{padding:10px;border-radius:10px;font-size:10px}
+.swot-quadrant-s{background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.15)}
+.swot-quadrant-w{background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15)}
+.swot-quadrant-o{background:rgba(59,130,246,.06);border:1px solid rgba(59,130,246,.15)}
+.swot-quadrant-t{background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.15)}
+.swot-q-title{font-weight:800;font-size:9px;text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
+.swot-q-item{padding:2px 0;color:var(--muted);line-height:1.4}
+.swot-q-item::before{content:'• '}
+.swot-models{display:flex;flex-wrap:wrap;gap:4px;padding:8px 16px 14px}
+.swot-model-tag{padding:2px 8px;border-radius:4px;font-size:9px;font-weight:600;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06)}
+.pf-lb-row:hover{background:rgba(255,255,255,.03)}
+.pf-lb-row:last-child{border-bottom:none}
+.pf-lb-rank{font-size:14px;font-weight:900;color:var(--muted)}
+.pf-lb-name{font-weight:700;display:flex;align-items:center;gap:6px;overflow:hidden}
+.pf-lb-pnl{font-weight:800;font-variant-numeric:tabular-nums}
+.pf-lb-invested{font-variant-numeric:tabular-nums;color:var(--muted)}
+.pf-lb-wr{font-weight:700;font-variant-numeric:tabular-nums}
+.pf-lb-wl{font-variant-numeric:tabular-nums;color:var(--muted)}
+.pf-lb-roi{font-weight:800;font-variant-numeric:tabular-nums;padding:2px 8px;border-radius:4px;text-align:center}
+.pf-lb-bar{height:4px;border-radius:2px;background:rgba(255,255,255,.05);overflow:hidden;margin-top:2px}
+.pf-lb-bar-fill{height:100%;border-radius:2px;transition:width .6s ease}
+
+/* Overlap Warning */
+.pf-overlap{margin-bottom:18px;background:rgba(245,158,11,.04);border:1px solid rgba(245,158,11,.15);border-radius:12px;padding:14px}
+.pf-overlap-title{font-size:11px;font-weight:700;color:var(--yellow);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;display:flex;align-items:center;gap:6px}
+
+/* Trade History */
+.pf-history{border:1px solid var(--border);border-radius:14px;overflow:hidden;background:var(--card)}
+.pf-history-header{padding:14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border)}
+.pf-history-list{max-height:400px;overflow-y:auto}
+.pf-history-list::-webkit-scrollbar{width:4px}
+.pf-history-list::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
+.pf-history-row{display:flex;align-items:center;gap:8px;padding:8px 14px;font-size:11px;border-bottom:1px solid rgba(255,255,255,.02);transition:all .15s}
+.pf-history-row:hover{background:rgba(255,255,255,.03)}
+.pf-history-row:last-child{border-bottom:none}
+
+@media(max-width:900px){
+.pf-lb-header,.pf-lb-row{grid-template-columns:28px 80px 70px 70px 50px 40px 40px 50px 70px;font-size:10px;padding:8px 10px}
+.pf-active-card{min-width:180px}
+}
+
+/* Footer */
+.footer{text-align:center;padding:30px;color:var(--muted);font-size:11px;letter-spacing:1px}
+.footer a{color:var(--pink);text-decoration:none}
+</style>
+</head>
+<body>
+<div id="root"></div>
+<script type="text/babel">
+const { useState, useEffect, useRef } = React;
+
+const api = (url) => fetch(url, { credentials: 'same-origin' }).then(r => r.json());
+
+function Particles() {
+  const particles = Array.from({ length: 30 }, (_, i) => ({
+    id: i,
+    left: Math.random() * 100 + '%',
+    delay: Math.random() * 10 + 's',
+    duration: 8 + Math.random() * 12 + 's',
+    color: ['#ff69b4','#8b5cf6','#3b82f6','#22c55e','#f59e0b','#06b6d4'][Math.floor(Math.random()*6)],
+    size: 1 + Math.random() * 2 + 'px',
+  }));
+  return (
+    <div className="particles">
+      {particles.map(p => (
+        <div key={p.id} className="particle" style={{
+          left: p.left, bottom: 0, animationDelay: p.delay, animationDuration: p.duration,
+          background: p.color, width: p.size, height: p.size, boxShadow: '0 0 6px ' + p.color,
+        }} />
+      ))}
+    </div>
+  );
+}
+
+function Clock() {
+  const [now, setNow] = useState(new Date());
+  useEffect(() => { const iv = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(iv); }, []);
+  const pt = now.toLocaleString('en-US', { timeZone: 'America/Los_Angeles', hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true });
+  const dateStr = now.toLocaleDateString('en-US', { timeZone: 'America/Los_Angeles', weekday: 'short', month: 'short', day: 'numeric' });
+  return <div className="clock"><div className="clock-time">{pt} PT</div><div className="clock-date">{dateStr}</div></div>;
+}
+
+function AgentCard({ agent, expanded, onToggle }) {
+  const bgColor = agent.color + '15';
+  return (
+    <div className={'agent-card' + (expanded ? ' expanded' : '')} onClick={onToggle}
+      style={{ '--agent-color': agent.color }}>
+      <div className="status-indicator" style={{ background: agent.status === 'active' ? 'var(--green)' : 'var(--yellow)', color: agent.status === 'active' ? 'var(--green)' : 'var(--yellow)' }} />
+      <div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 3, borderRadius: '16px 16px 0 0', background: agent.color, opacity: 0.7 }} />
+      <div className="agent-header">
+        <div className="agent-emoji" style={{ background: bgColor }}>{agent.emoji}</div>
+        <div>
+          <div className="agent-name" style={{ color: agent.color }}>{agent.name}</div>
+          <div className="agent-role">{agent.role}</div>
+        </div>
+      </div>
+      <div className="agent-desc">{agent.desc}</div>
+      <div className="agent-stats">
+        {Object.entries(agent.stats || {}).map(([k, v]) => (
+          <div key={k} className="agent-stat"><strong>{typeof v === 'number' ? v.toLocaleString() : v}</strong> {k.replace(/([A-Z])/g, ' $1').toLowerCase()}</div>
+        ))}
+      </div>
+      {expanded && (
+        <div className="skills-wrap">
+          {(agent.skills || []).map(s => (
+            <span key={s} className="skill-tag" style={{ background: agent.color + '20', color: agent.color, border: '1px solid ' + agent.color + '30' }}>{s}</span>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
+
+function SignalFeed({ signals }) {
+  if (!signals?.length) return <div style={{ textAlign: 'center', color: 'var(--muted)', padding: 30, fontSize: 13 }}>Waiting for signals...</div>;
+
+  function timeAgo(ts) {
+    const ms = Date.now() - new Date(ts).getTime();
+    if (ms < 60000) return Math.floor(ms/1000) + 's ago';
+    if (ms < 3600000) return Math.floor(ms/60000) + 'm ago';
+    return Math.floor(ms/3600000) + 'h ago';
+  }
+
+  return (
+    <div className="signal-list">
+      {signals.slice(0, 12).map((s, i) => (
+        <div key={i} className="signal-item">
+          <div className={'signal-dir ' + (s.direction === 'BUY_YES' ? 'buy-yes' : 'buy-no')}>
+            {s.direction === 'BUY_YES' ? 'BUY YES' : 'BUY NO'}
+          </div>
+          <div className="signal-market" title={s.market}>{s.market}</div>
+          <div className="signal-edge" style={{ color: Math.abs(s.edge) > 0.1 ? 'var(--green)' : 'var(--muted)' }}>
+            {(s.edge * 100).toFixed(1)}%
+          </div>
+          <div className="signal-conf">{(s.confidence * 100).toFixed(0)}%</div>
+          <div className="signal-approved" style={{ background: s.approved ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.1)', color: s.approved ? 'var(--green)' : 'var(--red)' }}>
+            {s.approved ? '✓' : '✗'}
+          </div>
+          <div className="signal-time">{timeAgo(s.time)}</div>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+// ── PORTFOLIOS TAB ──
+function PortfoliosTab() {
+  const [data, setData] = useState(null);
+  const [histFilter, setHistFilter] = useState('all');
+  const [chartData, setChartData] = useState({});
+  const [selectedChart, setSelectedChart] = useState(null);
+  const [compareMode, setCompareMode] = useState(false);
+  const [compareIds, setCompareIds] = useState([]);
+  const [sortBy, setSortBy] = useState('pnl');
+  const [sortDir, setSortDir] = useState('desc');
+  useEffect(() => {
+    const load = () => api('/api/portfauxlio').then(setData).catch(e => console.error(e));
+    load();
+    const iv = setInterval(load, 15000);
+    return () => clearInterval(iv);
+  }, []);
+  useEffect(() => {
+    const loadCharts = () => api('/api/portfauxlio/charts?period=7d').then(d => {
+      const byId = {};
+      (d.portfolios || []).forEach(p => { byId[p.id] = p; });
+      setChartData(byId);
+    }).catch(() => {});
+    loadCharts();
+    const cv = setInterval(loadCharts, 120000);
+    return () => clearInterval(cv);
+  }, []);
+  const [swotData, setSwotData] = useState(null);
+  const [swotExpanded, setSwotExpanded] = useState({});
+  useEffect(() => {
+    const loadSwot = () => api('/api/portfauxlio/swot').then(setSwotData).catch(() => {});
+    loadSwot();
+    const sv = setInterval(loadSwot, 60000);
+    return () => clearInterval(sv);
+  }, []);
+
+  const stratColors = {aggressive:'#ef4444',conservative:'#3b82f6',weather:'#06b6d4',momentum:'#f59e0b',contrarian:'#8b5cf6',ai_enhanced:'#ec4899',mc_validated:'#a855f7',scalper:'#eab308',patient:'#22c55e',random:'#64748b',penny_pincher:'#94a3b8',spray_pray:'#fb923c',nickel_slots:'#a78bfa',degen_lite:'#f43f5e',politics_junkie:'#0ea5e9',double_down:'#dc2626',volatility_rider:'#14b8a6',whale:'#1d4ed8',yolo:'#e11d48',full_send:'#b91c1c',kelly_criterion:'#059669',sharpe_sniper:'#7c3aed',bayesian_blend:'#0891b2',ensemble_lock:'#c026d3',mean_revert:'#d97706',info_ratio:'#2563eb',anti_fomo:'#16a34a',nightcrawler:'#4c1d95',heatseeker:'#ea580c',quant_core:'#0d9488',etf_presidential:'#dc143c',etf_cabinet:'#ff6347',etf_congress:'#4169e1',etf_scotus:'#2f4f4f',etf_fed:'#228b22',etf_economy:'#daa520',etf_geopolitics:'#8b0000',etf_territory:'#006400',etf_climate:'#00bfff',etf_energy:'#ff8c00',etf_tech:'#7b68ee',etf_space:'#191970',etf_crypto:'#ffd700',etf_sports:'#32cd32',etf_entertainment:'#ff1493',etf_legal:'#708090',etf_financials:'#4682b4',etf_govreform:'#b22222',etf_global_social:'#9370db',etf_broad_market:'#c0a050'};
+  const stratEmojis = {aggressive:'🔥',conservative:'🏛️',weather:'⛈️',momentum:'🚀',contrarian:'🔄',ai_enhanced:'🧠',mc_validated:'🎲',scalper:'⚡',patient:'🐢',random:'🎰',penny_pincher:'🪙',spray_pray:'💦',nickel_slots:'🎰',degen_lite:'😈',politics_junkie:'🏛️',double_down:'⬆️',volatility_rider:'🏄',whale:'🐳',yolo:'🧨',full_send:'💣',kelly_criterion:'🎰',sharpe_sniper:'🎯',bayesian_blend:'🧮',ensemble_lock:'🔐',mean_revert:'↩️',info_ratio:'📊',anti_fomo:'🧘',nightcrawler:'🦇',heatseeker:'🔬',quant_core:'∑',etf_presidential:'🏛️',etf_cabinet:'💼',etf_congress:'🗳️',etf_scotus:'⚖️',etf_fed:'🏦',etf_economy:'📈',etf_geopolitics:'🌍',etf_territory:'🗺️',etf_climate:'🌡️',etf_energy:'⛽',etf_tech:'🤖',etf_space:'🚀',etf_crypto:'₿',etf_sports:'⚽',etf_entertainment:'🎬',etf_legal:'🔨',etf_financials:'💹',etf_govreform:'✂️',etf_global_social:'⛪',etf_broad_market:'🌐'};
+  const stratNames = {aggressive:'Alpha',conservative:'Beta',weather:'Storm',momentum:'Momentum',contrarian:'Contrarian',ai_enhanced:'Nova',mc_validated:'Oracle',scalper:'Shark',patient:'Turtle',random:'Wildcard',penny_pincher:'Penny',spray_pray:'Spray',nickel_slots:'Nickel',degen_lite:'Degen',politics_junkie:'Capitol',double_down:'Doubler',volatility_rider:'Rider',whale:'Moby',yolo:'YOLO',full_send:'Cannon',kelly_criterion:'Kelly',sharpe_sniper:'Sniper',bayesian_blend:'Bayes',ensemble_lock:'Voltron',mean_revert:'Revert',info_ratio:'Signal',anti_fomo:'Zen',nightcrawler:'Shadow',heatseeker:'Heatseek',quant_core:'Quant',etf_presidential:'PRES',etf_cabinet:'Cabinet',etf_congress:'Congress',etf_scotus:'SCOTUS',etf_fed:'Fed',etf_economy:'Economy',etf_geopolitics:'Geo',etf_territory:'Territory',etf_climate:'Climate',etf_energy:'Energy',etf_tech:'Tech',etf_space:'Space',etf_crypto:'Crypto',etf_sports:'Sports',etf_entertainment:'Showbiz',etf_legal:'Legal',etf_financials:'Finance',etf_govreform:'GovReform',etf_global_social:'Global',etf_broad_market:'Broad Mkt'};
+  const stratDesc = {aggressive:'High edge (>15%), low confidence — speculative swings',conservative:'High confidence (>70%) — quality over quantity',weather:'Weather/climate markets only',momentum:'High agreement + articles — follows the trend',contrarian:'BUY_NO only — bets against the crowd',ai_enhanced:'Gemini AI enhanced signals only',mc_validated:'Monte Carlo validated, tight spread',scalper:'Small edges (3-10%) — quantity plays',patient:'Needs everything: conf>80%, MC, 5+ articles',random:'20% random — control group for luck testing',penny_pincher:'Any edge >=1% — micro bets, max volume',spray_pray:'Random 50% of all signals — luck vs skill',nickel_slots:'Edge >=2% + conf >=20% — low bar catcher',degen_lite:'Edge >=5%, ignores confidence — pure edge',politics_junkie:'Political markets only — elections/congress',double_down:'Edge >=20% — goes big on massive edges',volatility_rider:'High MC stddev + edge — volatile markets',whale:'Edge >=25% + conf >=60% — rare massive sniper',yolo:'Edge >=30% — max degen, no filters',full_send:'Takes EVERY signal — pipeline profitability test',kelly_criterion:'Kelly fraction: edge/(1+edge) — bankroll math',sharpe_sniper:'Signal-to-noise ratio >2.0 — edge/stddev',bayesian_blend:'Bayesian posterior: conf * evidence * edge',ensemble_lock:'ALL must align: AI + MC + conf + edge + articles',mean_revert:'High volatility + moderate edge — reversion play',info_ratio:'(articles * agreement) / noise — quality intel',anti_fomo:'Small edge + high conf — calm, certain plays',nightcrawler:'BUY_NO + AI + MC — sophisticated shorts',heatseeker:'Multi-factor composite: 40e + 30c + 20a + 10n',quant_core:'Geometric mean sqrt(edge*conf) — pure math',etf_presidential:'Presidential election & 2028 race markets',etf_cabinet:'Cabinet confirmations & departures',etf_congress:'House, Senate & legislative markets',etf_scotus:'Supreme Court rulings & retirements',etf_fed:'Federal Reserve, rates & monetary policy',etf_economy:'GDP, jobs & economic indicators',etf_geopolitics:'Wars, treaties, sanctions & foreign affairs',etf_territory:'Greenland, statehood & territorial expansion',etf_climate:'Weather, warming & climate events',etf_energy:'LNG, oil, power grid & energy commodities',etf_tech:'AI, fusion, tech companies & semiconductors',etf_space:'SpaceX, NASA, Mars & Moon missions',etf_crypto:'Bitcoin, Ethereum & digital assets',etf_sports:'NBA, NFL, MLB & sports outcomes',etf_entertainment:'Celebrity, media, awards & pop culture',etf_legal:'Pardons, impeachment & legal proceedings',etf_financials:'Bonds, yields & financial markets',etf_govreform:'DOGE, agency cuts & government reform',etf_global_social:'Pope, referendums & global social movements',etf_broad_market:'Index fund — high-quality signals all sectors'};
+
+  if (!data) return <div style={{textAlign:'center',padding:60,color:'var(--muted)'}}>Loading portFAUXlio data...</div>;
+
+  const { portfolios: rawPortfolios, activeTrades, tradeHistory, overlaps, summary } = data;
+  const maxInvested = Math.max(...rawPortfolios.map(p => parseInt(p.total_invested_cents) || 0), 1);
+  const filteredHistory = histFilter === 'all' ? tradeHistory : tradeHistory.filter(t => t.strategy === histFilter);
+
+  const sortFns = {
+    pnl: p => parseInt(p.pnl_cents) || 0,
+    roi: p => parseFloat(p.roi_pct) || 0,
+    invested: p => parseInt(p.total_invested_cents) || 0,
+    trades: p => (parseInt(p.trades_won)||0) + (parseInt(p.trades_lost)||0) + (parseInt(p.trades_expired)||0),
+    wins: p => parseInt(p.trades_won) || 0,
+    name: p => (p.name || '').toLowerCase(),
+    strategy: p => (p.strategy || '').toLowerCase(),
+  };
+  const getSortVal = sortFns[sortBy] || sortFns.pnl;
+  const portfolios = [...rawPortfolios].sort((a, b) => {
+    const av = getSortVal(a), bv = getSortVal(b);
+    if (typeof av === 'string') return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av);
+    return sortDir === 'desc' ? bv - av : av - bv;
+  });
+  const toggleSort = (col) => { if (sortBy === col) setSortDir(d => d === 'desc' ? 'asc' : 'desc'); else { setSortBy(col); setSortDir('desc'); } };
+
+  return (<div>
+    {/* ═══ Banner ═══ */}
+    <div className="pf-banner">
+      <div className="pf-banner-title">portFAUXlio</div>
+      <div className="pf-banner-sub">50 Strategy Models &bull; Paper Trading &bull; Real Signals &bull; Zero Real Money</div>
+    </div>
+
+    {/* ═══ Summary Stats ═══ */}
+    <div className="pf-summary-row">
+      <div className="pf-summary-card">
+        <div className="pf-summary-val" style={{color: summary.totalPnl >= 0 ? 'var(--green)' : 'var(--red)'}}>
+          {summary.totalPnl >= 0 ? '+' : ''}{'$'}{(summary.totalPnl / 100).toFixed(2)}
+        </div>
+        <div className="pf-summary-lbl">Total P&L</div>
+      </div>
+      <div className="pf-summary-card">
+        <div className="pf-summary-val" style={{color:'var(--blue)'}}>{'$'}{(summary.totalInvested / 100).toFixed(2)}</div>
+        <div className="pf-summary-lbl">Invested</div>
+      </div>
+      <div className="pf-summary-card">
+        <div className="pf-summary-val" style={{color:'var(--purple)'}}>{summary.totalTrades}</div>
+        <div className="pf-summary-lbl">Total Trades</div>
+      </div>
+      <div className="pf-summary-card">
+        <div className="pf-summary-val" style={{color:'var(--green)'}}>{summary.openTrades}</div>
+        <div className="pf-summary-lbl">Open Positions</div>
+      </div>
+      <div className="pf-summary-card">
+        <div className="pf-summary-val" style={{color:'var(--cyan)'}}>{summary.uniqueMarkets}</div>
+        <div className="pf-summary-lbl">Markets</div>
+      </div>
+      <div className="pf-summary-card">
+        <div className="pf-summary-val" style={{color:'var(--yellow)'}}>{summary.activeModels}/{portfolios ? portfolios.length : 50}</div>
+        <div className="pf-summary-lbl">Active Models</div>
+      </div>
+    </div>
+
+    {/* ═══ Active Trades Strip ═══ */}
+    {activeTrades.length > 0 && <div className="pf-active-strip">
+      <div className="pf-active-header">
+        <span className="dot" />
+        {activeTrades.length} Open Positions
+      </div>
+      <div className="pf-active-scroll">
+        {activeTrades.map((t, i) => {
+          const c = stratColors[t.strategy] || 'var(--muted)';
+          return (<div key={i} className="pf-active-card" style={{borderTopColor: c}}>
+            <div style={{display:'flex',alignItems:'center',gap:6,marginBottom:4}}>
+              <span style={{fontSize:14}}>{stratEmojis[t.strategy] || ''}</span>
+              <span style={{fontSize:11,fontWeight:700,color:c}}>{t.portfolio_name}</span>
+              <span style={{marginLeft:'auto',padding:'1px 6px',borderRadius:4,fontSize:9,fontWeight:700,
+                background: t.direction === 'BUY_YES' ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)',
+                color: t.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
+              }}>{t.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
+            </div>
+            <div style={{fontSize:10,fontWeight:600,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',marginBottom:3}} title={t.market_title || t.market_id}>
+              {t.market_title || t.market_id}
+            </div>
+            <div style={{display:'flex',justifyContent:'space-between',fontSize:10,color:'var(--muted)'}}>
+              <span>{t.contracts}x @ {t.entry_price_cents}¢</span>
+              <span>{'$'}{(t.cost_cents / 100).toFixed(2)}</span>
+              <span>{timeAgo(t.created_at)}</span>
+            </div>
+          </div>);
+        })}
+      </div>
+    </div>}
+
+    {/* ═══ Model Leaderboard ═══ */}
+    <div style={{marginBottom:18}}>
+      <div style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,marginBottom:10,color:'var(--pink)',display:'flex',alignItems:'center',gap:8}}>
+        Model Leaderboard
+        <span style={{fontSize:9,fontWeight:400,color:'var(--muted)',textTransform:'none',letterSpacing:0}}>ranked by {sortBy}</span>
+        <div style={{marginLeft:'auto',display:'flex',gap:6}}>
+          {compareMode && compareIds.length >= 2 && <button onClick={() => setCompareMode('show')} style={{padding:'3px 10px',borderRadius:6,fontSize:9,fontWeight:700,background:'rgba(6,182,212,.15)',border:'1px solid var(--cyan)',color:'var(--cyan)',cursor:'pointer'}}>
+            Compare {compareIds.length} →
+          </button>}
+          <button onClick={() => { if (compareMode) { setCompareMode(false); setCompareIds([]); } else setCompareMode(true); }} style={{padding:'3px 10px',borderRadius:6,fontSize:9,fontWeight:700,background:compareMode?'rgba(168,85,247,.15)':'transparent',border:'1px solid '+(compareMode?'var(--purple)':'var(--border)'),color:compareMode?'var(--purple)':'var(--muted)',cursor:'pointer'}}>
+            {compareMode ? '✕ Cancel' : '📊 Compare'}
+          </button>
+        </div>
+      </div>
+      <div className="pf-leaderboard">
+        <div className="pf-lb-header">
+          <span>#</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('name')}>Model {sortBy==='name' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('pnl')}>P&L {sortBy==='pnl' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('invested')}>Invested {sortBy==='invested' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('roi')}>ROI {sortBy==='roi' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('wins')}>W {sortBy==='wins' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
+          <span>L</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('trades')}>Trades {sortBy==='trades' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
+          <span style={{cursor:'pointer'}} onClick={() => toggleSort('strategy')}>Strategy {sortBy==='strategy' ? (sortDir==='asc'?'▲':'▼') : ''}</span>
+          <span>Chart</span>
+        </div>
+        {portfolios.map((p, i) => {
+          const c = stratColors[p.strategy] || 'var(--muted)';
+          const pnl = parseInt(p.pnl_cents) || 0;
+          const invested = parseInt(p.total_invested_cents) || 0;
+          const resolved = parseInt(p.resolved_trades) || 0;
+          const roi = parseFloat(p.roi_pct) || 0;
+          const totalT = (parseInt(p.trades_won) || 0) + (parseInt(p.trades_lost) || 0) + (parseInt(p.trades_expired) || 0);
+          const openCount = activeTrades.filter(t => t.portfolio_id === p.id).length;
+          const medals = ['🥇','🥈','🥉'];
+          const cd = chartData[p.id];
+          const miniPts = cd ? (cd.snapshots || []).map(s => s.nav / 100) : (cd?.daily || []).map(d => d.cumulative / 100);
+          return (<div key={p.id} className="pf-lb-row" style={compareMode && compareIds.includes(p.id) ? {background:'rgba(168,85,247,.08)',borderLeft:'3px solid var(--purple)'} : {}} onClick={() => {
+            if (compareMode) { setCompareIds(prev => prev.includes(p.id) ? prev.filter(x => x !== p.id) : [...prev, p.id]); }
+            else if (cd) setSelectedChart({id:p.id,strategy:p.strategy,name:p.name,color:c});
+          }}>
+            <span className="pf-lb-rank">{i < 3 ? medals[i] : (i+1)}</span>
+            <span className="pf-lb-name" style={{color:c}}>
+              <span style={{fontSize:16}}>{stratEmojis[p.strategy]||''}</span>
+              {p.name}
+            </span>
+            <span className="pf-lb-pnl" style={{color: pnl >= 0 ? 'var(--green)' : 'var(--red)'}}>
+              {pnl >= 0 ? '+' : ''}{'$'}{(pnl / 100).toFixed(2)}
+            </span>
+            <span className="pf-lb-invested">{'$'}{(invested / 100).toFixed(2)}</span>
+            <span className="pf-lb-roi" style={{
+              background: roi > 0 ? 'rgba(34,197,94,.12)' : roi < 0 ? 'rgba(239,68,68,.12)' : 'rgba(255,255,255,.04)',
+              color: roi > 0 ? 'var(--green)' : roi < 0 ? 'var(--red)' : 'var(--muted)'
+            }}>{roi > 0 ? '+' : ''}{roi}%</span>
+            <span className="pf-lb-wl" style={{color:'var(--green)'}}>{p.trades_won}</span>
+            <span className="pf-lb-wl" style={{color:'var(--red)'}}>{p.trades_lost}</span>
+            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>
+              {openCount > 0 ? <span>{openCount} <span style={{fontSize:8,color:'var(--green)'}}>OPEN</span></span> : <span style={{color:'var(--muted)'}}>0</span>}
+              {resolved > 0 && <span style={{marginLeft:4,fontSize:9}}>+{resolved}</span>}
+            </span>
+            <span style={{fontSize:9,color:'var(--muted)',overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={stratDesc[p.strategy]}>
+              {p.strategy}
+            </span>
+            <span style={{display:'flex',alignItems:'center',justifyContent:'center'}}>
+              {miniPts && miniPts.length >= 2 ? <MiniChart data={miniPts} color={c} /> : <span style={{fontSize:9,color:'var(--muted)'}}>--</span>}
+            </span>
+          </div>);
+        })}
+      </div>
+    </div>
+
+    {/* ═══ Overlap Analysis ═══ */}
+    {overlaps.length > 0 && <div className="pf-overlap">
+      <div className="pf-overlap-title">
+        ⚠️ Strategy Overlap — Markets with 2+ models
+      </div>
+      <div style={{display:'flex',flexDirection:'column',gap:4}}>
+        {overlaps.slice(0, 8).map((o, i) => (
+          <div key={i} style={{display:'flex',alignItems:'center',gap:8,fontSize:10,padding:'4px 0'}}>
+            <span style={{fontWeight:700,color:'var(--yellow)',minWidth:20}}>{o.num_strategies}x</span>
+            <span style={{fontWeight:600,minWidth:180,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>{o.market_id}</span>
+            <span style={{color:'var(--muted)',flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}}>
+              {(o.portfolios || []).join(', ')}
+            </span>
+            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>{'$'}{((o.total_exposure || 0) / 100).toFixed(2)}</span>
+          </div>
+        ))}
+      </div>
+    </div>}
+
+    {/* ═══ SWOT Trade Intelligence ═══ */}
+    {swotData && swotData.markets && swotData.markets.length > 0 && <div className="swot-section">
+      <div style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,marginBottom:12,color:'var(--purple)',display:'flex',alignItems:'center',gap:8}}>
+        Trade Intelligence & SWOT
+        <span style={{fontSize:9,fontWeight:400,color:'var(--muted)',textTransform:'none',letterSpacing:0}}>{swotData.markets.length} active markets &bull; {swotData.total_trades} trades this week</span>
+      </div>
+      {swotData.markets.map((m, idx) => {
+        const isOpen = swotExpanded[m.market_id];
+        return (<div key={m.market_id} className="swot-card">
+          <div className="swot-card-header" onClick={() => setSwotExpanded(prev => ({...prev, [m.market_id]: !prev[m.market_id]}))}>
+            <span style={{fontSize:16,fontWeight:900,color:'var(--muted)',minWidth:24}}>{idx+1}</span>
+            <span style={{padding:'2px 8px',borderRadius:4,fontSize:9,fontWeight:800,
+              background: m.direction === 'BUY_YES' ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)',
+              color: m.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
+            }}>{m.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
+            <span style={{flex:1,fontWeight:700,fontSize:11,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={m.title}>{m.title}</span>
+            <span style={{fontSize:10,color:'var(--cyan)',fontWeight:700}}>{m.model_count} models</span>
+            <span style={{fontSize:10,fontWeight:700,color:m.edge >= 10 ? 'var(--green)' : 'var(--yellow)'}}>{m.edge.toFixed(1)}% edge</span>
+            <span style={{fontSize:10,fontVariantNumeric:'tabular-nums',color:'var(--muted)'}}>{'$'}{(m.total_cost / 100).toFixed(2)}</span>
+            <span style={{fontSize:10,fontWeight:700,color: m.open_count > 0 ? 'var(--green)' : 'var(--muted)'}}>{m.open_count > 0 ? m.open_count + ' OPEN' : 'CLOSED'}</span>
+            <span style={{fontSize:14,color:'var(--muted)',transform:isOpen?'rotate(180deg)':'rotate(0)',transition:'transform .2s'}}>▼</span>
+          </div>
+          {isOpen && <div>
+            <div className="swot-grid">
+              <div className="swot-quadrant swot-quadrant-s">
+                <div className="swot-q-title" style={{color:'var(--green)'}}>Strengths</div>
+                {m.swot.strengths.map((s, i) => <div key={i} className="swot-q-item">{s}</div>)}
+              </div>
+              <div className="swot-quadrant swot-quadrant-w">
+                <div className="swot-q-title" style={{color:'var(--red)'}}>Weaknesses</div>
+                {m.swot.weaknesses.map((w, i) => <div key={i} className="swot-q-item">{w}</div>)}
+              </div>
+              <div className="swot-quadrant swot-quadrant-o">
+                <div className="swot-q-title" style={{color:'var(--blue)'}}>Opportunities</div>
+                {m.swot.opportunities.map((o, i) => <div key={i} className="swot-q-item">{o}</div>)}
+              </div>
+              <div className="swot-quadrant swot-quadrant-t">
+                <div className="swot-q-title" style={{color:'var(--yellow)'}}>Threats</div>
+                {m.swot.threats.map((t, i) => <div key={i} className="swot-q-item">{t}</div>)}
+              </div>
+            </div>
+            <div style={{padding:'8px 16px',fontSize:10,color:'var(--muted)',lineHeight:1.5,borderTop:'1px solid rgba(255,255,255,.03)'}}>
+              <strong style={{color:'var(--text)'}}>Signal:</strong> {m.reasoning}
+            </div>
+            <div style={{padding:'4px 16px 6px',fontSize:10,color:'var(--muted)',display:'flex',gap:16,flexWrap:'wrap'}}>
+              <span>Conf: <strong style={{color:'var(--cyan)'}}>{Math.round(m.confidence * 100)}%</strong></span>
+              <span>Entry: <strong>{m.entry_price}¢</strong></span>
+              {m.mc_mean > 0 && <span>MC: <strong>{m.mc_mean.toFixed(1)}% ±{m.mc_std.toFixed(1)}%</strong></span>}
+              {m.articles > 0 && <span>News: <strong>{m.articles} articles</strong></span>}
+              {m.agreement > 0 && <span>Agree: <strong>{m.agreement}%</strong></span>}
+            </div>
+            <div className="swot-models">
+              {m.trades.map((t, i) => (
+                <span key={i} className="swot-model-tag" style={{color:stratColors[t.strategy]||'var(--muted)',borderColor:(stratColors[t.strategy]||'var(--border)')+'40'}}>
+                  {stratEmojis[t.strategy]||''} {t.portfolio} &bull; {t.contracts}x &bull; {t.status === 'open' ? 'OPEN' : (t.pnl >= 0 ? '+' : '') + '$' + (t.pnl / 100).toFixed(2)}
+                </span>
+              ))}
+            </div>
+          </div>}
+        </div>);
+      })}
+    </div>}
+
+    {/* ═══ Trade History ═══ */}
+    <div className="pf-history">
+      <div className="pf-history-header">
+        <span style={{fontSize:13,fontWeight:700,textTransform:'uppercase',letterSpacing:.5,color:'var(--cyan)'}}>Trade History</span>
+        <div style={{display:'flex',gap:4}}>
+          <button onClick={() => setHistFilter('all')} style={{padding:'3px 10px',borderRadius:6,border:'1px solid '+(histFilter==='all'?'var(--cyan)':'var(--border)'),background:histFilter==='all'?'rgba(6,182,212,.1)':'transparent',color:histFilter==='all'?'var(--cyan)':'var(--muted)',fontSize:10,fontWeight:600,cursor:'pointer'}}>All</button>
+          {portfolios.filter(p => parseInt(p.total_invested_cents) > 0).map(p => (
+            <button key={p.strategy} onClick={() => setHistFilter(p.strategy)} style={{padding:'3px 10px',borderRadius:6,border:'1px solid '+(histFilter===p.strategy?stratColors[p.strategy]:'var(--border)'),background:histFilter===p.strategy?stratColors[p.strategy]+'18':'transparent',color:histFilter===p.strategy?stratColors[p.strategy]:'var(--muted)',fontSize:10,fontWeight:600,cursor:'pointer'}}>
+              {stratEmojis[p.strategy] || ''} {p.name}
+            </button>
+          ))}
+        </div>
+      </div>
+      <div className="pf-history-list">
+        {filteredHistory.length === 0 && <div style={{textAlign:'center',padding:30,color:'var(--muted)',fontSize:12}}>No trades for this filter</div>}
+        {filteredHistory.slice(0, 100).map((t, i) => {
+          const c = stratColors[t.strategy] || 'var(--muted)';
+          const pnl = parseInt(t.pnl_cents) || 0;
+          return (<div key={i} className="pf-history-row">
+            <span style={{width:6,height:6,borderRadius:'50%',flexShrink:0,background:
+              t.status === 'open' ? 'var(--green)' : t.status === 'won' ? 'var(--blue)' : t.status === 'lost' ? 'var(--red)' : 'var(--muted)'
+            }} />
+            <span style={{minWidth:60,fontWeight:700,color:c,fontSize:10}}>{t.portfolio_name}</span>
+            <span style={{padding:'1px 6px',borderRadius:3,fontSize:9,fontWeight:700,
+              background: t.direction === 'BUY_YES' ? 'rgba(34,197,94,.12)' : 'rgba(239,68,68,.12)',
+              color: t.direction === 'BUY_YES' ? 'var(--green)' : 'var(--red)'
+            }}>{t.direction === 'BUY_YES' ? 'YES' : 'NO'}</span>
+            <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',color:'var(--text)'}} title={t.market_title || t.market_id}>
+              {t.market_title || t.market_id}
+            </span>
+            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:10}}>
+              {t.contracts}x @ {t.entry_price_cents}¢
+            </span>
+            <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:10,minWidth:45,textAlign:'right'}}>
+              {'$'}{(t.cost_cents / 100).toFixed(2)}
+            </span>
+            <span style={{fontWeight:800,fontVariantNumeric:'tabular-nums',minWidth:55,textAlign:'right',fontSize:10,
+              color: t.status === 'open' ? 'var(--yellow)' : pnl >= 0 ? 'var(--green)' : 'var(--red)'
+            }}>
+              {t.status === 'open' ? 'OPEN' : (pnl >= 0 ? '+' : '') + (pnl / 100).toFixed(2)}
+            </span>
+            <span style={{fontSize:9,color:'var(--muted)',minWidth:55,textAlign:'right'}}>{timeAgo(t.created_at)}</span>
+          </div>);
+        })}
+      </div>
+    </div>
+
+    {/* ═══ FullChart Modal ═══ */}
+    {selectedChart && chartData[selectedChart.id] && <FullChart
+      chartData={chartData[selectedChart.id]}
+      portfolioId={selectedChart.id}
+      strategy={selectedChart.strategy}
+      name={selectedChart.name}
+      color={selectedChart.color || stratColors[selectedChart.strategy] || '#8b5cf6'}
+      emoji={stratEmojis[selectedChart.strategy] || ''}
+      desc={stratDesc[selectedChart.strategy] || ''}
+      onClose={() => setSelectedChart(null)}
+    />}
+
+    {/* ═══ Comparison Chart Modal ═══ */}
+    {compareMode === 'show' && compareIds.length >= 2 && <ComparisonChart
+      portfolioIds={compareIds}
+      allChartData={chartData}
+      stratColors={stratColors}
+      stratNames={stratNames}
+      stratEmojis={stratEmojis}
+      onClose={() => { setCompareMode(false); setCompareIds([]); }}
+    />}
+
+    {/* ═══ Strategy Guide ═══ */}
+    <div style={{marginTop:18,padding:16,background:'var(--card)',border:'1px solid var(--border)',borderRadius:14}}>
+      <div style={{fontSize:11,fontWeight:700,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.5,marginBottom:10}}>Strategy Guide</div>
+      <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(280px,1fr))',gap:6}}>
+        {Object.entries(stratDesc).map(([strat, desc]) => (
+          <div key={strat} style={{display:'flex',alignItems:'flex-start',gap:8,fontSize:10,padding:'4px 0',color:'var(--muted)'}}>
+            <span style={{fontSize:14,flexShrink:0}}>{stratEmojis[strat]||''}</span>
+            <div>
+              <span style={{fontWeight:700,color:stratColors[strat]}}>{stratNames[strat]}</span>
+              <span style={{margin:'0 4px'}}>—</span>
+              {desc}
+            </div>
+          </div>
+        ))}
+      </div>
+    </div>
+  </div>);
+}
+
+// ── PREDICTIONS TAB ──
+function PredictionsTab() {
+  const [data, setData] = useState(null);
+  useEffect(() => {
+    api('/api/predictions-history').then(setData).catch(()=>{});
+    const iv = setInterval(() => api('/api/predictions-history').then(setData).catch(()=>{}), 30000);
+    return () => clearInterval(iv);
+  }, []);
+  if (!data) return <div style={{textAlign:'center',padding:40,color:'var(--muted)'}}>Loading predictions...</div>;
+
+  const statsMap = {};
+  (data.stats||[]).forEach(s => { statsMap[s.outcome] = s; });
+
+  return (<div>
+    <div style={{display:'flex',gap:12,marginBottom:20,flexWrap:'wrap'}}>
+      {['win','loss','expired'].map(o => {
+        const s = statsMap[o]||{cnt:0,avg_pnl:0,avg_conf:0};
+        const c = o==='win'?'var(--green)':o==='loss'?'var(--red)':'var(--muted)';
+        return (<div key={o} style={{background:'var(--card)',border:'1px solid var(--border)',borderRadius:12,padding:16,flex:1,minWidth:140,textAlign:'center'}}>
+          <div style={{fontSize:28,fontWeight:700,color:c}}>{s.cnt}</div>
+          <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:1,marginTop:2}}>{o==='win'?'Wins':o==='loss'?'Losses':'Expired'}</div>
+          <div style={{fontSize:11,color:'var(--muted)',marginTop:4}}>Avg P&L: <strong style={{color:c}}>{parseFloat(s.avg_pnl||0)>0?'+':''}{s.avg_pnl||0}¢</strong></div>
+          <div style={{fontSize:11,color:'var(--muted)'}}>Avg Conf: {s.avg_conf||0}%</div>
+        </div>);
+      })}
+      <div style={{background:'var(--card)',border:'1px solid var(--border)',borderRadius:12,padding:16,flex:1,minWidth:140,textAlign:'center'}}>
+        <div style={{fontSize:28,fontWeight:700,color:'var(--blue)'}}>{statsMap.win?Math.round(parseInt(statsMap.win.cnt)/(parseInt(statsMap.win.cnt)+parseInt(statsMap.loss?.cnt||0))*100):0}%</div>
+        <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:1,marginTop:2}}>Win Rate</div>
+        <div style={{fontSize:11,color:'var(--muted)',marginTop:4}}>W/L only (excl. expired)</div>
+      </div>
+    </div>
+
+    <h3 style={{fontSize:14,fontWeight:600,marginBottom:10,display:'flex',alignItems:'center',gap:8}}>Resolved Predictions <span style={{fontSize:11,color:'var(--muted)',fontWeight:400}}>— what actually happened</span></h3>
+    <div style={{display:'flex',flexDirection:'column',gap:4}}>
+      {(data.predictions||[]).slice(0,30).map((p,i) => {
+        const oc = p.outcome==='win'?'var(--green)':p.outcome==='loss'?'var(--red)':'var(--muted)';
+        return (<div key={i} style={{display:'flex',alignItems:'center',gap:10,padding:'8px 14px',background:'var(--card)',border:'1px solid var(--border)',borderRadius:8,fontSize:12}}>
+          <span style={{padding:'2px 8px',borderRadius:4,fontWeight:700,fontSize:10,background:p.outcome==='win'?'rgba(34,197,94,.15)':p.outcome==='loss'?'rgba(239,68,68,.15)':'rgba(255,255,255,.05)',color:oc,minWidth:48,textAlign:'center',textTransform:'uppercase'}}>{p.outcome}</span>
+          <span style={{padding:'2px 8px',borderRadius:4,fontSize:10,fontWeight:600,background:p.side==='YES'||p.side==='yes'?'rgba(34,197,94,.1)':'rgba(239,68,68,.1)',color:p.side==='YES'||p.side==='yes'?'var(--green)':'var(--red)'}}>{p.side}</span>
+          <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap'}} title={p.title}>{p.title||p.ticker}</span>
+          <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:11}}>Conf:{p.confidence}%</span>
+          <span style={{fontVariantNumeric:'tabular-nums',color:'var(--muted)',fontSize:11}}>{p.entry_price}¢→{p.exit_price||'?'}¢</span>
+          <span style={{fontWeight:700,fontVariantNumeric:'tabular-nums',color:oc,minWidth:45,textAlign:'right'}}>{p.pnl_cents>0?'+':''}{p.pnl_cents||0}¢</span>
+          <span style={{fontSize:10,color:'var(--muted)',minWidth:70,textAlign:'right'}}>{p.resolved_at?new Date(p.resolved_at).toLocaleDateString('en-US',{month:'short',day:'numeric'}):''}</span>
+        </div>);
+      })}
+    </div>
+  </div>);
+}
+
+// ── Sparkline SVG Component ──
+function Sparkline({ data, width = 120, height = 30, color = '#8b5cf6', showDots = false, showArea = true, label }) {
+  if (!data || data.length < 2) return null;
+  const max = Math.max(...data);
+  const min = Math.min(...data);
+  const range = max - min || 1;
+  const gradId = 'grad-' + color.replace('#','') + '-' + width;
+  const points = data.map((v, i) => {
+    const x = (i / (data.length - 1)) * width;
+    const y = height - ((v - min) / range) * (height - 6) - 3;
+    return x + ',' + y;
+  }).join(' ');
+  const areaPoints = points + ' ' + width + ',' + height + ' 0,' + height;
+  const lastVal = data[data.length - 1];
+  const prevVal = data[data.length - 2];
+  const trending = lastVal >= prevVal;
+  return (
+    <div style={{position:'relative'}}>
+      <svg className="sparkline-svg" width={width} height={height} viewBox={'0 0 ' + width + ' ' + height}>
+        <defs><linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
+          <stop offset="0%" stopColor={color} stopOpacity="0.25"/><stop offset="100%" stopColor={color} stopOpacity="0.01"/>
+        </linearGradient></defs>
+        {showArea && <polygon points={areaPoints} fill={'url(#' + gradId + ')'} />}
+        <polyline points={points} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
+        {showDots && data.map((v, i) => {
+          const x = (i / (data.length - 1)) * width;
+          const y = height - ((v - min) / range) * (height - 6) - 3;
+          return <circle key={i} cx={x} cy={y} r={i === data.length - 1 ? 3 : 1.5} fill={i === data.length - 1 ? color : 'transparent'} stroke={color} strokeWidth={i === data.length - 1 ? 0 : 0.5} opacity={i === data.length - 1 ? 1 : 0.5} />;
+        })}
+      </svg>
+      {label && <div style={{position:'absolute',top:2,right:4,fontSize:9,color:trending?'var(--green)':'var(--red)',fontWeight:700}}>
+        {trending?'▲':'▼'} {lastVal.toLocaleString()}
+      </div>}
+    </div>
+  );
+}
+
+// ── MiniChart: Canvas-based sparkline for leaderboard rows ──
+function MiniChart({ data, color, width = 140, height = 36 }) {
+  const canvasRef = React.useRef(null);
+  const chartRef = React.useRef(null);
+  React.useEffect(() => {
+    if (!data || data.length < 2 || !canvasRef.current) return;
+    if (chartRef.current) chartRef.current.destroy();
+    const ctx = canvasRef.current.getContext('2d');
+    const gradient = ctx.createLinearGradient(0, 0, 0, height);
+    gradient.addColorStop(0, color + '40');
+    gradient.addColorStop(1, color + '05');
+    chartRef.current = new Chart(ctx, {
+      type: 'line',
+      data: { labels: data.map((_, i) => i), datasets: [{ data, borderColor: color, borderWidth: 1.5, backgroundColor: gradient, fill: true, pointRadius: 0, tension: 0.4 }] },
+      options: { responsive: false, animation: { duration: 800, easing: 'easeOutQuart' }, plugins: { legend: { display: false }, tooltip: { enabled: false } }, scales: { x: { display: false }, y: { display: false } } }
+    });
+    return () => { if (chartRef.current) chartRef.current.destroy(); };
+  }, [data, color]);
+  return <canvas ref={canvasRef} width={width} height={height} style={{borderRadius:4}} />;
+}
+
+// ── FullChart: Modal chart for a single portfolio with period selector ──
+function FullChart({ chartData: initialData, portfolioId, strategy, name, color, emoji, desc, onClose }) {
+  const canvasRef = React.useRef(null);
+  const chartRef = React.useRef(null);
+  const clr = color || '#8b5cf6';
+  const [period, setPeriod] = React.useState('7d');
+  const [liveData, setLiveData] = React.useState(initialData);
+  const periods = ['1d','3d','7d','14d','30d'];
+
+  React.useEffect(() => {
+    if (period === '7d') { setLiveData(initialData); return; }
+    api('/api/portfauxlio/charts?period=' + period).then(d => {
+      const match = (d.portfolios || []).find(p => p.id === portfolioId);
+      if (match) setLiveData(match);
+    }).catch(() => {});
+  }, [period, portfolioId]);
+
+  React.useEffect(() => {
+    const cd = liveData || initialData;
+    const snaps = cd?.snapshots || [];
+    const daily = cd?.daily || [];
+    const dataPoints = snaps.length > 1 ? snaps.map(s => ({ x: new Date(s.time), y: s.nav / 100 }))
+      : daily.length > 1 ? daily.map(d => ({ x: new Date(d.date), y: d.cumulative / 100 })) : [];
+    if (!dataPoints.length || !canvasRef.current) return;
+    if (chartRef.current) chartRef.current.destroy();
+    const ctx = canvasRef.current.getContext('2d');
+    const gradient = ctx.createLinearGradient(0, 0, 0, 300);
+    gradient.addColorStop(0, clr + '30');
+    gradient.addColorStop(1, clr + '02');
+    const zeroLine = { id: 'zeroLine', beforeDraw(chart) {
+      const yScale = chart.scales.y;
+      if (!yScale) return;
+      const y0 = yScale.getPixelForValue(0);
+      if (y0 < yScale.top || y0 > yScale.bottom) return;
+      const cctx = chart.ctx;
+      cctx.save();
+      cctx.strokeStyle = 'rgba(255,255,255,0.15)';
+      cctx.lineWidth = 1;
+      cctx.setLineDash([4, 4]);
+      cctx.beginPath();
+      cctx.moveTo(chart.chartArea.left, y0);
+      cctx.lineTo(chart.chartArea.right, y0);
+      cctx.stroke();
+      cctx.restore();
+    }};
+    chartRef.current = new Chart(ctx, {
+      type: 'line',
+      data: { datasets: [{ label: 'NAV', data: dataPoints, borderColor: clr, borderWidth: 2, backgroundColor: gradient, fill: true, pointRadius: dataPoints.length > 50 ? 0 : 2, pointHoverRadius: 6, pointBackgroundColor: clr, tension: 0.3 }] },
+      plugins: [zeroLine],
+      options: {
+        responsive: true, maintainAspectRatio: false,
+        animation: { duration: 1200, easing: 'easeInOutQuart' },
+        interaction: { intersect: false, mode: 'index' },
+        plugins: {
+          legend: { display: false },
+          tooltip: { backgroundColor: 'rgba(15,15,35,0.95)', borderColor: clr + '60', borderWidth: 1, titleColor: '#e2e8f0', bodyColor: '#e2e8f0', padding: 12, displayColors: false,
+            callbacks: { label: (tipCtx) => 'NAV: ' + (tipCtx.parsed.y >= 0 ? '+' : '') + '$' + tipCtx.parsed.y.toFixed(2) }
+          }
+        },
+        scales: {
+          x: { type: 'time', time: { unit: snaps.length > 1 ? 'hour' : 'day' }, ticks: { color: '#64748b', maxRotation: 45, font: { size: 9 } }, grid: { color: 'rgba(255,255,255,.03)' } },
+          y: { ticks: { color: '#64748b', callback: (v) => '$' + v.toFixed(0), font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }
+        }
+      }
+    });
+    return () => { if (chartRef.current) chartRef.current.destroy(); };
+  }, [liveData, strategy, clr]);
+  return (
+    <div className="chart-modal" onClick={onClose}>
+      <div className="chart-modal-content" onClick={e => e.stopPropagation()}>
+        <div className="chart-modal-header">
+          <span style={{fontSize:18}}>{emoji||''}</span>
+          <span style={{fontWeight:800,color:clr}}>{name || strategy}</span>
+          <span style={{fontSize:10,color:'var(--muted)',flex:1}}>{desc||''}</span>
+          <div className="chart-period-btns">
+            {periods.map(p => (
+              <button key={p} className={period === p ? 'chart-period-active' : 'chart-period'} onClick={() => setPeriod(p)}>{p}</button>
+            ))}
+          </div>
+          <button className="chart-close" onClick={onClose}>✕</button>
+        </div>
+        <div style={{height:340,position:'relative'}}><canvas ref={canvasRef} /></div>
+      </div>
+    </div>
+  );
+}
+
+// ── ComparisonChart: Overlay multiple portfolios on one chart ──
+function ComparisonChart({ portfolioIds, allChartData, stratColors, stratNames, stratEmojis, onClose }) {
+  const canvasRef = React.useRef(null);
+  const chartRef = React.useRef(null);
+  const [period, setPeriod] = React.useState('7d');
+  const [compareData, setCompareData] = React.useState(allChartData);
+  const periods = ['1d','3d','7d','14d','30d'];
+
+  React.useEffect(() => {
+    if (period === '7d') { setCompareData(allChartData); return; }
+    api('/api/portfauxlio/charts?period=' + period).then(d => {
+      const byId = {};
+      (d.portfolios || []).forEach(p => { byId[p.id] = p; });
+      setCompareData(byId);
+    }).catch(() => {});
+  }, [period]);
+
+  React.useEffect(() => {
+    if (!canvasRef.current) return;
+    if (chartRef.current) chartRef.current.destroy();
+    const datasets = portfolioIds.map(id => {
+      const cd = compareData[id];
+      if (!cd) return null;
+      const strat = cd.strategy || '';
+      const clr = stratColors[strat] || '#8b5cf6';
+      const snaps = cd.snapshots || [];
+      const daily = cd.daily || [];
+      const pts = snaps.length > 1 ? snaps.map(s => ({ x: new Date(s.time), y: s.nav / 100 }))
+        : daily.length > 1 ? daily.map(d => ({ x: new Date(d.date), y: d.cumulative / 100 })) : [];
+      if (pts.length < 2) return null;
+      return { label: (stratEmojis[strat]||'') + ' ' + (cd.name || strat), data: pts, borderColor: clr, borderWidth: 2, backgroundColor: 'transparent', fill: false, pointRadius: 0, pointHoverRadius: 5, tension: 0.3 };
+    }).filter(Boolean);
+    if (!datasets.length) return;
+    const ctx = canvasRef.current.getContext('2d');
+    chartRef.current = new Chart(ctx, {
+      type: 'line', data: { datasets },
+      options: {
+        responsive: true, maintainAspectRatio: false,
+        animation: { duration: 1000, easing: 'easeOutQuart' },
+        interaction: { intersect: false, mode: 'index' },
+        plugins: {
+          legend: { display: true, position: 'bottom', labels: { color: '#94a3b8', font: { size: 10 }, boxWidth: 12, padding: 8 } },
+          tooltip: { backgroundColor: 'rgba(15,15,35,0.95)', borderWidth: 1, titleColor: '#e2e8f0', bodyColor: '#e2e8f0', padding: 12 }
+        },
+        scales: {
+          x: { type: 'time', ticks: { color: '#64748b', maxRotation: 45, font: { size: 9 } }, grid: { color: 'rgba(255,255,255,.03)' } },
+          y: { ticks: { color: '#64748b', callback: (v) => '$' + v.toFixed(0), font: { size: 10 } }, grid: { color: 'rgba(255,255,255,.05)' } }
+        }
+      }
+    });
+    return () => { if (chartRef.current) chartRef.current.destroy(); };
+  }, [compareData, portfolioIds]);
+
+  return (
+    <div className="chart-modal" onClick={onClose}>
+      <div className="chart-modal-content" onClick={e => e.stopPropagation()} style={{maxWidth:1100}}>
+        <div className="chart-modal-header">
+          <span style={{fontSize:16}}>📊</span>
+          <span style={{fontWeight:800,color:'var(--cyan)'}}>Compare {portfolioIds.length} Portfolios</span>
+          <div className="chart-period-btns" style={{marginLeft:'auto'}}>
+            {periods.map(p => (
+              <button key={p} className={period === p ? 'chart-period-active' : 'chart-period'} onClick={() => setPeriod(p)}>{p}</button>
+            ))}
+          </div>
+          <button className="chart-close" onClick={onClose}>✕</button>
+        </div>
+        <div style={{height:400,position:'relative'}}><canvas ref={canvasRef} /></div>
+      </div>
+    </div>
+  );
+}
+
+// ── Vertical Bar Chart ──
+function BarChart({ data, maxVal, height = 80, color = '#8b5cf6' }) {
+  if (!data || data.length === 0) return null;
+  const mx = maxVal || Math.max(...data.map(d => d.value)) || 1;
+  return (
+    <div className="mkt-bar-chart" style={{ height }}>
+      {data.map((d, i) => (
+        <div key={i} className="mkt-bar" style={{ height: Math.max(2, (d.value / mx) * height) + 'px', background: d.color || color, opacity: 0.7 + (d.value / mx) * 0.3 }}>
+          <div className="mkt-bar-label">{d.label}: {d.value}</div>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+// ── Confidence Histogram ──
+function ConfHistogram({ data }) {
+  if (!data || data.length === 0) return null;
+  const max = Math.max(...data.map(d => d.count)) || 1;
+  const colors = ['#ef4444','#f97316','#f59e0b','#eab308','#84cc16','#22c55e','#14b8a6','#06b6d4','#3b82f6','#8b5cf6'];
+  return (
+    <div className="histogram">
+      {data.map((d, i) => (
+        <div key={i} className="hist-bar" style={{
+          height: Math.max(2, (d.count / max) * 70) + 'px',
+          background: colors[i] || 'var(--muted)',
+          opacity: d.count > 0 ? 0.8 : 0.2,
+        }}>
+          <div className="hist-label">{d.range}</div>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+// ── Time ago helper ──
+function timeAgo(ts) {
+  if (!ts) return '';
+  const diff = Date.now() - new Date(ts).getTime();
+  if (diff < 60000) return Math.floor(diff / 1000) + 's ago';
+  if (diff < 3600000) return Math.floor(diff / 60000) + 'm ago';
+  if (diff < 86400000) return Math.floor(diff / 3600000) + 'h ago';
+  return Math.floor(diff / 86400000) + 'd ago';
+}
+
+// ── Market Data Tab — Bloomberg-style terminal ──
+function MarketDataTab() {
+  const [mkt, setMkt] = useState(null);
+  const [tick, setTick] = useState(0);
+
+  useEffect(() => {
+    const load = () => api('/api/inc/market-data').then(setMkt).catch(e => console.error(e));
+    load();
+    const iv = setInterval(load, 8000);
+    return () => clearInterval(iv);
+  }, []);
+  useEffect(() => { const iv = setInterval(() => setTick(t => t + 1), 1000); return () => clearInterval(iv); }, []);
+
+  if (!mkt) return <div style={{textAlign:'center',padding:60,color:'var(--muted)'}}>
+    <div style={{fontSize:24,marginBottom:8,animation:'avatarFloat 2s ease-in-out infinite'}}>Loading market data...</div>
+  </div>;
+
+  const signalVals = (mkt.signalTrend || []).map(s => s.signals);
+  const edgeVals = (mkt.signalTrend || []).map(s => s.avgEdge * 100);
+  const sentVals = (mkt.sentimentTrend || []).map(s => s.articles);
+  const sentScoreVals = (mkt.sentimentTrend || []).map(s => s.avgSentiment);
+  const totalSent24h = sentVals.reduce((a, b) => a + b, 0);
+  const totalSignals = signalVals.reduce((a, b) => a + b, 0);
+  const at = mkt.approvalTrend || {};
+  const changeArrow = at.change > 0 ? '▲' : at.change < 0 ? '▼' : '—';
+  const changeClass = at.change > 0 ? 'up' : at.change < 0 ? 'down' : 'flat';
+  const totalPnl = (mkt.portfolios || []).reduce((s, p) => s + (p.pnl_cents || 0), 0);
+  const totalTrades = (mkt.portfolios || []).reduce((s, p) => s + (p.total_trades || 0), 0);
+  const avgEdge = edgeVals.length > 0 ? (edgeVals.reduce((a,b)=>a+b,0) / edgeVals.length) : 0;
+
+  // Ticker items
+  const tickerItems = [
+    { label: 'P&L', value: (totalPnl >= 0 ? '+' : '') + (totalPnl / 100).toFixed(2), color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)' },
+    { label: 'SIGNALS', value: totalSignals, color: 'var(--blue)' },
+    { label: 'APPROVAL', value: at.current + '%', change: at.change, color: 'var(--green)' },
+    { label: 'SENTIMENT', value: totalSent24h.toLocaleString(), color: 'var(--pink)' },
+    { label: 'AVG EDGE', value: avgEdge.toFixed(1) + '%', color: 'var(--purple)' },
+    { label: 'TRADES', value: totalTrades, color: 'var(--yellow)' },
+    { label: 'MARKETS', value: mkt.activeMarkets, color: 'var(--cyan)' },
+    { label: 'GEMINI', value: mkt.gemini.callsToday + '/' + mkt.gemini.dailyLimit, color: 'var(--teal)' },
+  ];
+  const doubledTicker = tickerItems.concat(tickerItems);
+
+  const stratColors = {aggressive:'#ef4444',conservative:'#3b82f6',weather:'#06b6d4',momentum:'#f59e0b',contrarian:'#8b5cf6',ai_enhanced:'#ec4899',mc_validated:'#a855f7',scalper:'#eab308',patient:'#22c55e',random:'#64748b',penny_pincher:'#94a3b8',spray_pray:'#fb923c',nickel_slots:'#a78bfa',degen_lite:'#f43f5e',politics_junkie:'#0ea5e9',double_down:'#dc2626',volatility_rider:'#14b8a6',whale:'#1d4ed8',yolo:'#e11d48',full_send:'#b91c1c',kelly_criterion:'#059669',sharpe_sniper:'#7c3aed',bayesian_blend:'#0891b2',ensemble_lock:'#c026d3',mean_revert:'#d97706',info_ratio:'#2563eb',anti_fomo:'#16a34a',nightcrawler:'#4c1d95',heatseeker:'#ea580c',quant_core:'#0d9488',etf_presidential:'#dc143c',etf_cabinet:'#ff6347',etf_congress:'#4169e1',etf_scotus:'#2f4f4f',etf_fed:'#228b22',etf_economy:'#daa520',etf_geopolitics:'#8b0000',etf_territory:'#006400',etf_climate:'#00bfff',etf_energy:'#ff8c00',etf_tech:'#7b68ee',etf_space:'#191970',etf_crypto:'#ffd700',etf_sports:'#32cd32',etf_entertainment:'#ff1493',etf_legal:'#708090',etf_financials:'#4682b4',etf_govreform:'#b22222',etf_global_social:'#9370db',etf_broad_market:'#c0a050'};
+  const stratEmojis = {aggressive:'🔥',conservative:'🏛️',weather:'⛈️',momentum:'🚀',contrarian:'🔄',ai_enhanced:'🧠',mc_validated:'🎲',scalper:'⚡',patient:'🐢',random:'🎰',penny_pincher:'🪙',spray_pray:'💦',nickel_slots:'🎰',degen_lite:'😈',politics_junkie:'🏛️',double_down:'⬆️',volatility_rider:'🏄',whale:'🐳',yolo:'🧨',full_send:'💣',kelly_criterion:'🎰',sharpe_sniper:'🎯',bayesian_blend:'🧮',ensemble_lock:'🔐',mean_revert:'↩️',info_ratio:'📊',anti_fomo:'🧘',nightcrawler:'🦇',heatseeker:'🔬',quant_core:'∑',etf_presidential:'🏛️',etf_cabinet:'💼',etf_congress:'🗳️',etf_scotus:'⚖️',etf_fed:'🏦',etf_economy:'📈',etf_geopolitics:'🌍',etf_territory:'🗺️',etf_climate:'🌡️',etf_energy:'⛽',etf_tech:'🤖',etf_space:'🚀',etf_crypto:'₿',etf_sports:'⚽',etf_entertainment:'🎬',etf_legal:'🔨',etf_financials:'💹',etf_govreform:'✂️',etf_global_social:'⛪',etf_broad_market:'🌐'};
+
+  return (<div>
+    {/* ═══ Hero P&L Banner ═══ */}
+    <div style={{textAlign:'center',padding:'20px 0 8px',borderBottom:'1px solid var(--border)'}}>
+      <div style={{fontSize:10,color:'var(--muted)',textTransform:'uppercase',letterSpacing:2,marginBottom:4}}>Total Portfolio P&L</div>
+      <div className="pnl-hero" style={{color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)', animation: totalPnl !== 0 ? 'pnlPulse 3s ease-in-out infinite' : 'none'}}>
+        {totalPnl >= 0 ? '+' : ''}{(totalPnl / 100).toFixed(2)}
+      </div>
+      <div style={{display:'flex',justifyContent:'center',gap:24,marginTop:8,fontSize:11,color:'var(--muted)'}}>
+        <span>{totalTrades} trades</span>
+        <span>{(mkt.portfolios||[]).filter(p => p.pnl_cents > 0).length} winning</span>
+        <span>{(mkt.portfolios||[]).filter(p => p.pnl_cents < 0).length} losing</span>
+        <span>{avgEdge.toFixed(1)}% avg edge</span>
+      </div>
+    </div>
+
+    {/* ═══ Scrolling Ticker ═══ */}
+    <div className="mkt-ticker-strip">
+      <div className="mkt-ticker-track">
+        {doubledTicker.map((t, i) => (
+          <div key={i} className="mkt-ticker-item">
+            <span style={{color:'var(--muted)',fontSize:9,textTransform:'uppercase',letterSpacing:.5}}>{t.label}</span>
+            <span style={{color:t.color,fontWeight:800}}>{t.value}</span>
+            {t.change !== undefined && <span className={'mkt-change ' + (t.change > 0 ? 'up' : t.change < 0 ? 'down' : 'flat')}>
+              {t.change > 0 ? '▲' : t.change < 0 ? '▼' : ''}{Math.abs(t.change)}%
+            </span>}
+          </div>
+        ))}
+      </div>
+    </div>
+
+    <div className="mkt-grid">
+      {/* ═══ Live Signal Feed ═══ */}
+      <div className="mkt-card" style={{gridColumn:'span 2'}}>
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--blue)'}}>
+            <span style={{display:'inline-block',width:6,height:6,borderRadius:'50%',background:'var(--green)',marginRight:6,animation:'statusPulse 2s ease infinite'}}></span>
+            Live Signal Feed
+          </span>
+          <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{(mkt.latestSignals||[]).length} latest</span>
+        </div>
+        <div className="signal-feed">
+          {(mkt.latestSignals || []).map((sig, i) => (
+            <div key={i} className="signal-row">
+              <div className="signal-approved" style={{background: sig.approved ? 'var(--green)' : 'var(--red)'}} title={sig.approved ? 'Approved' : 'Rejected'} />
+              <span className={'signal-dir ' + sig.direction}>{sig.direction}</span>
+              <span className="signal-market" title={sig.market}>{sig.market}</span>
+              <span className="signal-edge" style={{color: sig.edge > 0 ? 'var(--green)' : 'var(--red)'}}>
+                {sig.edge > 0 ? '+' : ''}{(sig.edge * 100).toFixed(1)}%
+              </span>
+              <span className="signal-conf" title="Confidence">{(sig.confidence * 100).toFixed(0)}%</span>
+              <span className="signal-time">{timeAgo(sig.time)}</span>
+            </div>
+          ))}
+          {(!mkt.latestSignals || mkt.latestSignals.length === 0) && <div style={{padding:20,textAlign:'center',color:'var(--muted)',fontSize:12}}>No recent signals</div>}
+        </div>
+      </div>
+
+      {/* ═══ Signal Volume Sparkline ═══ */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--blue)'}}>Signal Volume</span>
+          <span className="mkt-card-badge" style={{background:'rgba(59,130,246,.15)',color:'var(--blue)'}}>{totalSignals}</span>
+        </div>
+        <Sparkline data={signalVals} width={300} height={50} color="#3b82f6" showDots={true} label={true} />
+        <BarChart data={(mkt.signalTrend || []).map((s) => ({
+          value: s.signals,
+          label: new Date(s.hour).toLocaleTimeString('en-US', {hour:'numeric',hour12:true}),
+          color: s.approved > s.signals * 0.5 ? 'rgba(34,197,94,.6)' : 'rgba(239,68,68,.4)',
+        }))} height={45} />
+      </div>
+
+      {/* ═══ Sentiment Flow ═══ */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--pink)'}}>Sentiment Flow</span>
+          <span className="mkt-card-badge" style={{background:'rgba(236,72,153,.15)',color:'var(--pink)'}}>{totalSent24h.toLocaleString()}</span>
+        </div>
+        <Sparkline data={sentVals} width={300} height={50} color="#ec4899" label={true} />
+        <div style={{display:'flex',justifyContent:'space-between',marginTop:8,fontSize:10,color:'var(--muted)'}}>
+          <span>Avg: <strong style={{color: sentScoreVals.length > 0 && sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length > 0 ? 'var(--green)' : 'var(--red)'}}>
+            {sentScoreVals.length > 0 ? (sentScoreVals.reduce((a,b)=>a+b,0)/sentScoreVals.length).toFixed(3) : '—'}
+          </strong></span>
+          <span>{(mkt.sourceMix||[]).length} sources</span>
+        </div>
+      </div>
+
+      {/* ═══ Approval Rate ═══ */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--green)'}}>Approval Rate</span>
+          <span className={'mkt-change ' + changeClass} style={{fontSize:14}}>{changeArrow} {Math.abs(at.change)}%</span>
+        </div>
+        <div style={{display:'flex',alignItems:'baseline',gap:12,marginBottom:8}}>
+          <span style={{fontSize:42,fontWeight:800,color:'var(--green)',fontVariantNumeric:'tabular-nums'}}>{at.current}%</span>
+          <span style={{fontSize:12,color:'var(--muted)'}}>vs {at.prior}% prior</span>
+        </div>
+        <div className="mkt-gauge">
+          <div className="mkt-gauge-fill" style={{width: at.current + '%', background:'linear-gradient(90deg,var(--green),var(--teal))'}} />
+          <div className="mkt-gauge-label">{at.recentSignals} signals</div>
+        </div>
+      </div>
+
+      {/* ═══ Edge Trend ═══ */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--purple)'}}>Edge Trend</span>
+          <span className="mkt-card-badge" style={{background:'rgba(139,92,246,.15)',color:'var(--purple)'}}>
+            {avgEdge.toFixed(1)}% avg
+          </span>
+        </div>
+        <Sparkline data={edgeVals} width={300} height={50} color="#8b5cf6" showDots={true} label={true} />
+      </div>
+
+      {/* ═══ Market Heatmap ═══ */}
+      {(mkt.marketStats || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Market Heatmap (24h)</span>
+          <span className="mkt-card-badge" style={{background:'rgba(245,158,11,.15)',color:'var(--yellow)'}}>{(mkt.marketStats||[]).length} markets</span>
+        </div>
+        <div className="heatmap-grid">
+          {(mkt.marketStats || []).map((m, i) => {
+            const edge = m.avgEdge * 100;
+            const intensity = Math.min(1, Math.abs(edge) / 25);
+            const bg = edge >= 0
+              ? 'rgba(34,197,94,' + (0.08 + intensity * 0.2) + ')'
+              : 'rgba(239,68,68,' + (0.08 + intensity * 0.2) + ')';
+            const borderC = edge >= 0 ? 'rgba(34,197,94,' + (0.2 + intensity * 0.3) + ')' : 'rgba(239,68,68,' + (0.2 + intensity * 0.3) + ')';
+            return (<div key={i} className="heatmap-cell" style={{background: bg, borderColor: borderC}}>
+              <div className="heatmap-market">{m.market}</div>
+              <div className="heatmap-edge" style={{color: edge >= 0 ? 'var(--green)' : 'var(--red)'}}>
+                {edge >= 0 ? '+' : ''}{edge.toFixed(1)}%
+              </div>
+              <div className="heatmap-meta">{m.signals} sig | {m.approved} appr | {(m.avgConf * 100).toFixed(0)}% conf</div>
+            </div>);
+          })}
+        </div>
+      </div>}
+
+      {/* ═══ Confidence Distribution ═══ */}
+      {(mkt.confDistribution || []).length > 0 && <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--cyan)'}}>Confidence Distribution</span>
+        </div>
+        <ConfHistogram data={mkt.confDistribution} />
+        <div style={{marginTop:18,display:'flex',justifyContent:'space-between',fontSize:9,color:'var(--muted)'}}>
+          <span>Low conf</span><span>High conf</span>
+        </div>
+      </div>}
+
+      {/* ═══ Intelligence Sources ═══ */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--cyan)'}}>Intelligence Sources</span>
+          <span className="mkt-card-badge" style={{background:'rgba(6,182,212,.15)',color:'var(--cyan)'}}>{(mkt.sourceMix||[]).length}</span>
+        </div>
+        <div className="mkt-source-list">
+          {(mkt.sourceMix || []).slice(0, 12).map((s, i) => {
+            const maxC = (mkt.sourceMix || [])[0]?.count || 1;
+            const colors = ['#3b82f6','#8b5cf6','#ec4899','#ef4444','#f59e0b','#22c55e','#06b6d4','#14b8a6','#6366f1','#d946ef','#f97316','#a855f7'];
+            return (<div key={i} className="mkt-source-row">
+              <span className="mkt-source-name">{s.source}</span>
+              <div style={{flex:1,display:'flex',alignItems:'center',gap:4}}>
+                <div className="mkt-source-bar" style={{width: (s.count / maxC * 100) + '%', background: colors[i % colors.length]}} />
+              </div>
+              <span className="mkt-source-val">{s.count.toLocaleString()}</span>
+              <span style={{fontSize:9,color: s.avgSentiment > 0 ? 'var(--green)' : s.avgSentiment < 0 ? 'var(--red)' : 'var(--muted)', minWidth:35, textAlign:'right'}}>
+                {s.avgSentiment > 0 ? '+' : ''}{s.avgSentiment.toFixed(2)}
+              </span>
+            </div>);
+          })}
+        </div>
+      </div>
+
+      {/* ═══ Gemini + Categories row ═══ */}
+      <div className="mkt-card">
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--teal)'}}>Gemini AI</span>
+          <span className="mkt-card-badge" style={{background: mkt.gemini.pctUsed > 80 ? 'rgba(239,68,68,.15)' : 'rgba(20,184,166,.15)', color: mkt.gemini.pctUsed > 80 ? 'var(--red)' : 'var(--teal)'}}>
+            {mkt.gemini.pctUsed}%
+          </span>
+        </div>
+        <div style={{display:'flex',alignItems:'baseline',gap:8,marginBottom:8}}>
+          <span style={{fontSize:32,fontWeight:800,color:'var(--teal)',fontVariantNumeric:'tabular-nums'}}>{mkt.gemini.callsToday}</span>
+          <span style={{fontSize:11,color:'var(--muted)'}}>/ {mkt.gemini.dailyLimit}</span>
+        </div>
+        <div className="mkt-gauge">
+          <div className="mkt-gauge-fill" style={{width: mkt.gemini.pctUsed + '%', background: mkt.gemini.pctUsed > 80 ? 'linear-gradient(90deg,var(--yellow),var(--red))' : 'linear-gradient(90deg,var(--teal),var(--cyan))'}} />
+          <div className="mkt-gauge-label">{mkt.gemini.pctUsed}%</div>
+        </div>
+        {/* Categories inline */}
+        <div style={{marginTop:14,display:'flex',flexWrap:'wrap',gap:4}}>
+          {(mkt.categories || []).slice(0, 8).map((c, i) => {
+            const colors = ['#f59e0b','#3b82f6','#ec4899','#22c55e','#8b5cf6','#ef4444','#06b6d4','#14b8a6'];
+            return (<div key={i} style={{padding:'3px 10px',borderRadius:6,background:'rgba(255,255,255,.03)',border:'1px solid var(--border)',fontSize:9,display:'flex',alignItems:'center',gap:4}}>
+              <div style={{width:4,height:4,borderRadius:'50%',background:colors[i % colors.length]}} />
+              <span style={{fontWeight:600}}>{c.category}</span>
+              <span style={{color:'var(--muted)'}}>{c.count}</span>
+            </div>);
+          })}
+        </div>
+      </div>
+
+      {/* ═══ Recent Trades ═══ */}
+      {(mkt.recentTrades || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--yellow)'}}>Recent Trades</span>
+          <span className="mkt-card-badge" style={{background:'rgba(245,158,11,.15)',color:'var(--yellow)'}}>{totalTrades} total</span>
+        </div>
+        <div className="trade-log">
+          {(mkt.recentTrades || []).map((t, i) => {
+            const c = stratColors[t.strategy] || 'var(--muted)';
+            return (<div key={i} className="trade-row">
+              <span className="trade-portfolio" style={{color:c}}>{t.portfolio_name}</span>
+              <span className={'signal-dir ' + t.direction} style={{fontSize:9}}>{t.direction}</span>
+              <span style={{flex:1,overflow:'hidden',textOverflow:'ellipsis',whiteSpace:'nowrap',color:'var(--muted)'}}>{t.market_id}</span>
+              <span style={{fontSize:10,color:'var(--muted)'}}>{t.contracts}x @ {t.entry_price_cents}¢</span>
+              <span className="trade-pnl" style={{color: t.pnl_cents >= 0 ? 'var(--green)' : 'var(--red)'}}>
+                {t.pnl_cents >= 0 ? '+' : ''}{(t.pnl_cents / 100).toFixed(2)}
+              </span>
+              <span className="signal-time">{timeAgo(t.created_at)}</span>
+            </div>);
+          })}
+        </div>
+      </div>}
+
+      {/* ═══ Portfolio Leaderboard ═══ */}
+      {(mkt.portfolios || []).length > 0 && <div className="mkt-card" style={{gridColumn:'span 2'}}>
+        <div className="mkt-card-header">
+          <span className="mkt-card-title" style={{color:'var(--pink)'}}>Portfolio Leaderboard</span>
+          <span className="mkt-card-badge" style={{background:'rgba(255,105,180,.15)',color:'var(--pink)'}}>{totalTrades} trades</span>
+        </div>
+        <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill,minmax(220px,1fr))',gap:8}}>
+          {(mkt.portfolios || []).map((p, i) => {
+            const c = stratColors[p.strategy] || 'var(--muted)';
+            const emoji = stratEmojis[p.strategy] || '';
+            const pnl = p.pnl_cents || 0;
+            const invested = p.total_invested_cents || 0;
+            const roi = invested > 0 ? ((pnl / invested) * 100).toFixed(1) : '0.0';
+            return (<div key={i} style={{padding:'12px 14px',background:'rgba(255,255,255,.02)',border:'1px solid var(--border)',borderRadius:10,transition:'all .3s',borderLeftColor:c,borderLeftWidth:3,position:'relative',overflow:'hidden'}}>
+              {/* Rank badge */}
+              <div style={{position:'absolute',top:6,right:8,fontSize:8,color:'var(--muted)',fontWeight:800}}>#{i+1}</div>
+              <div style={{display:'flex',alignItems:'center',gap:8,marginBottom:6}}>
+                <span style={{fontSize:20}}>{emoji}</span>
+                <div style={{flex:1}}>
+                  <div style={{fontSize:13,fontWeight:700,color:c}}>{p.name}</div>
+                  <div style={{fontSize:9,color:'var(--muted)',textTransform:'uppercase',letterSpacing:.3}}>{p.strategy}</div>
+                </div>
+                <div style={{textAlign:'right'}}>
+                  <div style={{fontSize:18,fontWeight:900,color: pnl >= 0 ? 'var(--green)' : 'var(--red)',fontVariantNumeric:'tabular-nums'}}>
+                    {pnl >= 0 ? '+' : ''}{(pnl / 100).toFixed(2)}
+                  </div>
+                  <div style={{fontSize:9,color:'var(--muted)'}}>ROI: {roi}%</div>
+                </div>
+              </div>
+              <div style={{display:'flex',gap:4,fontSize:9,flexWrap:'wrap'}}>
+                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(34,197,94,.1)',color:'var(--green)'}}>W:{p.trades_won}</span>
+                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(239,68,68,.1)',color:'var(--red)'}}>L:{p.trades_lost}</span>
+                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>WR:{p.win_rate}%</span>
+                <span style={{padding:'2px 7px',borderRadius:4,background:'rgba(255,255,255,.04)',color:'var(--muted)'}}>{p.total_trades} trades</span>
+                {p.streak !== 0 && <span style={{padding:'2px 7px',borderRadius:4,background: p.streak > 0 ? 'rgba(34,197,94,.1)' : 'rgba(239,68,68,.1)',color: p.streak > 0 ? 'var(--green)' : 'var(--red)'}}>{p.streak > 0 ? '+' : ''}{p.streak} streak</span>}
+              </div>
+              {invested > 0 && <div style={{marginTop:6}}>
+                <div style={{height:3,borderRadius:2,background:'rgba(255,255,255,.05)',overflow:'hidden'}}>
+                  <div style={{height:'100%',borderRadius:2,width: Math.min(100, Math.abs(parseFloat(roi))) + '%',background: pnl >= 0 ? 'var(--green)' : 'var(--red)',transition:'width .5s'}} />
+                </div>
+              </div>}
+            </div>);
+          })}
+        </div>
+      </div>}
+    </div>
+  </div>);
+}
+
+function App() {
+  const [data, setData] = useState(null);
+  const [expandedAgent, setExpandedAgent] = useState(null);
+  const [tab, setTab] = useState('command');
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    const load = async () => {
+      try {
+        const d = await api('/api/inc');
+        setData(d);
+        setLoading(false);
+      } catch(e) { console.error(e); setLoading(false); }
+    };
+    load();
+    const iv = setInterval(load, 15000);
+    return () => clearInterval(iv);
+  }, []);
+
+  if (loading || !data) return (
+    <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', flexDirection: 'column', gap: 16 }}>
+      <div style={{ width: 60, height: 60, borderRadius: '50%', background: 'linear-gradient(135deg,#ff69b4,#8b5cf6)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 28, animation: 'avatarFloat 2s ease-in-out infinite' }}>K</div>
+      <div style={{ color: 'var(--muted)', fontSize: 14, letterSpacing: 2 }}>INITIALIZING KEN INC...</div>
+    </div>
+  );
+
+  const d = data;
+  const approvalRate = d.pipeline.signalsLast24h > 0
+    ? Math.round(d.pipeline.approvedLast24h / d.pipeline.signalsLast24h * 100) : 0;
+
+  const tabStyle = (t) => ({
+    padding: '10px 24px', fontSize: 13, fontWeight: 600, border: '1px solid ' + (tab===t?'rgba(255,255,255,.2)':'var(--border)'),
+    borderRadius: 10, background: tab===t?'rgba(255,255,255,.08)':'transparent', color: tab===t?'var(--text)':'var(--muted)',
+    cursor: 'pointer', transition: 'all .2s', letterSpacing: .3,
+  });
+
+  return (
+    <div className="container">
+      <Particles />
+      <Clock />
+
+      <div className="header">
+        <div className="header-glow" />
+        <div className="ceo-avatar">K</div>
+        <div className="ceo-name">Ken</div>
+        <div className="ceo-title">Chief Executive Officer</div>
+        <div className="ceo-tagline">Trading Intelligence Corporation</div>
+      </div>
+
+      <div style={{display:'flex',justifyContent:'center',gap:8,marginBottom:24,flexWrap:'wrap'}}>
+        <button style={tabStyle('command')} onClick={()=>setTab('command')}>Command Center</button>
+        <button style={tabStyle('market')} onClick={()=>setTab('market')}>Market Data</button>
+        <button style={tabStyle('portfolios')} onClick={()=>setTab('portfolios')}>portFAUXlio</button>
+        <button style={tabStyle('predictions')} onClick={()=>setTab('predictions')}>Predictions</button>
+        <button style={tabStyle('signals')} onClick={()=>setTab('signals')}>Live Signals</button>
+      </div>
+
+      {tab === 'command' && <>
+        <div className="stats-bar">
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--pink)'}}>{d.agents.length}</div><div className="stat-label">Sub-Agents</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--blue)'}}>{d.pipeline.signalsLast24h}</div><div className="stat-label">Signals / 24h</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--green)'}}>{approvalRate}%</div><div className="stat-label">Approval Rate</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--purple)'}}>{d.pipeline.lastHour}</div><div className="stat-label">Last Hour</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--cyan)'}}>{(parseFloat(d.pipeline.avgEdge)*100).toFixed(1)}%</div><div className="stat-label">Avg Edge</div></div>
+          <div className="stat-item"><div className="stat-val" style={{color:'var(--yellow)'}}>{d.system.memory_mb}MB</div><div className="stat-label">Memory</div></div>
+          <div className="stat-item"><div className="stat-val">{d.system.cpuCores}</div><div className="stat-label">CPU Cores</div></div>
+        </div>
+        <div className="network">
+          <div className="agents-grid">
+            {d.agents.map(a => (
+              <AgentCard key={a.id} agent={a} expanded={expandedAgent===a.id} onToggle={()=>setExpandedAgent(expandedAgent===a.id?null:a.id)} />
+            ))}
+          </div>
+        </div>
+      </>}
+
+      {tab === 'market' && <MarketDataTab />}
+      {tab === 'portfolios' && <PortfoliosTab />}
+      {tab === 'predictions' && <PredictionsTab />}
+
+      {tab === 'signals' && <div className="signal-feed">
+        <h2><span className="live-dot" /> Live Signal Feed</h2>
+        <SignalFeed signals={d.recentSignals} />
+      </div>}
+
+      <div className="footer">
+        Ken Inc. Trading Intelligence &bull; {d.system.cpuCores} cores &bull; {d.system.uptime_hours}h uptime &bull; 50 paper portfolios &bull;
+        <a href="/"> Back to Dashboard</a>
+      </div>
+    </div>
+  );
+}
+
+ReactDOM.createRoot(document.getElementById('root')).render(<App />);
+</script>
+</body>
+</html>`;
+
+// ════════════════════════════════════════
+// HTTP SERVER
+// ════════════════════════════════════════
+
+const server = http.createServer(async (req, res) => {
+  // CORS preflight
+  if (req.method === 'OPTIONS') {
+    res.writeHead(200, {
+      'Access-Control-Allow-Origin': '*',
+      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
+    });
+    res.end();
+    return;
+  }
+
+  // Route key for matching
+  const urlPath = req.url.split('?')[0];
+  const routeKey = `${req.method} ${urlPath}`;
+
+  // Public endpoints (no auth required)
+  const publicPaths = [
+    '/api/auth/login',
+    '/api/auth/logout',
+    '/api/auth/session',
+    '/api/auth/google',
+    '/api/auth/google-config',
+    '/api/health',
+    '/api/portfauxlio',
+    '/api/portfauxlio/charts',
+    '/api/portfauxlio/swot',
+    '/api/markets',
+    '/api/signals',
+    '/api/energy-arb',
+    '/api/predictions',
+    '/api/portfolios',
+    '/api/scan-config',
+    '/api/dashboard',
+  ];
+  const isPublic = publicPaths.includes(urlPath);
+
+  // Enforce auth on all other API routes
+  if (!isPublic && urlPath.startsWith('/api/') && !isAuthenticated(req)) {
+    return json(res, { error: 'Authentication required' }, 401);
+  }
+
+  // Check route handlers
+  if (routes[routeKey]) {
+    try {
+      await routes[routeKey](req, res);
+    } catch (err) {
+      console.error(`[ERROR] ${routeKey}:`, err.message);
+      json(res, { error: err.message }, 500);
+    }
+    return;
+  }
+
+  // Unknown API route
+  if (urlPath.startsWith('/api/')) {
+    return json(res, { error: `Unknown API endpoint: ${req.method} ${urlPath}` }, 404);
+  }
+
+  // ── Ken Inc. Command Center ──
+  if (urlPath === '/inc' || urlPath === '/inc/') {
+    if (!isAuthenticated(req)) {
+      return json(res, { error: 'Authentication required' }, 401);
+    }
+    res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
+    res.end(KEN_INC_HTML);
+    return;
+  }
+
+  // ── Tournament Arena — 3D Team Competition ──
+  if (urlPath === '/arena' || urlPath === '/arena/') {
+    res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
+    res.end(ARENA_HTML);
+    return;
+  }
+
+  // ── PortFAUXlios Dashboard ──
+  if (urlPath === '/portfauxlios' || urlPath === '/portfauxlios/') {
+    try {
+      const dashHtml = fs.readFileSync('/root/DW-Agents/ken-portfauxlios-dashboard.html', 'utf8');
+      res.writeHead(200, { 'Content-Type': 'text/html', 'Cache-Control': 'no-cache' });
+      res.end(dashHtml);
+    } catch (e) {
+      res.writeHead(500);
+      res.end('Dashboard not found: ' + e.message);
+    }
+    return;
+  }
+
+  // ── Serve Static Files / SPA Routing ──
+  let filePath = path.join(DIST, urlPath === '/' ? 'index.html' : urlPath);
+
+  // Security: prevent directory traversal
+  if (!filePath.startsWith(DIST)) {
+    res.writeHead(403);
+    res.end('Forbidden');
+    return;
+  }
+
+  // Check if file exists; if not, serve index.html for SPA routing
+  if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
+    filePath = path.join(DIST, 'index.html');
+  }
+
+  try {
+    const content = fs.readFileSync(filePath);
+    const ext = path.extname(filePath);
+    const contentType = MIME[ext] || 'application/octet-stream';
+
+    // Cache static assets (hashed filenames from Vite)
+    const cacheHeader = ext === '.html'
+      ? 'no-cache, no-store, must-revalidate'
+      : 'public, max-age=31536000, immutable';
+
+    res.writeHead(200, {
+      'Content-Type': contentType,
+      'Cache-Control': cacheHeader,
+    });
+    res.end(content);
+  } catch {
+    res.writeHead(404);
+    res.end('Not found');
+  }
+});
+
+// ══════════════════════════════════════════════════════════════
+// KEN AUTONOMOUS TRADING BRAIN — 24/7 MARKET INTELLIGENCE
+// Scans every 15 min: Kalshi markets, Reddit, news, Polymarket
+// Stores everything in PostgreSQL, tracks own performance
+// ══════════════════════════════════════════════════════════════
+
+const KEN_DB = requiredEnv('KEN_DATABASE_URL');
+const kenPool = new pg.Pool({ connectionString: KEN_DB, max: 20, idleTimeoutMillis: 30000 });
+async function kenQ(text, params) { return kenPool.query(text, params); }
+
+// ── Mutable scan/pipeline intervals (adjustable via Settings UI) ──
+let SCAN_INTERVAL_MS = 8 * 60 * 1000; // 8 minutes — market scans
+let REDDIT_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes — intelligence sweep
+let SIGNAL_PIPELINE_MS = 2 * 60 * 1000; // 2 minutes — signal pipeline
+let RESOLVE_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes — trade resolution
+const SLACK_COOLDOWN_MS = 4 * 60 * 60 * 1000;
+let lastSlackTime = 0;
+let cachedActiveMarkets = [];
+let cachedMomentumMap = {};
+
+// Interval handles (stored so we can clear + reset when config changes)
+let scanIntervalId = null;
+let redditIntervalId = null;
+let signalIntervalId = null;
+let resolveIntervalId = null;
+
+// Load scan config from DB on startup
+async function loadScanConfig() {
+  try {
+    await initConfigTable();
+    const { rows } = await kenQ("SELECT value FROM ken_config WHERE key = 'scan_config'");
+    if (rows.length > 0) {
+      const cfg = rows[0].value;
+      if (cfg.scan_interval_ms) SCAN_INTERVAL_MS = cfg.scan_interval_ms;
+      if (cfg.reddit_interval_ms) REDDIT_INTERVAL_MS = cfg.reddit_interval_ms;
+      if (cfg.signal_pipeline_ms) SIGNAL_PIPELINE_MS = cfg.signal_pipeline_ms;
+      if (cfg.resolve_interval_ms) RESOLVE_INTERVAL_MS = cfg.resolve_interval_ms;
+      if (cfg.min_edge_threshold !== undefined) RISK_LIMITS.minEdgeThreshold = cfg.min_edge_threshold;
+      if (cfg.min_liquidity !== undefined) RISK_LIMITS.minLiquidity = cfg.min_liquidity;
+      console.log(`[Ken] Loaded scan_config: scan=${SCAN_INTERVAL_MS/1000}s, signal=${SIGNAL_PIPELINE_MS/1000}s, sweep=${REDDIT_INTERVAL_MS/1000}s, resolve=${RESOLVE_INTERVAL_MS/1000}s, edge=${RISK_LIMITS.minEdgeThreshold}, liq=${RISK_LIMITS.minLiquidity}`);
+    }
+  } catch (e) {
+    console.log(`[Ken] Failed to load scan config: ${e.message}`);
+  }
+}
+
+async function saveScanConfig() {
+  try {
+    await kenQ(`
+      INSERT INTO ken_config (key, value, updated_at) VALUES ('scan_config', $1, NOW())
+      ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()
+    `, [JSON.stringify({
+      scan_interval_ms: SCAN_INTERVAL_MS,
+      reddit_interval_ms: REDDIT_INTERVAL_MS,
+      signal_pipeline_ms: SIGNAL_PIPELINE_MS,
+      resolve_interval_ms: RESOLVE_INTERVAL_MS,
+      min_edge_threshold: RISK_LIMITS.minEdgeThreshold,
+      min_liquidity: RISK_LIMITS.minLiquidity,
+    })]);
+  } catch (e) {
+    console.log(`[Ken] Failed to save scan config: ${e.message}`);
+  }
+}
+
+function restartIntervals() {
+  if (scanIntervalId) clearInterval(scanIntervalId);
+  if (redditIntervalId) clearInterval(redditIntervalId);
+  if (signalIntervalId) clearInterval(signalIntervalId);
+  if (resolveIntervalId) clearInterval(resolveIntervalId);
+
+  scanIntervalId = setInterval(() => runScanSafe(autonomousScan), SCAN_INTERVAL_MS);
+  redditIntervalId = setInterval(() => runSweepSafe('Reddit', redditIntelligenceLoop), REDDIT_INTERVAL_MS);
+  signalIntervalId = setInterval(async () => {
+    if (cachedActiveMarkets.length > 0) {
+      try { await signalPipeline(cachedActiveMarkets); } catch (e) { console.log(`[Ken] Fast pipeline error: ${e.message}`); }
+    }
+  }, SIGNAL_PIPELINE_MS);
+  resolveIntervalId = setInterval(() => {
+    resolvePortfolioTrades().catch(e => console.error('[Ken] Portfolio resolution error:', e.message));
+  }, RESOLVE_INTERVAL_MS);
+
+  console.log(`[Ken] Intervals restarted: scan=${SCAN_INTERVAL_MS/60000}m, signal=${SIGNAL_PIPELINE_MS/60000}m, sweep=${REDDIT_INTERVAL_MS/60000}m, resolve=${RESOLVE_INTERVAL_MS/60000}m`);
+}
+
+// ── Reddit Scraper (no auth needed — uses .json API) ──
+async function scrapeReddit(subreddit, searchTerm, limit = 25) {
+  try {
+    const encoded = encodeURIComponent(searchTerm);
+    const url = `https://www.reddit.com/r/${subreddit}/search.json?q=${encoded}&sort=new&restrict_sr=on&limit=${limit}&t=week`;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch(url, {
+      headers: { 'User-Agent': 'Ken-Bot/2.0 (trading intelligence)' },
+      signal: controller.signal,
+    });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const data = await res.json();
+    const posts = (data?.data?.children || []).map(c => ({
+      title: c.data.title,
+      selftext: (c.data.selftext || '').substring(0, 500),
+      score: c.data.score || 0,
+      num_comments: c.data.num_comments || 0,
+      url: `https://reddit.com${c.data.permalink}`,
+      created: new Date(c.data.created_utc * 1000),
+      subreddit: c.data.subreddit,
+    }));
+    return posts;
+  } catch (e) {
+    console.log(`[Ken] Reddit scrape failed r/${subreddit} "${searchTerm}": ${e.message}`);
+    return [];
+  }
+}
+
+// ── Scrape Reddit hot/top posts for general sentiment ──
+async function scrapeSubredditHot(subreddit, limit = 15) {
+  try {
+    const url = `https://www.reddit.com/r/${subreddit}/hot.json?limit=${limit}`;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch(url, {
+      headers: { 'User-Agent': 'Ken-Bot/2.0 (trading intelligence)' },
+      signal: controller.signal,
+    });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const data = await res.json();
+    return (data?.data?.children || []).map(c => ({
+      title: c.data.title,
+      selftext: (c.data.selftext || '').substring(0, 500),
+      score: c.data.score || 0,
+      num_comments: c.data.num_comments || 0,
+      url: `https://reddit.com${c.data.permalink}`,
+      created: new Date(c.data.created_utc * 1000),
+      subreddit: c.data.subreddit,
+    }));
+  } catch (e) {
+    return [];
+  }
+}
+
+// ── Google News RSS scraper ──
+async function scrapeGoogleNews(query, limit = 10) {
+  try {
+    const encoded = encodeURIComponent(query);
+    const url = `https://news.google.com/rss/search?q=${encoded}&hl=en-US&gl=US&ceid=US:en`;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch(url, { signal: controller.signal });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const xml = await res.text();
+    // Simple XML parse for RSS items
+    const items = [];
+    const itemRegex = /<item>([\s\S]*?)<\/item>/g;
+    let match;
+    while ((match = itemRegex.exec(xml)) && items.length < limit) {
+      const block = match[1];
+      const title = (block.match(/<title>([\s\S]*?)<\/title>/) || [])[1] || '';
+      const link = (block.match(/<link>([\s\S]*?)<\/link>/) || [])[1] || '';
+      const pubDate = (block.match(/<pubDate>([\s\S]*?)<\/pubDate>/) || [])[1] || '';
+      const source = (block.match(/<source[^>]*>([\s\S]*?)<\/source>/) || [])[1] || '';
+      items.push({
+        title: title.replace(/<!\[CDATA\[|\]\]>/g, '').trim(),
+        url: link.trim(),
+        source: source.replace(/<!\[CDATA\[|\]\]>/g, '').trim(),
+        published: pubDate ? new Date(pubDate) : new Date(),
+      });
+    }
+    return items;
+  } catch (e) {
+    console.log(`[Ken] News scrape failed "${query}": ${e.message}`);
+    return [];
+  }
+}
+
+// ── Polymarket cross-reference (public events API) ──
+async function scrapePolymarketEvents(limit = 100) {
+  try {
+    const url = `https://gamma-api.polymarket.com/events?closed=false&limit=${limit}&order=volume&ascending=false`;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 15000);
+    const res = await fetch(url, { signal: controller.signal });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const events = await res.json();
+    const results = [];
+    for (const evt of (events || [])) {
+      const mkts = (evt.markets || []).sort((a, b) => (b.volume || 0) - (a.volume || 0));
+      for (const m of mkts.slice(0, 3)) {
+        let price = null;
+        try { price = Math.round(parseFloat(JSON.parse(m.outcomePrices)[0]) * 100); } catch {}
+        if (price !== null && price > 0 && price < 100) {
+          results.push({
+            event_title: evt.title || '',
+            question: m.question || '',
+            price,
+            volume: Math.round(m.volume || 0),
+            slug: evt.slug || m.slug || '',
+          });
+        }
+      }
+    }
+    return results;
+  } catch (e) {
+    console.log(`[Ken] Polymarket scrape failed: ${e.message}`);
+    return [];
+  }
+}
+
+// ── Simple keyword sentiment scorer ──
+function scoreSentiment(text) {
+  const t = (text || '').toLowerCase();
+  const bullish = ['confirmed', 'likely', 'expected', 'approved', 'passed', 'agreed', 'winning', 'surge', 'soaring', 'breaking', 'imminent', 'certain', 'guarantee', 'official', 'announced', 'done deal'];
+  const bearish = ['unlikely', 'denied', 'rejected', 'failed', 'blocked', 'withdrawn', 'canceled', 'impossible', 'no chance', 'not going', 'walked back', 'reversed', 'postponed', 'delayed', 'collapse'];
+  const uncertain = ['maybe', 'could', 'might', 'rumor', 'speculation', 'sources say', 'unconfirmed', 'developing', 'unclear'];
+
+  let score = 0;
+  for (const w of bullish) if (t.includes(w)) score += 0.15;
+  for (const w of bearish) if (t.includes(w)) score -= 0.15;
+  for (const w of uncertain) if (t.includes(w)) score *= 0.7;
+  return Math.max(-1, Math.min(1, score));
+}
+
+// ── Map keywords in text to Kalshi event tickers ──
+function matchTextToMarkets(text, activeMarkets) {
+  const t = (text || '').toLowerCase();
+  const matches = [];
+  const keywords = {
+    'fed chair': 'KXFEDCHAIRNOM',
+    'federal reserve': 'KXFEDCHAIRNOM',
+    'kevin warsh': 'KXFEDCHAIRNOM',
+    'greenland': 'KXGREEN',
+    'cabinet': 'KXCABOUT',
+    'gabbard': 'KXCABOUT',
+    'tulsi': 'KXCABOUT',
+    'hegseth': 'KXCABOUT',
+    'defense secretary': 'KXTRUMPDEFCOUNT',
+    'insurrection act': 'KXINSURRECTION',
+    'supreme court': 'KXSCOTUS',
+    'seattle basketball': 'KXNBASEATTLE',
+    'seattle nba': 'KXNBASEATTLE',
+    '2028 election': 'KXPRESPERSON',
+    'presidential': 'KXPRESPERSON',
+  };
+  for (const [kw, prefix] of Object.entries(keywords)) {
+    if (t.includes(kw)) {
+      const related = activeMarkets.filter(m => m.event_ticker?.startsWith(prefix) || m.ticker?.startsWith(prefix));
+      for (const m of related) {
+        if (!matches.find(x => x.ticker === m.ticker)) matches.push(m);
+      }
+    }
+  }
+  return matches;
+}
+
+function generateSyntheticWeather(cities) {
+  const month = new Date().getMonth();
+  const results = [];
+  for (const city of cities) {
+    const coords = CITY_COORDS[city]; if (!coords) continue;
+    const baseTemp = [35,38,48,58,68,78,85,83,75,62,48,38][month] + (coords.lat < 35 ? 15 : coords.lat > 42 ? -8 : 0);
+    const temps = Array.from({length: 14}, (_, i) => baseTemp + Math.round((Math.random() - 0.5) * 12) + (i < 7 ? 3 : -2));
+    const precips = Array.from({length: 14}, () => Math.round(Math.random() * 60));
+    const winds = Array.from({length: 14}, () => 5 + Math.round(Math.random() * 15));
+    const mkStat = (arr) => ({ mean: arr.reduce((a,b)=>a+b)/arr.length, min: Math.min(...arr), max: Math.max(...arr), latest: arr[0] });
+    results.push({
+      city, synthetic: true,
+      temperature: { variable: 'temperature', unit: 'F', values: temps, ...mkStat(temps) },
+      precipitation: { variable: 'precipitation', unit: 'percent', values: precips, ...mkStat(precips) },
+      wind: { variable: 'wind', unit: 'mph', values: winds, ...mkStat(winds) },
+    });
+  }
+  return results;
+}
+
+// ── Hacker News scraper (top stories + search) ──
+// ── GDELT TV News mentions (covers CNN, MSNBC, Fox News, BBC captions) ──
+async function scrapeGDELTtv(query, station = 'CNN', limit = 15) {
+  try {
+    const url = `https://api.gdeltproject.org/api/v2/tv/tv?query=${encodeURIComponent(query)}%20station:${station}&mode=clipgallery&format=json&maxrecords=${limit}&last24hours=yes`;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 15000);
+    const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'Ken-Bot/2.0' } });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const text = await res.text();
+    if (!text.startsWith('{')) return []; // API returns error text, not JSON
+    const data = JSON.parse(text);
+    return (data.clips || []).map(c => ({
+      title: c.snippet || '',
+      url: c.preview_url || c.url || '',
+      source: c.station || 'TV News',
+      show: c.show || '',
+      date: c.date || '',
+    }));
+  } catch (e) {
+    return [];
+  }
+}
+
+// ── YouTube RSS search (public API, no key needed) ──
+async function scrapeYouTubeRSS(query) {
+  try {
+    const url = `https://www.youtube.com/results?search_query=${encodeURIComponent(query)}&sp=CAISBAgBEAE%253D`; // last hour filter
+    // YouTube doesn't have direct RSS search, use channel feeds for major news channels
+    const channels = [
+      { id: 'UCupvZG-5ko_eiXAupbDfxWw', name: 'CNN' },
+      { id: 'UCaXkIU1QidjPwiAYu6GcHjg', name: 'MSNBC' },
+      { id: 'UCXIJgqnII2ZOINSWNOGFThA', name: 'Fox News' },
+      { id: 'UC16niRr50-MSBwiO3YDb3RA', name: 'Bloomberg TV' },
+      { id: 'UCvJJ_dzjViJCoLf5uKUTwoA', name: 'CNBC' },
+      { id: 'UCknLrEdhRCp1aegoMqRaCZg', name: 'NBC News' },
+      { id: 'UCBi2mrWuNuyYy4gbM6fU18Q', name: 'ABC News' },
+      { id: 'UC8p1vwvWtl6T73JiExfWs1g', name: 'CBS News' },
+      { id: 'UC52X5wxOL_s5yw0dQk7NtgA', name: 'PBS NewsHour' },
+    ];
+    const results = [];
+    for (const ch of channels) {
+      try {
+        const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${ch.id}`;
+        const items = await scrapeRSSFeed(feedUrl, `YT ${ch.name}`, 5);
+        results.push(...items);
+      } catch {}
+    }
+    return results;
+  } catch { return []; }
+}
+
+// ── Lemmy (federated Reddit) — public JSON API, no auth needed ──
+async function scrapeLemmy(sort = 'Hot', limit = 20) {
+  const instances = ['lemmy.world', 'lemmy.ml', 'sh.itjust.works'];
+  const results = [];
+  for (const inst of instances) {
+    try {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 10000);
+      const res = await fetch(`https://${inst}/api/v3/post/list?sort=${sort}&limit=${limit}&type_=All`, {
+        signal: controller.signal,
+        headers: { 'Accept': 'application/json' },
+      });
+      clearTimeout(timeout);
+      if (!res.ok) continue;
+      const data = await res.json();
+      for (const p of (data.posts || [])) {
+        const post = p.post || {};
+        const counts = p.counts || {};
+        if (post.name && post.name.length > 10) {
+          results.push({
+            title: post.name.substring(0, 500),
+            url: post.ap_id || `https://${inst}/post/${post.id}`,
+            source: `Lemmy ${inst}`,
+            score: counts.score || 0,
+            num_comments: counts.comments || 0,
+          });
+        }
+      }
+      if (results.length > 0) break; // got data from one instance
+    } catch {}
+  }
+  return results;
+}
+
+// ── Lobsters (tech/politics news) — public JSON API ──
+async function scrapeLobsters(limit = 25) {
+  try {
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch('https://lobste.rs/hottest.json', { signal: controller.signal });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const stories = await res.json();
+    return stories.slice(0, limit).map(s => ({
+      title: s.title || '',
+      url: s.url || s.short_id_url || '',
+      source: 'Lobsters',
+      score: s.score || 0,
+      num_comments: s.comment_count || 0,
+      author: s.submitter_user?.username || '',
+      tags: (s.tags || []).join(', '),
+    }));
+  } catch { return []; }
+}
+
+// ── Tildes (curated discussion) — RSS feed ──
+async function scrapeTildes() {
+  try {
+    return await scrapeRSSFeed('https://tildes.net/topics.rss', 'Tildes', 20);
+  } catch { return []; }
+}
+
+// ── Manifold Markets (open prediction market — public API) ──
+async function scrapeManifold(queries) {
+  const results = [];
+  for (const q of queries) {
+    try {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 8000);
+      const res = await fetch(`https://manifold.markets/api/v0/search-markets?term=${encodeURIComponent(q)}&sort=liquidity&limit=10`, {
+        signal: controller.signal, headers: { 'Accept': 'application/json' },
+      });
+      clearTimeout(timeout);
+      if (!res.ok) continue;
+      const markets = await res.json();
+      for (const m of (markets || [])) {
+        if (m.question && m.probability != null && (m.volume || 0) > 100) {
+          results.push({
+            title: m.question,
+            url: m.url || `https://manifold.markets/${m.creatorUsername}/${m.slug}`,
+            source: 'Manifold',
+            probability: Math.round((m.probability || 0) * 100),
+            volume: Math.round(m.volume || 0),
+            liquidity: Math.round(m.totalLiquidity || 0),
+          });
+        }
+      }
+    } catch {}
+    await new Promise(r => setTimeout(r, 500));
+  }
+  return results;
+}
+
+// ── Mastodon/Fediverse trending (public API, no auth) ──
+async function scrapeMastodon() {
+  const instances = ['mastodon.social', 'mas.to'];
+  const results = [];
+  for (const inst of instances) {
+    try {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 8000);
+      const res = await fetch(`https://${inst}/api/v1/trends/statuses?limit=10`, {
+        signal: controller.signal,
+      });
+      clearTimeout(timeout);
+      if (!res.ok) continue;
+      const statuses = await res.json();
+      for (const s of statuses) {
+        const text = (s.content || '').replace(/<[^>]+>/g, '').trim();
+        if (text.length > 20) {
+          results.push({
+            title: text.substring(0, 500),
+            url: s.url || s.uri || '',
+            source: `Mastodon ${inst}`,
+            score: s.favourites_count || 0,
+            num_comments: s.replies_count || 0,
+          });
+        }
+      }
+    } catch {}
+  }
+  return results;
+}
+
+// ── Bluesky trending (public API — using feed generators, not searchPosts which is 403) ──
+async function scrapeBluesky() {
+  const feeds = [
+    { uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot', name: 'whats-hot' },
+    { uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/hot-classic', name: 'hot-classic' },
+  ];
+  const results = [];
+  for (const feed of feeds) {
+    try {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 10000);
+      const url = `https://public.api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=${encodeURIComponent(feed.uri)}&limit=20`;
+      const res = await fetch(url, { signal: controller.signal, headers: { 'Accept': 'application/json' } });
+      clearTimeout(timeout);
+      if (!res.ok) continue;
+      const data = await res.json();
+      for (const item of (data.feed || [])) {
+        const p = item.post || {};
+        const text = (p.record?.text || '').trim();
+        if (text.length > 20) {
+          results.push({
+            title: text.substring(0, 500),
+            url: `https://bsky.app/profile/${p.author?.handle}/post/${p.uri?.split('/').pop()}`,
+            source: `Bluesky ${feed.name}`,
+            author: p.author?.handle || '',
+            score: (p.likeCount || 0) + (p.repostCount || 0),
+            num_comments: p.replyCount || 0,
+          });
+        }
+      }
+    } catch {}
+  }
+  return results;
+}
+
+async function scrapeHackerNews(query, limit = 15) {
+  try {
+    const url = query
+      ? `https://hn.algolia.com/api/v1/search_by_date?query=${encodeURIComponent(query)}&tags=story&hitsPerPage=${limit}`
+      : `https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=${limit}`;
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch(url, { signal: controller.signal });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const data = await res.json();
+    return (data.hits || []).map(h => ({
+      title: h.title || '',
+      url: h.url || `https://news.ycombinator.com/item?id=${h.objectID}`,
+      score: h.points || 0,
+      num_comments: h.num_comments || 0,
+      created: new Date(h.created_at),
+      source: 'hackernews',
+    }));
+  } catch (e) {
+    console.log(`[Ken] HN scrape failed: ${e.message}`);
+    return [];
+  }
+}
+
+// ── AP News RSS scraper ──
+// ── Generic RSS feed scraper ──
+async function scrapeRSSFeed(url, source, limit = 15) {
+  try {
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch(url, {
+      signal: controller.signal,
+      headers: { 'User-Agent': 'Ken-Bot/2.0 (RSS Reader)' },
+    });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const xml = await res.text();
+    const items = [];
+    // Handle both <item> (RSS) and <entry> (Atom) formats
+    const itemRegex = /<(?:item|entry)>([\s\S]*?)<\/(?:item|entry)>/g;
+    let match;
+    while ((match = itemRegex.exec(xml)) && items.length < limit) {
+      const block = match[1];
+      const title = (block.match(/<title[^>]*>([\s\S]*?)<\/title>/) || [])[1] || '';
+      const link = (block.match(/<link[^>]*href="([^"]+)"/) || block.match(/<link>([\s\S]*?)<\/link>/) || [])[1] || '';
+      const cleanTitle = title.replace(/<!\[CDATA\[|\]\]>/g, '').replace(/<[^>]+>/g, '').trim();
+      if (cleanTitle && cleanTitle.length > 10) {
+        items.push({ title: cleanTitle, url: link.trim(), source });
+      }
+    }
+    return items;
+  } catch { return []; }
+}
+
+// ── All news channel feeds ──
+const NEWS_FEEDS = [
+  // ═══ POLITICS & POLICY (Kalshi political markets) ═══
+  { url: 'https://feeds.bbci.co.uk/news/world/rss.xml', source: 'BBC World' },
+  { url: 'https://feeds.bbci.co.uk/news/politics/rss.xml', source: 'BBC Politics' },
+  { url: 'https://feeds.bbci.co.uk/news/us_and_canada/rss.xml', source: 'BBC US' },
+  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Politics.xml', source: 'NYT Politics' },
+  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/World.xml', source: 'NYT World' },
+  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/US.xml', source: 'NYT US' },
+  { url: 'https://feeds.washingtonpost.com/rss/politics', source: 'WaPo Politics' },
+  { url: 'https://feeds.washingtonpost.com/rss/world', source: 'WaPo World' },
+  { url: 'https://www.politico.com/rss/politicopicks.xml', source: 'Politico' },
+  { url: 'https://www.politico.com/rss/congress.xml', source: 'Politico Congress' },
+  { url: 'https://thehill.com/feed/', source: 'The Hill' },
+  { url: 'https://feeds.npr.org/1001/rss.xml', source: 'NPR News' },
+  { url: 'https://feeds.npr.org/1014/rss.xml', source: 'NPR Politics' },
+  { url: 'https://www.aljazeera.com/xml/rss/all.xml', source: 'Al Jazeera' },
+  { url: 'https://rss.dw.com/xml/rss-en-all', source: 'DW News' },
+  { url: 'https://feeds.skynews.com/feeds/rss/world.xml', source: 'Sky News World' },
+  { url: 'https://feeds.skynews.com/feeds/rss/us.xml', source: 'Sky News US' },
+  { url: 'https://www.theguardian.com/us-news/rss', source: 'Guardian US' },
+  { url: 'https://www.theguardian.com/world/rss', source: 'Guardian World' },
+  { url: 'https://rss.cbc.ca/lineup/world.xml', source: 'CBC World' },
+  { url: 'https://feeds.abcnews.com/abcnews/politicsheadlines', source: 'ABC Politics' },
+  { url: 'http://feeds.foxnews.com/foxnews/politics', source: 'Fox Politics' },
+  { url: 'https://www.axios.com/feeds/feed.rss', source: 'Axios' },
+  { url: 'https://www.vox.com/rss/index.xml', source: 'Vox' },
+
+  // ═══ ECONOMICS & FINANCIAL MARKETS (Kalshi econ/rate markets) ═══
+  { url: 'https://feeds.bloomberg.com/politics/news.rss', source: 'Bloomberg Politics' },
+  { url: 'https://feeds.bloomberg.com/markets/news.rss', source: 'Bloomberg Markets' },
+  { url: 'https://www.cnbc.com/id/10000664/device/rss/rss.html', source: 'CNBC Top' },
+  { url: 'https://www.cnbc.com/id/10001147/device/rss/rss.html', source: 'CNBC Economy' },
+  { url: 'https://search.cnbc.com/rs/search/combinedcms/view.xml?partnerId=wrss01&id=15839069', source: 'CNBC Politics' },
+  { url: 'https://www.cnbc.com/id/20910258/device/rss/rss.html', source: 'CNBC Finance' },
+  { url: 'https://feeds.marketwatch.com/marketwatch/topstories/', source: 'MarketWatch' },
+  { url: 'https://feeds.marketwatch.com/marketwatch/marketpulse/', source: 'MarketWatch Pulse' },
+  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Business.xml', source: 'NYT Business' },
+  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Economy.xml', source: 'NYT Economy' },
+  { url: 'https://www.ft.com/rss/home/us', source: 'FT US' },
+  { url: 'https://www.investing.com/rss/news.rss', source: 'Investing.com' },
+  { url: 'https://www.federalreserve.gov/feeds/press_all.xml', source: 'Fed Reserve' },
+  { url: 'https://www.bis.org/doclist/bis_fsi_publs.rss', source: 'BIS' },
+  { url: 'https://seekingalpha.com/feed.xml', source: 'Seeking Alpha' },
+  { url: 'https://finance.yahoo.com/news/rssindex', source: 'Yahoo Finance' },
+
+  // ═══ CRYPTO & PREDICTION MARKETS (cross-ref with Polymarket) ═══
+  { url: 'https://cointelegraph.com/rss', source: 'CoinTelegraph' },
+  { url: 'https://www.coindesk.com/arc/outboundfeeds/rss/', source: 'CoinDesk' },
+  { url: 'https://decrypt.co/feed', source: 'Decrypt' },
+  { url: 'https://thedefiant.io/feed', source: 'The Defiant' },
+
+  // ═══ PREDICTION & POLLING ANALYSIS ═══
+  { url: 'https://fivethirtyeight.com/features/feed/', source: 'FiveThirtyEight' },
+  { url: 'https://www.realclearpolitics.com/index.xml', source: 'RealClearPolitics' },
+  { url: 'https://electionbettingodds.com/feed', source: 'ElectionBettingOdds' },
+  { url: 'https://www.predictit.org/feed', source: 'PredictIt' },
+
+  // ═══ TECH & AI POLICY (Kalshi tech markets) ═══
+  { url: 'https://www.theverge.com/rss/index.xml', source: 'The Verge' },
+  { url: 'https://techcrunch.com/feed/', source: 'TechCrunch' },
+  { url: 'https://arstechnica.com/feed/', source: 'Ars Technica' },
+  { url: 'https://www.wired.com/feed/rss', source: 'WIRED' },
+  { url: 'https://feeds.feedburner.com/venturebeat/SZYF', source: 'VentureBeat' },
+
+  // ═══ WEATHER & CLIMATE (Kalshi weather markets - temps, snow, etc.) ═══
+  { url: 'https://www.weather.gov/rss_page.php?site_name=nws', source: 'NWS' },
+  { url: 'https://weather.com/feeds/enhancedconditions/rss', source: 'Weather.com' },
+  { url: 'https://www.accuweather.com/en/us/national/current-weather/rss', source: 'AccuWeather' },
+  { url: 'https://www.climate.gov/rss.xml', source: 'NOAA Climate' },
+
+  // ═══ SPORTS (Kalshi has sports-adjacent markets) ═══
+  { url: 'https://www.espn.com/espn/rss/news', source: 'ESPN' },
+  { url: 'https://rss.nytimes.com/services/xml/rss/nyt/Sports.xml', source: 'NYT Sports' },
+
+  // ═══ ENTERTAINMENT & CULTURE (Kalshi awards/culture markets) ═══
+  { url: 'https://variety.com/feed/', source: 'Variety' },
+  { url: 'https://deadline.com/feed/', source: 'Deadline' },
+  { url: 'https://www.hollywoodreporter.com/feed/', source: 'Hollywood Reporter' },
+
+  // ═══ ENERGY & COMMODITIES (Kalshi gas/oil markets) ═══
+  { url: 'https://oilprice.com/rss/main', source: 'OilPrice' },
+  { url: 'https://www.eia.gov/rss/todayinenergy.xml', source: 'EIA Energy' },
+
+  // ═══ DEFENSE & GEOPOLITICS (conflict/territory markets) ═══
+  { url: 'https://www.defensenews.com/arc/outboundfeeds/rss/category/global/?outputType=xml', source: 'Defense News' },
+  { url: 'https://foreignpolicy.com/feed/', source: 'Foreign Policy' },
+  { url: 'https://warontherocks.com/feed/', source: 'War On The Rocks' },
+
+  // ═══ SCIENCE & SPACE (Kalshi space markets) ═══
+  { url: 'https://www.nasa.gov/rss/dyn/breaking_news.rss', source: 'NASA' },
+  { url: 'https://www.space.com/feeds/all', source: 'Space.com' },
+  { url: 'https://www.nature.com/nature.rss', source: 'Nature' },
+  { url: 'https://www.science.org/rss/news_current.xml', source: 'Science' },
+];
+
+async function scrapeAllNewsFeeds() {
+  const allArticles = [];
+  // Process feeds in batches of 15 concurrently for speed (maxed for 16 cores)
+  const batchSize = 15;
+  for (let i = 0; i < NEWS_FEEDS.length; i += batchSize) {
+    const batch = NEWS_FEEDS.slice(i, i + batchSize);
+    const results = await Promise.allSettled(
+      batch.map(feed => scrapeRSSFeed(feed.url, feed.source, 8))
+    );
+    for (const result of results) {
+      if (result.status === 'fulfilled') allArticles.push(...result.value);
+    }
+    await new Promise(r => setTimeout(r, 100)); // minimal pause between batches
+  }
+  return allArticles;
+}
+
+// ── Metaculus predictions scraper (prediction market cross-ref) ──
+async function scrapeMetaculus() {
+  try {
+    const url = 'https://www.metaculus.com/api2/questions/?limit=20&order_by=-activity&status=open&type=forecast';
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch(url, { headers: { 'Accept': 'application/json' }, signal: controller.signal });
+    clearTimeout(timeout);
+    if (!res.ok) return [];
+    const data = await res.json();
+    return (data.results || []).map(q => ({
+      title: q.title || '',
+      url: `https://www.metaculus.com/questions/${q.id}`,
+      community_prediction: q.community_prediction?.full?.q2,
+      num_predictions: q.number_of_predictions || 0,
+      source: 'metaculus',
+    }));
+  } catch (e) {
+    console.log(`[Ken] Metaculus scrape failed: ${e.message}`);
+    return [];
+  }
+}
+
+// ── PredictIt API (another prediction market for cross-reference) ──
+async function scrapePredictIt() {
+  try {
+    const controller = new AbortController();
+    setTimeout(() => controller.abort(), 8000);
+    const res = await fetch('https://www.predictit.org/api/marketdata/all/', {
+      signal: controller.signal,
+      headers: { 'User-Agent': 'Ken-Bot/1.0' }
+    });
+    if (!res.ok) return [];
+    const data = await res.json();
+    return (data.markets || []).slice(0, 100).map(m => ({
+      title: m.name,
+      url: m.url || `https://www.predictit.org/markets/detail/${m.id}`,
+      score: Math.round((m.contracts?.[0]?.lastTradePrice || 0) * 100),
+      num_comments: m.contracts?.length || 0,
+      source: 'predictit',
+      extra: JSON.stringify({
+        contracts: (m.contracts || []).slice(0, 5).map(c => ({
+          name: c.name, lastPrice: c.lastTradePrice, bestBuy: c.bestBuyYesCost, bestSell: c.bestSellYesCost
+        }))
+      })
+    }));
+  } catch (e) {
+    console.log(`[Ken] PredictIt scrape failed: ${e.message}`);
+    return [];
+  }
+}
+
+// ── FRED Economic Data API (real economic indicators — no key needed for some endpoints) ──
+async function scrapeFredIndicators() {
+  const indicators = [
+    { series: 'CPIAUCSL', name: 'CPI Inflation' },
+    { series: 'UNRATE', name: 'Unemployment Rate' },
+    { series: 'GDP', name: 'GDP Growth' },
+    { series: 'FEDFUNDS', name: 'Federal Funds Rate' },
+    { series: 'T10Y2Y', name: '10Y-2Y Treasury Spread' },
+    { series: 'VIXCLS', name: 'VIX Volatility Index' },
+    { series: 'DGS10', name: '10-Year Treasury Yield' },
+    { series: 'DCOILWTICO', name: 'WTI Crude Oil Price' },
+  ];
+  const results = [];
+  for (const ind of indicators) {
+    try {
+      const controller = new AbortController();
+      setTimeout(() => controller.abort(), 5000);
+      const res = await fetch(`https://fred.stlouisfed.org/graph/fredgraph.csv?id=${ind.series}&cosd=${new Date(Date.now() - 90*86400000).toISOString().split('T')[0]}`, {
+        signal: controller.signal,
+        headers: { 'User-Agent': 'Ken-Bot/1.0' }
+      });
+      if (!res.ok) continue;
+      const csv = await res.text();
+      const lines = csv.trim().split('\n');
+      if (lines.length < 2) continue;
+      const lastLine = lines[lines.length - 1];
+      const [date, value] = lastLine.split(',');
+      if (value && value !== '.') {
+        results.push({
+          title: `${ind.name}: ${value} (${date})`,
+          url: `https://fred.stlouisfed.org/series/${ind.series}`,
+          score: Math.round(parseFloat(value) * 100) || 0,
+          source: 'fred_economic',
+          keyword: ind.series,
+        });
+      }
+    } catch {}
+  }
+  return results;
+}
+
+// ── NWS Active Weather Alerts (real severe weather data for weather markets) ──
+async function scrapeNWSAlerts() {
+  try {
+    const controller = new AbortController();
+    setTimeout(() => controller.abort(), 8000);
+    const res = await fetch('https://api.weather.gov/alerts/active?status=actual&message_type=alert&limit=50', {
+      signal: controller.signal,
+      headers: { 'User-Agent': 'Ken-Bot/1.0 (ken@designerwallcoverings.com)', 'Accept': 'application/geo+json' }
+    });
+    if (!res.ok) return [];
+    const data = await res.json();
+    return (data.features || []).map(f => ({
+      title: `${f.properties.event}: ${f.properties.headline || f.properties.description?.substring(0, 200) || ''}`.substring(0, 500),
+      url: f.properties['@id'] || 'https://alerts.weather.gov',
+      score: f.properties.severity === 'Extreme' ? 100 : f.properties.severity === 'Severe' ? 75 : f.properties.severity === 'Moderate' ? 50 : 25,
+      num_comments: 0,
+      source: 'nws_alerts',
+      keyword: f.properties.event || 'weather',
+    }));
+  } catch (e) {
+    console.log(`[Ken] NWS Alerts failed: ${e.message}`);
+    return [];
+  }
+}
+
+// ── Congressional/Government Activity (GovTrack API) ──
+async function scrapeGovTrack() {
+  try {
+    const controller = new AbortController();
+    setTimeout(() => controller.abort(), 8000);
+    const res = await fetch('https://www.govtrack.us/api/v2/vote?order_by=-created&limit=20', {
+      signal: controller.signal,
+      headers: { 'User-Agent': 'Ken-Bot/1.0' }
+    });
+    if (!res.ok) return [];
+    const data = await res.json();
+    return (data.objects || []).map(v => ({
+      title: `${v.chamber === 'senate' ? 'Senate' : 'House'} Vote: ${v.question || v.result || 'Unknown'}`.substring(0, 500),
+      url: `https://www.govtrack.us${v.link || ''}`,
+      score: v.total_plus || 0,
+      num_comments: v.total_minus || 0,
+      source: 'govtrack',
+      keyword: v.category || 'legislation',
+    }));
+  } catch (e) {
+    console.log(`[Ken] GovTrack failed: ${e.message}`);
+    return [];
+  }
+}
+
+// ── X/Twitter via Nitter instances (public, no auth needed) ──
+async function scrapeNitterTrending() {
+  const nitterInstances = [
+    'https://nitter.poast.org',
+    'https://nitter.privacydev.net',
+    'https://nitter.cz',
+  ];
+  const searchTerms = ['kalshi', 'polymarket', 'prediction market', 'fed rate decision', 'hurricane season'];
+  const results = [];
+  for (const term of searchTerms) {
+    for (const instance of nitterInstances) {
+      try {
+        const controller = new AbortController();
+        setTimeout(() => controller.abort(), 6000);
+        const url = `${instance}/search?f=tweets&q=${encodeURIComponent(term)}&since=`;
+        const res = await fetch(url, { signal: controller.signal, headers: { 'User-Agent': 'Mozilla/5.0' } });
+        if (!res.ok) continue;
+        const html = await res.text();
+        // Extract tweet text from Nitter HTML
+        const tweetMatches = html.match(/<div class="tweet-content[^"]*"[^>]*>([\s\S]*?)<\/div>/g) || [];
+        for (const match of tweetMatches.slice(0, 5)) {
+          const text = match.replace(/<[^>]+>/g, '').trim().substring(0, 500);
+          if (text.length > 10) {
+            results.push({
+              title: text,
+              url: `${instance}/search?q=${encodeURIComponent(term)}`,
+              score: 0,
+              num_comments: 0,
+              source: 'twitter_nitter',
+              keyword: term,
+            });
+          }
+        }
+        break; // One working instance per term is enough
+      } catch {} // Nitter instances are unreliable, fail silently
+    }
+    await new Promise(r => setTimeout(r, 2000));
+  }
+  return results;
+}
+
+// ── Discord Bot Intelligence (reads messages from joined servers) ──
+const DISCORD_BOT_TOKEN = 'MTQ3MjYyNDg2OTk3NDg2ODA2Mg.GifeyF.C5l9KqQUia-Ex54Ldg23fuxislxwf2QtmkXYII';
+const DISCORD_API = 'https://discord.com/api/v10';
+
+// Channel names to monitor (matched case-insensitive) — covers all Kalshi/Polymarket categories
+const DISCORD_CHANNEL_KEYWORDS = [
+  // Core
+  'general', 'chat', 'discussion', 'lounge', 'main',
+  // Trading & Markets
+  'trading', 'markets', 'predictions', 'analysis', 'strategy', 'signals', 'alerts',
+  'arbitrage', 'polymarket', 'kalshi', 'bets', 'picks', 'plays', 'odds',
+  // News & Facts
+  'news', 'breaking', 'headlines', 'updates', 'osint', 'intel', 'events',
+  // Weather
+  'weather', 'hurricane', 'tropical', 'storm', 'forecast', 'severe', 'climate',
+  // Politics & Elections
+  'politics', 'election', 'polling', 'congress', 'senate', 'trump', 'biden',
+  'policy', 'legislation', 'vote', 'debate',
+  // Economics & Finance
+  'economics', 'economy', 'finance', 'stocks', 'fed', 'inflation', 'rates',
+  'macro', 'earnings', 'options', 'futures', 'bonds',
+  // Sports
+  'sports', 'nfl', 'nba', 'mlb', 'nhl', 'soccer', 'football', 'basketball',
+  'baseball', 'ufc', 'mma', 'boxing', 'f1', 'golf', 'tennis',
+  // Crypto
+  'crypto', 'bitcoin', 'btc', 'ethereum', 'eth', 'defi', 'altcoin', 'solana',
+  // Geopolitics
+  'geopolitics', 'ukraine', 'russia', 'china', 'military', 'conflict', 'war', 'nato',
+  // Entertainment
+  'entertainment', 'movies', 'film', 'oscars', 'grammys', 'awards', 'box-office',
+  'music', 'tv', 'streaming', 'celebrity',
+  // Science & Space
+  'space', 'spacex', 'nasa', 'launch', 'science', 'rocket',
+  // Tech & AI
+  'tech', 'ai', 'artificial', 'openai', 'gpt', 'machine-learning', 'nvidia',
+];
+
+async function discordFetch(endpoint) {
+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), 10000);
+  const res = await fetch(`${DISCORD_API}${endpoint}`, {
+    headers: { Authorization: `Bot ${DISCORD_BOT_TOKEN}`, 'Content-Type': 'application/json' },
+    signal: controller.signal,
+  });
+  clearTimeout(timeout);
+  if (!res.ok) {
+    if (res.status === 429) {
+      const retry = await res.json().catch(() => ({}));
+      const wait = (retry.retry_after || 5) * 1000;
+      console.log(`[Ken] Discord rate limited, waiting ${wait}ms`);
+      await new Promise(r => setTimeout(r, wait));
+      return null;
+    }
+    return null;
+  }
+  return res.json();
+}
+
+async function scrapeDiscord(limit = 50) {
+  const results = [];
+  try {
+    // 1. Get all guilds the bot is in
+    const guilds = await discordFetch('/users/@me/guilds');
+    if (!guilds || !Array.isArray(guilds)) {
+      console.log('[Ken] Discord: no guilds found (bot may not have joined any servers yet)');
+      return [];
+    }
+    console.log(`[Ken] Discord: bot is in ${guilds.length} server(s): ${guilds.map(g => g.name).join(', ')}`);
+
+    for (const guild of guilds) {
+      // 2. Get channels for this guild
+      const channels = await discordFetch(`/guilds/${guild.id}/channels`);
+      if (!channels || !Array.isArray(channels)) continue;
+
+      // 3. Filter to text channels matching keywords
+      const textChannels = channels.filter(ch => {
+        if (ch.type !== 0) return false; // type 0 = text channel
+        const name = (ch.name || '').toLowerCase();
+        return DISCORD_CHANNEL_KEYWORDS.some(kw => name.includes(kw));
+      });
+
+      console.log(`[Ken] Discord [${guild.name}]: monitoring ${textChannels.length}/${channels.filter(c => c.type === 0).length} text channels`);
+
+      for (const channel of textChannels.slice(0, 15)) { // cap at 15 channels per guild
+        try {
+          const messages = await discordFetch(`/channels/${channel.id}/messages?limit=${Math.min(limit, 50)}`);
+          if (!messages || !Array.isArray(messages)) continue;
+
+          // Only messages from last 2 hours (fresh intel)
+          const cutoff = Date.now() - (2 * 60 * 60 * 1000);
+          for (const msg of messages) {
+            const msgTime = new Date(msg.timestamp).getTime();
+            if (msgTime < cutoff) continue;
+            if (!msg.content || msg.content.length < 20) continue;
+            if (msg.author?.bot) continue; // skip bot messages
+
+            results.push({
+              title: msg.content.substring(0, 500),
+              url: `https://discord.com/channels/${guild.id}/${channel.id}/${msg.id}`,
+              source: `Discord ${guild.name}`,
+              channel: channel.name,
+              author: msg.author?.username || 'unknown',
+              score: (msg.reactions || []).reduce((sum, r) => sum + (r.count || 0), 0),
+              num_comments: msg.referenced_message ? 1 : 0, // thread indicator
+              created: new Date(msg.timestamp),
+            });
+          }
+          // Rate limit: 500ms between channel reads
+          await new Promise(r => setTimeout(r, 500));
+        } catch (e) {
+          console.log(`[Ken] Discord channel ${channel.name} error: ${e.message}`);
+        }
+      }
+      await new Promise(r => setTimeout(r, 1000)); // between guilds
+    }
+    console.log(`[Ken] Discord: ${results.length} messages from ${guilds.length} server(s)`);
+  } catch (e) {
+    console.log(`[Ken] Discord scrape failed: ${e.message}`);
+  }
+  return results;
+}
+
+// ══════════════════════════════════════════════
+// INTELLIGENCE LOOP (every 30 min) — Reddit, HN, News, Metaculus, Discord
+// ══════════════════════════════════════════════
+function logMemory(label) {
+  const mem = process.memoryUsage();
+  const rss = Math.round(mem.rss / 1024 / 1024);
+  const heap = Math.round(mem.heapUsed / 1024 / 1024);
+  if (rss > 500) console.log(`[Ken] MEM ${label}: RSS=${rss}MB Heap=${heap}MB`);
+}
+
+async function redditIntelligenceLoop() {
+  console.log('[Ken] Running Reddit intelligence sweep...');
+  logMemory('sweep-start');
+  let totalPosts = 0;
+  try {
+    // Get tracking config from DB
+    const { rows: trackers } = await kenQ('SELECT * FROM ken_reddit_tracking WHERE enabled = true');
+
+    // Process tracked subreddits in parallel batches of 12 (maxed for 16 cores)
+    const TRACKER_BATCH = 12;
+    for (let i = 0; i < trackers.length; i += TRACKER_BATCH) {
+      const batch = trackers.slice(i, i + TRACKER_BATCH);
+      const results = await Promise.allSettled(
+        batch.map(tracker => scrapeReddit(tracker.subreddit, tracker.search_term, 20)
+          .then(posts => ({ tracker, posts })))
+      );
+      for (const result of results) {
+        if (result.status !== 'fulfilled') continue;
+        const { tracker, posts } = result.value;
+        for (const post of posts) {
+          const sentiment = scoreSentiment(post.title + ' ' + post.selftext);
+          if (post.score > 2 || post.num_comments > 1) {
+            await kenQ(`
+              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, relevance_ticker, raw_text)
+              VALUES ('reddit', $1, $2, $3, $4, $5, $6, $7, $8, $9)
+              ON CONFLICT DO NOTHING
+            `, [
+              tracker.subreddit, tracker.search_term, post.title, post.url,
+              post.score, post.num_comments, sentiment,
+              (tracker.related_tickers || [])[0] || null,
+              (post.selftext || '').substring(0, 1000),
+            ]);
+            totalPosts++;
+          }
+        }
+      }
+      await new Promise(r => setTimeout(r, 500)); // 0.5s between batches (was 1.5s)
+    }
+
+    // Also scrape hot posts from key subreddits for general intelligence
+    // Categories: Weather | Politics | Economics | Sports | Crypto | Geopolitics | Entertainment | Science/Space | Tech/AI | General Sentiment
+    const hotSubs = [
+      // ── Core prediction markets ──
+      'Kalshi', 'Polymarket', 'PredictionMarket', 'sportsbook',
+      // ── Weather (hurricane/temp/storm markets) ──
+      'TropicalWeather', 'weather', 'tornado', 'hurricane', 'climate',
+      'NaturalGas', 'energy', 'propane', 'HomeImprovement',
+      // ── Heating Bills & Utility Costs ──
+      'personalfinance', 'Frugal', 'povertyfinance', 'homeowners',
+      'HVAC', 'utilities', 'electricvehicles',
+      // ── Politics & Elections ──
+      'politics', 'Conservative', 'Liberal', 'NeutralPolitics', 'PoliticalDiscussion',
+      'fivethirtyeight', 'elections', 'supremecourt', 'moderatepolitics', 'law',
+      // ── Economics & Finance ──
+      'economics', 'wallstreetbets', 'economy', 'inflation', 'FederalReserve',
+      'StockMarket', 'investing', 'RealEstate', 'finance', 'commodities',
+      // ── Sports ──
+      'nfl', 'nba', 'baseball', 'soccer', 'MMA', 'CFB', 'CollegeBasketball',
+      'formula1', 'golf', 'hockey', 'tennis', 'boxing',
+      // ── Crypto ──
+      'CryptoMarkets', 'Bitcoin', 'ethereum', 'CryptoCurrency', 'BitcoinMarkets', 'solana',
+      // ── Geopolitics ──
+      'worldnews', 'geopolitics', 'UkrainianConflict', 'ukraine', 'China',
+      'MiddleEastNews', 'foreignpolicy', 'military',
+      // ── Entertainment (awards/box office markets) ──
+      'movies', 'boxoffice', 'television', 'Oscars', 'Music', 'entertainment',
+      // ── Science & Space (launch/milestone markets) ──
+      'SpaceX', 'space', 'nasa', 'science', 'Futurology',
+      // ── Tech & AI ──
+      'technology', 'artificial', 'MachineLearning', 'OpenAI', 'singularity', 'Nvidia',
+      // ── General sentiment ──
+      'news', 'OutOfTheLoop', 'AskReddit', 'TrueReddit', 'dataisbeautiful',
+    ];
+    // Process subreddits in parallel batches of 12 (maxed for 16 cores)
+    const REDDIT_BATCH = 12;
+    for (let i = 0; i < hotSubs.length; i += REDDIT_BATCH) {
+      const batch = hotSubs.slice(i, i + REDDIT_BATCH);
+      const results = await Promise.allSettled(batch.map(sub => scrapeSubredditHot(sub, 8)));
+      for (let j = 0; j < results.length; j++) {
+        if (results[j].status !== 'fulfilled') continue;
+        const sub = batch[j];
+        for (const post of results[j].value) {
+          if (post.score > 30) { // lower threshold to catch more sentiment signals
+            const sentiment = scoreSentiment(post.title);
+            await kenQ(`
+              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+              VALUES ('reddit_hot', $1, 'hot', $2, $3, $4, $5, $6, $7)
+              ON CONFLICT DO NOTHING
+            `, [sub, post.title, post.url, post.score, post.num_comments, sentiment, '']);
+            totalPosts++;
+          }
+        }
+      }
+      await new Promise(r => setTimeout(r, 500)); // 0.5s between batches (was 1.5s)
+    }
+    console.log(`[Ken] Reddit hot: ${hotSubs.length} subreddits across 10 categories`);
+    logMemory('after-reddit');
+    if (global.gc) global.gc();
+
+    // Scrape Google News for top market topics (covers all Kalshi categories)
+    const newsQueries = [
+      // Politics & elections
+      'trump cabinet resign', 'federal reserve chair nomination', '2028 presidential election',
+      'trump executive order', 'government shutdown', 'supreme court ruling',
+      // Economics
+      'CPI inflation report', 'federal reserve rate decision', 'jobs report unemployment',
+      'GDP growth', 'recession forecast',
+      // Weather
+      'hurricane forecast atlantic', 'severe weather outbreak', 'temperature record',
+      // Geopolitics
+      'ukraine russia ceasefire', 'china taiwan', 'middle east conflict',
+      // Crypto
+      'bitcoin price prediction', 'SEC crypto regulation',
+      // Entertainment
+      'oscar nominations', 'box office weekend',
+      // Space
+      'SpaceX launch', 'NASA artemis',
+    ];
+    // Process Google News in batches of 5 (21 serial × 1s = too slow)
+    const NEWS_QUERY_BATCH = 5;
+    for (let i = 0; i < newsQueries.length; i += NEWS_QUERY_BATCH) {
+      const batch = newsQueries.slice(i, i + NEWS_QUERY_BATCH);
+      const results = await Promise.allSettled(
+        batch.map(nq => scrapeGoogleNews(nq, 5).then(articles => ({ nq, articles })))
+      );
+      for (const result of results) {
+        if (result.status !== 'fulfilled') continue;
+        const { nq, articles } = result.value;
+        for (const article of articles) {
+          const sentiment = scoreSentiment(article.title);
+          await kenQ(`
+            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score)
+            VALUES ('google_news', $1, $2, $3, $4, 0, $5)
+            ON CONFLICT DO NOTHING
+          `, [article.source || 'news', nq, article.title, article.url, sentiment]);
+          totalPosts++;
+        }
+      }
+      await new Promise(r => setTimeout(r, 1000));
+    }
+
+    // ── Hacker News (tech/policy sentiment) ──
+    const hnQueries = ['trump policy', 'federal reserve', 'tariff', 'ukraine russia', null]; // null = front page
+    for (const hq of hnQueries) {
+      try {
+        const stories = await scrapeHackerNews(hq, 10);
+        for (const story of stories) {
+          if (story.score > 5 || story.num_comments > 3) {
+            const sentiment = scoreSentiment(story.title);
+            await kenQ(`
+              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+              VALUES ('hackernews', 'hackernews', $1, $2, $3, $4, $5, $6, '')
+              ON CONFLICT DO NOTHING
+            `, [hq || 'front_page', story.title, story.url, story.score, story.num_comments, sentiment]);
+            totalPosts++;
+          }
+        }
+      } catch {}
+      await new Promise(r => setTimeout(r, 1500));
+    }
+
+    // ── All News Channels (BBC, NYT, WaPo, Politico, CNBC, Bloomberg, etc.) ──
+    try {
+      let newsStored = 0;
+      // Process feeds in small batches to limit memory — don't hold all articles at once
+      const batchSize = 5;
+      for (let i = 0; i < NEWS_FEEDS.length; i += batchSize) {
+        const batch = NEWS_FEEDS.slice(i, i + batchSize);
+        const results = await Promise.allSettled(
+          batch.map(feed => scrapeRSSFeed(feed.url, feed.source, 6))
+        );
+        for (const result of results) {
+          if (result.status !== 'fulfilled') continue;
+          for (const article of result.value) {
+            const sentiment = scoreSentiment(article.title);
+            await kenQ(`
+              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
+              VALUES ($1, $2, 'news', $3, $4, 0, $5, '')
+              ON CONFLICT DO NOTHING
+            `, [article.source.toLowerCase().replace(/\s+/g, '_'), article.source, article.title, article.url, sentiment]);
+            newsStored++;
+          }
+        }
+        await new Promise(r => setTimeout(r, 300));
+      }
+      totalPosts += newsStored;
+      console.log(`[Ken] News channels: ${newsStored} articles from ${NEWS_FEEDS.length} feeds`);
+    } catch (e) {
+      console.log(`[Ken] News channels failed: ${e.message}`);
+    }
+    logMemory('after-news');
+    if (global.gc) global.gc();
+
+    // ── Metaculus (prediction market community consensus) ──
+    try {
+      const metaQuestions = await scrapeMetaculus();
+      for (const mq of metaQuestions) {
+        const sentiment = scoreSentiment(mq.title);
+        const predText = mq.community_prediction ? `Community: ${Math.round(mq.community_prediction * 100)}%` : '';
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('metaculus', 'metaculus', 'predictions', $1, $2, $3, $4, $5, $6)
+          ON CONFLICT DO NOTHING
+        `, [mq.title, mq.url, mq.num_predictions || 0, 0, sentiment, predText]);
+        totalPosts++;
+      }
+      console.log(`[Ken] Metaculus: ${metaQuestions.length} predictions`);
+    } catch (e) {
+      console.log(`[Ken] Metaculus failed: ${e.message}`);
+    }
+
+    // ── Manifold Markets (cross-reference prediction probabilities) ──
+    let manifoldTotal = 0;
+    try {
+      const manifoldQueries = ['trump', 'fed rate', 'election 2028', 'ukraine', 'tariff'];
+      const manifoldMkts = await scrapeManifold(manifoldQueries);
+      for (const m of manifoldMkts) {
+        const sentiment = scoreSentiment(m.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('manifold', 'manifold', $1, $2, $3, $4, 0, $5, $6)
+          ON CONFLICT DO NOTHING
+        `, [m.source, m.title, m.url, m.volume || 0, sentiment, `prob:${m.probability}% vol:${m.volume}`]);
+        manifoldTotal++;
+      }
+    } catch {}
+    console.log(`[Ken] Manifold Markets: ${manifoldTotal} prediction markets`);
+
+    // ── Kalshi & Polymarket Reddit search (community sentiment on specific trades) ──
+    let predMarketRedditTotal = 0;
+    const pmSearchTerms = ['arbitrage', 'free money', 'mispriced', 'edge'];
+    for (const term of pmSearchTerms) {
+      for (const sub of ['Kalshi', 'Polymarket']) {
+        try {
+          const posts = await scrapeReddit(sub, term, 5);
+          for (const post of posts) {
+            const sentiment = scoreSentiment(post.title);
+            await kenQ(`
+              INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+              VALUES ('reddit_predmkt', $1, $2, $3, $4, $5, $6, $7, '')
+              ON CONFLICT DO NOTHING
+            `, [sub, term, post.title, post.url, post.score || 0, post.num_comments || 0, sentiment]);
+            predMarketRedditTotal++;
+          }
+        } catch {}
+      }
+      await new Promise(r => setTimeout(r, 2000));
+    }
+    console.log(`[Ken] Prediction market Reddit: ${predMarketRedditTotal} posts from r/Kalshi + r/Polymarket`);
+    if (global.gc) global.gc();
+
+    // ── GDELT TV News (CNN, Fox closed captions) ──
+    const tvStations = ['CNN', 'FOXNEWS'];
+    const tvQueries = ['trump', 'tariff', 'ukraine', 'election'];
+    let tvTotal = 0;
+    for (const station of tvStations) {
+      for (const tq of tvQueries) {
+        try {
+          const clips = await scrapeGDELTtv(tq, station, 5);
+          for (const clip of clips) {
+            if (clip.title && clip.title.length > 15) {
+              const sentiment = scoreSentiment(clip.title);
+              await kenQ(`
+                INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
+                VALUES ('tv_news', $1, $2, $3, $4, 0, $5, $6)
+                ON CONFLICT DO NOTHING
+              `, [clip.source || station, tq, clip.title.substring(0, 500), clip.url, sentiment, clip.show || '']);
+              tvTotal++;
+            }
+          }
+        } catch {}
+        await new Promise(r => setTimeout(r, 500));
+      }
+    }
+    console.log(`[Ken] GDELT TV: ${tvTotal} clips from ${tvStations.length} stations`);
+
+    // ── YouTube News Channels (CNN, MSNBC, Fox, Bloomberg, CNBC, etc.) ──
+    try {
+      const ytVideos = await scrapeYouTubeRSS();
+      let ytStored = 0;
+      for (const video of ytVideos) {
+        const sentiment = scoreSentiment(video.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
+          VALUES ('youtube_news', $1, 'video', $2, $3, 0, $4, '')
+          ON CONFLICT DO NOTHING
+        `, [video.source, video.title, video.url, sentiment]);
+        ytStored++;
+      }
+      totalPosts += ytStored;
+      console.log(`[Ken] YouTube News: ${ytStored} videos from 9 channels`);
+    } catch (e) {
+      console.log(`[Ken] YouTube News failed: ${e.message}`);
+    }
+    if (global.gc) global.gc();
+
+    // ── Lemmy (federated Reddit — public API, no auth) ──
+    let lemmyTotal = 0;
+    try {
+      const lemmyPosts = await scrapeLemmy('Hot', 25);
+      for (const p of lemmyPosts) {
+        const sentiment = scoreSentiment(p.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('lemmy', $1, 'hot', $2, $3, $4, $5, $6, '')
+          ON CONFLICT DO NOTHING
+        `, [p.source, p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
+        lemmyTotal++;
+      }
+    } catch {}
+    console.log(`[Ken] Lemmy: ${lemmyTotal} posts`);
+
+    // ── Lobsters (tech/policy news — public JSON API) ──
+    let lobstersTotal = 0;
+    try {
+      const stories = await scrapeLobsters(25);
+      for (const s of stories) {
+        const sentiment = scoreSentiment(s.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('lobsters', $1, $2, $3, $4, $5, $6, $7, '')
+          ON CONFLICT DO NOTHING
+        `, [s.author || 'lobsters', s.tags || 'tech', s.title, s.url, s.score || 0, s.num_comments || 0, sentiment]);
+        lobstersTotal++;
+      }
+    } catch {}
+    console.log(`[Ken] Lobsters: ${lobstersTotal} stories`);
+
+    // ── Tildes (curated discussion — RSS) ──
+    let tildesTotal = 0;
+    try {
+      const tildesPosts = await scrapeTildes();
+      for (const p of tildesPosts) {
+        const sentiment = scoreSentiment(p.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
+          VALUES ('tildes', 'tildes', 'discussion', $1, $2, 0, $3, '')
+          ON CONFLICT DO NOTHING
+        `, [p.title, p.url, sentiment]);
+        tildesTotal++;
+      }
+    } catch {}
+    console.log(`[Ken] Tildes: ${tildesTotal} discussions`);
+
+    // ── Bluesky (trending feeds — getFeed API) ──
+    let bskyTotal = 0;
+    try {
+      const posts = await scrapeBluesky();
+      for (const p of posts) {
+        if ((p.score || 0) > 2 || (p.num_comments || 0) > 1 || p.title.length > 30) {
+          const sentiment = scoreSentiment(p.title);
+          await kenQ(`
+            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+            VALUES ('bluesky', $1, $2, $3, $4, $5, $6, $7, '')
+            ON CONFLICT DO NOTHING
+          `, [p.author || 'bluesky', p.source || 'trending', p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
+          bskyTotal++;
+        }
+      }
+    } catch {}
+    console.log(`[Ken] Bluesky: ${bskyTotal} posts`);
+
+    // ── Mastodon/Fediverse trending ──
+    try {
+      const mastodonPosts = await scrapeMastodon();
+      let mastoTotal = 0;
+      for (const p of mastodonPosts) {
+        const sentiment = scoreSentiment(p.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('mastodon', $1, 'trending', $2, $3, $4, $5, $6, '')
+          ON CONFLICT DO NOTHING
+        `, [p.source, p.title, p.url, p.score || 0, p.num_comments || 0, sentiment]);
+        mastoTotal++;
+      }
+      totalPosts += mastoTotal;
+      console.log(`[Ken] Mastodon: ${mastoTotal} trending`);
+    } catch (e) {
+      console.log(`[Ken] Mastodon failed: ${e.message}`);
+    }
+
+    totalPosts += tvTotal + lemmyTotal + lobstersTotal + tildesTotal + bskyTotal;
+    totalPosts += manifoldTotal + predMarketRedditTotal;
+
+    // ── PredictIt (cross-reference prediction market) ──
+    let predictItTotal = 0;
+    try {
+      const piMarkets = await scrapePredictIt();
+      for (const m of piMarkets) {
+        const sentiment = scoreSentiment(m.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('predictit', 'predictit', 'market', $1, $2, $3, $4, $5, $6)
+          ON CONFLICT DO NOTHING
+        `, [m.title, m.url, m.score, m.num_comments || 0, sentiment, m.extra || '']);
+        predictItTotal++;
+      }
+      totalPosts += predictItTotal;
+      console.log(`[Ken] PredictIt: ${predictItTotal} markets`);
+    } catch (e) {
+      console.log(`[Ken] PredictIt failed: ${e.message}`);
+    }
+
+    // ── FRED Economic Indicators (real data for economic markets) ──
+    let fredTotal = 0;
+    try {
+      const fredData = await scrapeFredIndicators();
+      for (const ind of fredData) {
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, sentiment_score, raw_text)
+          VALUES ('fred_economic', 'fred', $1, $2, $3, $4, 0, '')
+          ON CONFLICT DO NOTHING
+        `, [ind.keyword, ind.title, ind.url, ind.score]);
+        fredTotal++;
+      }
+      totalPosts += fredTotal;
+      console.log(`[Ken] FRED Economic: ${fredTotal} indicators`);
+    } catch (e) {
+      console.log(`[Ken] FRED failed: ${e.message}`);
+    }
+
+    // ── NWS Active Weather Alerts (severe weather data for weather markets) ──
+    let nwsAlertTotal = 0;
+    try {
+      const alerts = await scrapeNWSAlerts();
+      for (const alert of alerts) {
+        const sentiment = scoreSentiment(alert.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('nws_alerts', 'weather', $1, $2, $3, $4, 0, $5, '')
+          ON CONFLICT DO NOTHING
+        `, [alert.keyword, alert.title, alert.url, alert.score, sentiment]);
+        nwsAlertTotal++;
+      }
+      totalPosts += nwsAlertTotal;
+      console.log(`[Ken] NWS Alerts: ${nwsAlertTotal} active alerts`);
+    } catch (e) {
+      console.log(`[Ken] NWS Alerts failed: ${e.message}`);
+    }
+
+    // ── GovTrack Congressional Votes (legislation for political markets) ──
+    let govTrackTotal = 0;
+    try {
+      const votes = await scrapeGovTrack();
+      for (const v of votes) {
+        const sentiment = scoreSentiment(v.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('govtrack', 'congress', $1, $2, $3, $4, $5, $6, '')
+          ON CONFLICT DO NOTHING
+        `, [v.keyword, v.title, v.url, v.score, v.num_comments || 0, sentiment]);
+        govTrackTotal++;
+      }
+      totalPosts += govTrackTotal;
+      console.log(`[Ken] GovTrack: ${govTrackTotal} congressional votes`);
+    } catch (e) {
+      console.log(`[Ken] GovTrack failed: ${e.message}`);
+    }
+
+    // ── Twitter/X via Nitter (public sentiment, no auth needed) ──
+    let nitterTotal = 0;
+    try {
+      const tweets = await scrapeNitterTrending();
+      for (const t of tweets) {
+        const sentiment = scoreSentiment(t.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('twitter_nitter', 'twitter', $1, $2, $3, 0, 0, $4, '')
+          ON CONFLICT DO NOTHING
+        `, [t.keyword, t.title, t.url, sentiment]);
+        nitterTotal++;
+      }
+      totalPosts += nitterTotal;
+      console.log(`[Ken] Twitter/Nitter: ${nitterTotal} tweets`);
+    } catch (e) {
+      console.log(`[Ken] Twitter/Nitter failed: ${e.message}`);
+    }
+
+    // ── Discord (Kalshi, Polymarket, Prediction Markets servers) ──
+    let discordTotal = 0;
+    try {
+      const discordMessages = await scrapeDiscord(30);
+      for (const msg of discordMessages) {
+        const sentiment = scoreSentiment(msg.title);
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('discord', $1, $2, $3, $4, $5, $6, $7, $8)
+          ON CONFLICT DO NOTHING
+        `, [
+          msg.source, msg.channel || 'general', msg.title.substring(0, 500),
+          msg.url, msg.score || 0, msg.num_comments || 0, sentiment,
+          (msg.author || '').substring(0, 200),
+        ]);
+        discordTotal++;
+      }
+      totalPosts += discordTotal;
+      console.log(`[Ken] Discord: ${discordTotal} messages`);
+    } catch (e) {
+      console.log(`[Ken] Discord failed: ${e.message}`);
+    }
+    if (global.gc) global.gc();
+
+    logMemory('sweep-end');
+    if (global.gc) global.gc();
+    console.log(`[Ken] ═══ FULL SWEEP: ${totalPosts} items (Reddit, HN, ${NEWS_FEEDS.length} feeds, TV, YT, Manifold, r/Kalshi, r/Polymarket, Lemmy, Lobsters, Tildes, Bluesky, Mastodon, Metaculus, PredictIt, FRED, NWS Alerts, GovTrack, Twitter, Discord) ═══`);
+    return totalPosts;
+  } catch (e) {
+    console.error('[Ken] Intelligence sweep error:', e.message);
+    logMemory('sweep-error');
+    return 0;
+  }
+}
+
+// ══════════════════════════════════════════════════════════════════════════════
+// SIGNAL PIPELINE: News → Sentiment → Event Map → Probability → Signals → Risk
+// Pipeline: narrative shifts → probability inference → edge detection
+// ══════════════════════════════════════════════════════════════════════════════
+
+// ── Enhanced Event-Specific Sentiment Engine ──
+// Unlike scoreSentiment() which is general, this maps headlines to specific market directions
+const EVENT_SENTIMENT_RULES = [
+  // Weather markets
+  { pattern: /hurricane|tropical storm|cyclone/i, category: 'weather', direction: 'severity', boost: 0.3 },
+  { pattern: /category\s*[3-5]|major hurricane|extreme/i, category: 'weather', direction: 'severity', boost: 0.5 },
+  { pattern: /weakens|dissipates|downgrade/i, category: 'weather', direction: 'severity', boost: -0.4 },
+  { pattern: /record heat|heat wave|above normal/i, category: 'weather', direction: 'temperature_high', boost: 0.4 },
+  { pattern: /cold snap|polar vortex|below normal|freeze/i, category: 'weather', direction: 'temperature_low', boost: 0.4 },
+  { pattern: /heavy rain|flood|rainfall record/i, category: 'weather', direction: 'precipitation_high', boost: 0.4 },
+  { pattern: /drought|dry spell|no rain/i, category: 'weather', direction: 'precipitation_low', boost: 0.4 },
+  // Political markets
+  { pattern: /resign|step down|quit|fired/i, category: 'politics', direction: 'negative', boost: 0.5 },
+  { pattern: /appointed|confirmed|sworn in|nominated/i, category: 'politics', direction: 'positive', boost: 0.4 },
+  { pattern: /impeach|investigation|scandal|indicted/i, category: 'politics', direction: 'negative', boost: 0.4 },
+  { pattern: /leading|ahead|polling|surge in polls/i, category: 'politics', direction: 'positive', boost: 0.3 },
+  { pattern: /trailing|behind|falling|losing ground/i, category: 'politics', direction: 'negative', boost: 0.3 },
+  { pattern: /shutdown|deadlock|stalemate/i, category: 'politics', direction: 'shutdown_yes', boost: 0.5 },
+  { pattern: /deal reached|agreement|bipartisan|passed/i, category: 'politics', direction: 'shutdown_no', boost: -0.4 },
+  // Economic markets
+  { pattern: /rate cut|dovish|easing/i, category: 'economics', direction: 'rate_down', boost: 0.4 },
+  { pattern: /rate hike|hawkish|tightening/i, category: 'economics', direction: 'rate_up', boost: 0.4 },
+  { pattern: /inflation.*(?:hot|higher|above|surge|accelerat)/i, category: 'economics', direction: 'inflation_up', boost: 0.4 },
+  { pattern: /inflation.*(?:cool|lower|below|slow|deceler)/i, category: 'economics', direction: 'inflation_down', boost: 0.4 },
+  { pattern: /recession|contraction|GDP.*(?:negative|shrink)/i, category: 'economics', direction: 'recession_yes', boost: 0.5 },
+  { pattern: /growth|expansion|GDP.*(?:beat|strong|positive)/i, category: 'economics', direction: 'recession_no', boost: -0.4 },
+  { pattern: /jobs.*(?:beat|strong|added more)/i, category: 'economics', direction: 'jobs_strong', boost: 0.3 },
+  { pattern: /jobs.*(?:miss|weak|fewer)/i, category: 'economics', direction: 'jobs_weak', boost: 0.3 },
+  // Crypto markets
+  { pattern: /bitcoin.*(?:new high|surge|rally|breakout|100k|150k)/i, category: 'crypto', direction: 'btc_up', boost: 0.4 },
+  { pattern: /bitcoin.*(?:crash|plunge|selloff|bear)/i, category: 'crypto', direction: 'btc_down', boost: 0.4 },
+  { pattern: /SEC.*(?:approve|green light)/i, category: 'crypto', direction: 'btc_up', boost: 0.3 },
+  { pattern: /SEC.*(?:reject|deny|sue)/i, category: 'crypto', direction: 'btc_down', boost: 0.3 },
+  // Sports/entertainment
+  { pattern: /favou?rite|odds.*increase|bet.*heavy/i, category: 'sports', direction: 'favourite_stronger', boost: 0.3 },
+  { pattern: /upset|underdog|surprise/i, category: 'sports', direction: 'favourite_weaker', boost: 0.3 },
+  { pattern: /injured|out for|suspended|DNP/i, category: 'sports', direction: 'team_weaker', boost: 0.4 },
+  // Space/tech
+  { pattern: /launch.*(?:success|orbit|nominal)/i, category: 'space', direction: 'launch_success', boost: 0.5 },
+  { pattern: /launch.*(?:scrub|delay|abort|fail)/i, category: 'space', direction: 'launch_fail', boost: 0.5 },
+  // Geopolitics
+  { pattern: /ceasefire|peace.*(?:deal|talks|agreement)/i, category: 'geopolitics', direction: 'de_escalation', boost: 0.5 },
+  { pattern: /escalat|offensive|invasion|attack|strike/i, category: 'geopolitics', direction: 'escalation', boost: 0.4 },
+];
+
+function scoreEventSentiment(headline, marketTitle = '') {
+  const text = `${headline} ${marketTitle}`.toLowerCase();
+  const result = {
+    sentiment: 0,
+    confidence: 0,
+    magnitude: 0,
+    category: 'general',
+    direction: 'neutral',
+    matchedRules: [],
+  };
+
+  // Apply rule-based event-specific scoring
+  for (const rule of EVENT_SENTIMENT_RULES) {
+    if (rule.pattern.test(text)) {
+      result.sentiment += rule.boost;
+      result.magnitude += Math.abs(rule.boost);
+      result.category = rule.category;
+      result.direction = rule.direction;
+      result.matchedRules.push(rule.direction);
+    }
+  }
+
+  // Certainty language modifiers
+  const certainWords = /confirm|official|announce|breaking|just.*happen/i;
+  const uncertainWords = /may|might|could|rumor|unconfirm|speculat|sources say/i;
+  if (certainWords.test(text)) { result.confidence += 0.3; result.magnitude *= 1.2; }
+  if (uncertainWords.test(text)) { result.confidence -= 0.2; result.magnitude *= 0.7; }
+
+  // Multiple source agreement boosts confidence
+  if (result.matchedRules.length > 1) result.confidence += 0.15;
+
+  // Normalize
+  result.sentiment = Math.max(-1, Math.min(1, result.sentiment));
+  result.confidence = Math.max(0, Math.min(1, 0.5 + result.confidence)); // base confidence 0.5
+  result.magnitude = Math.min(1, result.magnitude);
+
+  return result;
+}
+
+// ── Event Mapper: Map news entities → Kalshi market tickers ──
+// Uses keyword-based + fuzzy matching to connect news to specific contracts
+const MARKET_CATEGORY_KEYWORDS = {
+  // Weather
+  'temperature': ['temperature', 'temp', 'heat', 'cold', 'warm', 'degree', 'fahrenheit', 'celsius'],
+  'hurricane': ['hurricane', 'tropical', 'cyclone', 'storm', 'named storm'],
+  'rainfall': ['rain', 'rainfall', 'precipitation', 'flood', 'drought', 'inches'],
+  // Politics
+  'election': ['election', 'vote', 'ballot', 'primary', 'nominee', 'candidate', 'poll'],
+  'government': ['resign', 'cabinet', 'secretary', 'appointment', 'nomination', 'impeach'],
+  'legislation': ['bill', 'act', 'congress', 'senate', 'house', 'passed', 'shutdown'],
+  // Economics
+  'fed_rate': ['federal reserve', 'fed rate', 'interest rate', 'fomc', 'rate cut', 'rate hike', 'powell'],
+  'inflation': ['cpi', 'inflation', 'consumer price', 'pce', 'core inflation'],
+  'gdp': ['gdp', 'gross domestic', 'economic growth', 'recession', 'expansion'],
+  'jobs': ['jobs report', 'unemployment', 'nonfarm', 'payroll', 'jobless claims'],
+  // Crypto
+  'bitcoin': ['bitcoin', 'btc', 'cryptocurrency', 'crypto'],
+  'ethereum': ['ethereum', 'eth', 'ether'],
+  // Space
+  'spacex': ['spacex', 'falcon', 'starship', 'rocket launch', 'orbit'],
+  'nasa': ['nasa', 'artemis', 'moon', 'mars mission'],
+};
+
+// Map ticker suffixes to searchable names for multi-outcome events
+const TICKER_ENTITY_MAP = {
+  // KXCABOUT - Cabinet members
+  'RFK':  ['rfk', 'kennedy', 'robert kennedy', 'rfk jr', 'health and human'],
+  'KNOE': ['noem', 'kristi noem', 'homeland security'],
+  'SDUF': ['duffy', 'sean duffy', 'transportation'],
+  'BROL': ['rollins', 'brooke rollins', 'agriculture'],
+  'JRAT': ['ratcliffe', 'john ratcliffe', 'cia director'],
+  'SBES': ['bessent', 'scott bessent', 'treasury secretary'],
+  'MRUB': ['rubio', 'marco rubio', 'state department', 'secretary of state'],
+  'PHEG': ['hegseth', 'pete hegseth', 'defense secretary'],
+  'TGAB': ['gabbard', 'tulsi gabbard', 'intelligence director'],
+  'MWAL': ['waltz', 'mike waltz', 'national security'],
+  'HLUT': ['lutnick', 'howard lutnick', 'commerce secretary'],
+  'DBUR': ['burgum', 'doug burgum', 'interior secretary'],
+  'CWRI': ['chris wright', 'energy secretary'],
+  'LZEL': ['zeldin', 'lee zeldin', 'epa administrator'],
+  'DCOL': ['collins', 'doug collins', 'veterans affairs'],
+  'LMCM': ['mcmahon', 'linda mcmahon', 'education secretary'],
+  'PBON': ['bondi', 'pam bondi', 'attorney general'],
+  'RVOU': ['vought', 'russell vought', 'omb director'],
+  'STUR': ['turner', 'scott turner', 'hud secretary'],
+  'LCDR': ['cruz', 'linda cruz', 'sba administrator'],
+  'MKRA': ['kratsios', 'michael kratsios'],
+  'SWIL': ['wilson', 'sean wilson'],
+  // KXPRESPERSON - 2028 Presidential election
+  'DTRU':   ['trump', 'donald trump', 'president trump', 'former president trump'],
+  'DTRUJR': ['trump jr', 'donald trump jr', 'don jr', 'trump junior'],
+  'RDES':   ['desantis', 'ron desantis', 'florida governor'],
+  'JVAN':   ['vance', 'jd vance', 'vice president vance', 'j.d. vance'],
+  'GYOU':   ['youngkin', 'glenn youngkin', 'virginia governor'],
+  // KXFEDCHAIRNOM - Fed Chair nominees
+  'JS':   ['dimon', 'jamie dimon'],
+  'KW':   ['warsh', 'kevin warsh'],
+  'KH':   ['hassett', 'kevin hassett'],
+  'CWAL': ['waller', 'chris waller', 'christopher waller'],
+  'MBOW': ['bowman', 'michelle bowman'],
+};
+
+function extractMarketEntity(market) {
+  // Extract ticker suffix for entity lookup first (most reliable)
+  const parts = market.ticker.split('-');
+  const suffix = parts[parts.length - 1];
+  if (TICKER_ENTITY_MAP[suffix]) return TICKER_ENTITY_MAP[suffix];
+  // Try subtitle only if it's a real description (not just the ticker repeated)
+  if (market.subtitle && market.subtitle !== market.ticker && !market.subtitle.startsWith('KX')) {
+    return market.subtitle.toLowerCase().split(/[\s\-]+/).filter(w => w.length > 2);
+  }
+  // Try the yes_sub_title field
+  if (market.yes_sub_title && market.yes_sub_title !== market.ticker) {
+    return market.yes_sub_title.toLowerCase().split(/[\s\-]+/).filter(w => w.length > 2);
+  }
+  return [];
+}
+
+function isMultiOutcomeEvent(ticker) {
+  // Multi-outcome events have a common prefix followed by person/answer suffix
+  // e.g., KXCABOUT-29JAN-RFK, KXCABOUT-29JAN-KNOE all share KXCABOUT-29JAN
+  const parts = ticker.split('-');
+  return parts.length >= 3; // Event prefix + date + answer
+}
+
+function getEventPrefix(ticker) {
+  const parts = ticker.split('-');
+  if (parts.length >= 3) return parts.slice(0, -1).join('-');
+  return ticker;
+}
+
+function mapNewsToMarkets(headline, activeMarkets) {
+  const headlineLower = headline.toLowerCase();
+  const matches = [];
+
+  // Step 1: Identify which categories the headline relates to
+  const relevantCategories = [];
+  for (const [category, keywords] of Object.entries(MARKET_CATEGORY_KEYWORDS)) {
+    for (const kw of keywords) {
+      if (headlineLower.includes(kw)) {
+        relevantCategories.push(category);
+        break;
+      }
+    }
+  }
+
+  // Step 2: Group markets by event to detect multi-outcome events
+  const eventGroups = {};
+  for (const market of activeMarkets) {
+    const prefix = getEventPrefix(market.ticker);
+    if (!eventGroups[prefix]) eventGroups[prefix] = [];
+    eventGroups[prefix].push(market);
+  }
+
+  // Step 3: Find matching Kalshi markets by title/subtitle keywords
+  for (const market of activeMarkets) {
+    const marketText = `${market.title} ${market.subtitle || ''} ${market.yes_sub_title || ''}`.toLowerCase();
+    let matchScore = 0;
+
+    // Direct keyword overlap
+    const headlineWords = headlineLower.split(/\s+/).filter(w => w.length > 3);
+    for (const word of headlineWords) {
+      if (marketText.includes(word)) matchScore += 1;
+    }
+
+    // Category match bonus
+    for (const cat of relevantCategories) {
+      const catKws = MARKET_CATEGORY_KEYWORDS[cat];
+      if (catKws.some(kw => marketText.includes(kw))) matchScore += 2;
+    }
+
+    // Entity matching (city names, person names, etc.)
+    const cities = ['phoenix', 'houston', 'miami', 'chicago', 'new york', 'los angeles', 'atlanta', 'dallas', 'denver', 'seattle', 'boston', 'detroit', 'minneapolis', 'philadelphia'];
+    for (const city of cities) {
+      if (headlineLower.includes(city) && marketText.includes(city)) matchScore += 5;
+    }
+
+    // Political figure matching
+    const figures = ['trump', 'biden', 'harris', 'desantis', 'newsom', 'vance', 'powell', 'yellen'];
+    for (const fig of figures) {
+      if (headlineLower.includes(fig) && marketText.includes(fig)) matchScore += 5;
+    }
+
+    // MULTI-OUTCOME EVENT HANDLING: For events with multiple answer markets,
+    // require headline to mention the specific answer entity, not just the general topic
+    const eventPrefix = getEventPrefix(market.ticker);
+    const siblings = eventGroups[eventPrefix] || [];
+    if (siblings.length > 1 && isMultiOutcomeEvent(market.ticker)) {
+      const entityTerms = extractMarketEntity(market);
+      const entityMatch = entityTerms.some(term => headlineLower.includes(term));
+      if (entityMatch) {
+        matchScore += 8; // Strong boost for specific entity match
+      } else {
+        // Headline mentions the topic but NOT this specific answer — reject entirely
+        // With 20+ siblings, even small scores cause massive clustering
+        matchScore = 0;
+      }
+    }
+
+    if (matchScore >= 3) {
+      matches.push({ market, matchScore });
+    }
+  }
+
+  // Sort by match score descending
+  matches.sort((a, b) => b.matchScore - a.matchScore);
+  return matches.slice(0, 5); // Top 5 matches
+}
+
+// ── Probability Shift Model (Bayesian Update) ──
+// adjustedProb = priorProb + (sentiment * weight * confidence)
+// With decay for older signals, source diversity bonus, and category-specific weights
+const CATEGORY_SENTIMENT_WEIGHTS = {
+  politics: 0.18,    // Political events move markets fast
+  economics: 0.16,   // Economic data is highly impactful
+  crypto: 0.22,      // Crypto is volatile, sentiment-driven
+  weather: 0.10,     // Weather has model data, less sentiment-dependent
+  geopolitics: 0.20, // Geopolitical events create large moves
+  sports: 0.12,      // Sports have existing odds infrastructure
+  space: 0.14,       // Launch outcomes are binary
+  general: 0.12,     // Default
+};
+
+function calculateProbabilityShift(priorProb, sentimentSignals) {
+  if (!sentimentSignals.length) return { adjustedProb: priorProb, impact: 0, numArticles: 0, agreement: 0, sourceDiversity: 0 };
+
+  const RECENCY_DECAY = 0.85; // Faster decay — old news matters less
+  const AGREEMENT_BONUS = 1.5; // Stronger bonus for agreement
+  const DISAGREEMENT_PENALTY = 0.3; // Stronger penalty for mixed signals
+
+  let totalImpact = 0;
+  let totalWeight = 0;
+  const now = Date.now();
+
+  // Sort by recency
+  const sorted = [...sentimentSignals].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
+
+  // Calculate source diversity (unique sources boost signal)
+  const uniqueSources = new Set(sorted.map(s => s.source));
+  const sourceDiversity = Math.min(1, uniqueSources.size / 4); // 4+ unique sources = max diversity
+
+  for (const sig of sorted) {
+    const hoursAgo = Math.max(0, (now - (sig.timestamp || now)) / 3600000);
+    const recencyWeight = Math.pow(RECENCY_DECAY, hoursAgo);
+    // Use category-specific weight instead of flat 0.08
+    const catWeight = CATEGORY_SENTIMENT_WEIGHTS[sig.category] || CATEGORY_SENTIMENT_WEIGHTS.general;
+    // Magnitude amplifier: strong signals (|sentiment| > 0.3) get more weight
+    const magnitudeBoost = 1 + Math.min(0.5, (sig.magnitude || 0) * 0.8);
+    const impact = sig.sentiment * catWeight * sig.confidence * recencyWeight * magnitudeBoost;
+    totalImpact += impact;
+    totalWeight += recencyWeight * sig.confidence;
+  }
+
+  // Source diversity bonus: multiple independent sources confirming = stronger signal
+  totalImpact *= (1 + sourceDiversity * 0.4);
+
+  // Check agreement: if most signals agree on direction, boost; if mixed, penalize
+  const posSignals = sorted.filter(s => s.sentiment > 0).length;
+  const negSignals = sorted.filter(s => s.sentiment < 0).length;
+  const total = posSignals + negSignals;
+  if (total >= 2) {
+    const agreementRatio = Math.max(posSignals, negSignals) / total;
+    if (agreementRatio > 0.8) totalImpact *= AGREEMENT_BONUS; // Strong agreement
+    else if (agreementRatio > 0.6) totalImpact *= 1.1; // Mild agreement
+    else totalImpact *= DISAGREEMENT_PENALTY; // Mixed signals — dampen hard
+  }
+
+  // Diminishing returns: each additional article adds less new information
+  // 1 article = 100%, 4 = 50%, 9 = 33%, 16 = 25%
+  const articleDiminishing = Math.sqrt(sorted.length) / sorted.length;
+  totalImpact *= articleDiminishing;
+
+  // Prior-anchored capping: prevent unrealistic shifts on low-probability markets
+  // A 4% market shouldn't jump to 60% from sentiment alone; cap shift at 5x prior or 15%, whichever is higher
+  const maxShift = Math.max(0.15, priorProb * 5);
+  totalImpact = Math.sign(totalImpact) * Math.min(Math.abs(totalImpact), maxShift);
+
+  // Clamp adjusted probability to [0.01, 0.99]
+  const adjustedProb = Math.max(0.01, Math.min(0.99, priorProb + totalImpact));
+
+  return {
+    adjustedProb,
+    impact: totalImpact,
+    numArticles: sorted.length,
+    agreement: total > 0 ? Math.max(posSignals, negSignals) / total : 0,
+    sourceDiversity,
+  };
+}
+
+// ── Risk Engine ──
+const RISK_LIMITS = {
+  maxExposurePerMarket: 5000, // cents ($50)
+  maxExposurePerTheme: 15000, // cents ($150)
+  maxTotalExposure: 50000, // cents ($500)
+  maxContractsPerMarket: 25,
+  minEdgeThreshold: 0.03, // 3% edge minimum (lowered from 5% — entity-specific matching yields smaller edges)
+  maxDailyLoss: 5000, // cents ($50)
+  cooldownAfterLoss: 30, // minutes
+  timeDecayPenalty: 0.02, // Reduce edge by 2% per hour of signal age
+  minLiquidity: 100, // Minimum 24h volume
+};
+
+function riskCheck(signal, currentExposure = {}) {
+  const reasons = [];
+  let approved = true;
+
+  // Edge threshold
+  if (Math.abs(signal.expectedEdge) < RISK_LIMITS.minEdgeThreshold) {
+    approved = false;
+    reasons.push(`Edge ${(signal.expectedEdge * 100).toFixed(1)}% below ${(RISK_LIMITS.minEdgeThreshold * 100)}% threshold`);
+  }
+
+  // Market exposure
+  const marketExposure = currentExposure[signal.marketId] || 0;
+  if (marketExposure >= RISK_LIMITS.maxExposurePerMarket) {
+    approved = false;
+    reasons.push(`Market exposure $${(marketExposure / 100).toFixed(2)} at limit`);
+  }
+
+  // Liquidity check
+  if ((signal.volume24h || 0) < RISK_LIMITS.minLiquidity) {
+    approved = false;
+    reasons.push(`Low liquidity: ${signal.volume24h || 0} < ${RISK_LIMITS.minLiquidity} volume`);
+  }
+
+  // Time decay — use absolute edge values so BUY_NO signals aren't unfairly penalized
+  const signalAgeHours = (Date.now() - (signal.timestamp || Date.now())) / 3600000;
+  const absEdge = Math.abs(signal.expectedEdge);
+  const decayedAbsEdge = absEdge - (RISK_LIMITS.timeDecayPenalty * signalAgeHours);
+  if (decayedAbsEdge < RISK_LIMITS.minEdgeThreshold) {
+    approved = false;
+    reasons.push(`Edge decayed from ${(absEdge * 100).toFixed(1)}% to ${(decayedAbsEdge * 100).toFixed(1)}%`);
+  }
+
+  // Size recommendation based on edge and confidence
+  let sizeRecommendation = 1;
+  if (approved) {
+    const edgeFactor = Math.min(3, Math.abs(signal.expectedEdge) / RISK_LIMITS.minEdgeThreshold);
+    const confFactor = signal.confidence || 0.5;
+    sizeRecommendation = Math.max(1, Math.min(RISK_LIMITS.maxContractsPerMarket, Math.floor(edgeFactor * confFactor * 10)));
+  }
+
+  return { approved, reasons, sizeRecommendation, decayedEdge: decayedAbsEdge };
+}
+
+// ── Fast Signal Pipeline (runs every 5 minutes) ──
+async function signalPipeline(activeMarkets) {
+  const pipelineStart = Date.now();
+  let signalsGenerated = 0;
+
+  try {
+    // Step 1: Fetch recent news/sentiment from last 30 minutes
+    const { rows: recentArticles } = await kenQ(`
+      SELECT id, source, keyword, post_title as headline, post_url as url, sentiment_score, captured_at
+      FROM ken_sentiment
+      WHERE captured_at > NOW() - INTERVAL '30 minutes'
+      ORDER BY captured_at DESC
+      LIMIT 500
+    `);
+
+    if (recentArticles.length === 0) {
+      console.log('[Ken] Signal Pipeline: no new articles in last 30 min');
+      return 0;
+    }
+
+    // Step 2: Score each article with event-specific sentiment
+    const eventScores = [];
+    for (const article of recentArticles) {
+      const evtScore = scoreEventSentiment(article.headline);
+      if (evtScore.magnitude > 0.1) { // Only process articles with detectable event signal
+        // Step 3: Map to relevant markets
+        const mappedMarkets = mapNewsToMarkets(article.headline, activeMarkets);
+
+        for (const { market, matchScore } of mappedMarkets) {
+          eventScores.push({
+            articleId: String(article.id),
+            headline: article.headline,
+            source: article.source,
+            marketTicker: market.ticker,
+            marketTitle: market.title,
+            sentiment: evtScore.sentiment,
+            confidence: evtScore.confidence * (matchScore / 10), // Scale confidence by match quality
+            magnitude: evtScore.magnitude,
+            category: evtScore.category,
+            direction: evtScore.direction,
+            timestamp: new Date(article.captured_at).getTime(),
+          });
+
+          // Store event-specific sentiment
+          await kenQ(`
+            INSERT INTO ken_event_sentiment (article_id, event_tag, market_ticker, sentiment, confidence, magnitude, headline, source)
+            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
+          `, [String(article.id), evtScore.direction, market.ticker, evtScore.sentiment,
+              evtScore.confidence, evtScore.magnitude, article.headline?.substring(0, 500), article.source]);
+        }
+      }
+    }
+
+    console.log(`[Ken] Signal Pipeline: ${eventScores.length} event-specific scores from ${recentArticles.length} articles`);
+
+    // Step 4: Group by market and calculate probability shifts
+    const marketSignals = {};
+    for (const score of eventScores) {
+      if (!marketSignals[score.marketTicker]) marketSignals[score.marketTicker] = [];
+      marketSignals[score.marketTicker].push(score);
+    }
+
+    // Step 5: For each market with signals, compute probability shift
+    for (const [ticker, signals] of Object.entries(marketSignals)) {
+      const market = activeMarkets.find(m => m.ticker === ticker);
+      if (!market) continue;
+
+      const priorProb = (market.yes_bid + market.yes_ask) / 200; // Convert cents to probability
+      const { adjustedProb, impact, numArticles, agreement, sourceDiversity } = calculateProbabilityShift(priorProb, signals);
+      const marketProb = market.last_price / 100;
+      const edge = adjustedProb - marketProb;
+
+      // Store probability shift
+      await kenQ(`
+        INSERT INTO ken_probability_shifts (market_id, prior_probability, sentiment_impact, adjusted_probability, market_probability, edge, num_articles)
+        VALUES ($1, $2, $3, $4, $5, $6, $7)
+      `, [ticker, priorProb, impact, adjustedProb, marketProb, edge, numArticles]);
+
+      // Step 6: Generate signal if edge exceeds threshold
+      if (Math.abs(edge) >= RISK_LIMITS.minEdgeThreshold) {
+        const direction = edge > 0 ? 'BUY_YES' : 'BUY_NO';
+
+        // Step 6b: Monte Carlo simulation for probability validation (CPU-intensive)
+        const momentum = cachedMomentumMap?.[ticker] || { delta_2h: 0, delta_6h: 0 };
+        const mcResult = monteCarloSimulation({
+          priorProb,
+          sentimentImpact: impact,
+          momentum2h: momentum.delta_2h,
+          momentum6h: momentum.delta_6h,
+          volume24h: market.volume_24h || 0,
+          spread: (market.yes_ask || 0) - (market.yes_bid || 0),
+          articleCount: numArticles,
+          agreement: agreement || 0.5,
+        }, 15000); // 15k iterations for higher accuracy
+
+        // Blend Monte Carlo with statistical model
+        const mcEdge = mcResult.edgeConsistent ? (edge * 0.65 + mcResult.medianEdge * 0.35) : edge * 0.8;
+
+        // Dedup: skip if we already generated a signal for this market+direction in last 30 min
+        // Compare MC-blended edge (mcEdge) against stored MC-blended edge to avoid false positives
+        const recentDup = await kenQ(`
+          SELECT id, expected_edge, confidence FROM ken_strategy_signals
+          WHERE market_id = $1 AND direction = $2 AND created_at > NOW() - INTERVAL '30 minutes'
+          ORDER BY created_at DESC LIMIT 1
+        `, [ticker, direction]);
+        if (recentDup.rows.length > 0) {
+          const prevEdge = parseFloat(recentDup.rows[0].expected_edge);
+          if (Math.abs(Math.abs(mcEdge) - Math.abs(prevEdge)) < 0.02) continue;
+        }
+
+        // Improved confidence using MC validation
+        const edgeScore = Math.min(1, Math.pow(Math.abs(mcEdge) / 0.15, 1.5));
+        const volumeScore = Math.min(1, numArticles / 8);
+        const agreementScore = Math.pow(agreement || 0.5, 0.7);
+        const diversityScore = sourceDiversity || 0;
+        const mcConfBoost = mcResult.confidence > 0.6 ? 0.08 : mcResult.confidence > 0.4 ? 0.04 : 0;
+        const consistencyBoost = mcResult.edgeConsistent ? 0.05 : -0.05;
+        const confidence = Math.min(0.95, Math.max(0.15,
+          edgeScore * 0.30 +
+          agreementScore * 0.18 +
+          volumeScore * 0.18 +
+          diversityScore * 0.12 +
+          mcConfBoost +
+          consistencyBoost +
+          (numArticles >= 5 ? 0.10 : numArticles >= 3 ? 0.05 : 0)
+        ));
+
+        let signal = {
+          marketId: ticker,
+          direction,
+          expectedEdge: mcEdge,
+          confidence,
+          volume24h: market.volume_24h || 0,
+          timestamp: Date.now(),
+          agreement,
+          mcSimulation: {
+            meanEdge: mcResult.meanEdge,
+            stdDev: mcResult.stdDev,
+            percentiles: mcResult.percentiles,
+            consistent: mcResult.edgeConsistent,
+          },
+        };
+
+        // Step 6c: AI Enhancement for high-edge signals (Gemini)
+        if (Math.abs(mcEdge) >= 0.08 && confidence >= 0.4 && numArticles >= 3) {
+          signal = await aiEnhanceSignal(signal, signals, market);
+        }
+
+        // Step 7: Risk check
+        const risk = riskCheck(signal);
+
+        const mcInfo = `MC: μ=${(mcResult.meanEdge * 100).toFixed(1)}%±${(mcResult.stdDev * 100).toFixed(1)}%, p5-p95=[${(mcResult.percentiles.p5 * 100).toFixed(1)}%,${(mcResult.percentiles.p95 * 100).toFixed(1)}%]`;
+        const aiInfo = signal.aiEnhanced ? ` | AI: ${signal.aiReasoning || 'enhanced'}` : '';
+        const reasoning = `${direction} on "${market.title}" — edge ${(mcEdge * 100).toFixed(1)}% from ${numArticles} articles (${signals[0]?.category}/${signals[0]?.direction}). Agreement: ${((agreement || 0) * 100).toFixed(0)}%. ${mcInfo}${aiInfo}. ${risk.approved ? 'RISK APPROVED' : `RISK BLOCKED: ${risk.reasons.join('; ')}`}`;
+
+        await kenQ(`
+          INSERT INTO ken_strategy_signals (market_id, direction, expected_edge, size_recommendation, confidence, reasoning, sources, risk_approved)
+          VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
+        `, [ticker, direction, mcEdge, risk.sizeRecommendation, confidence, reasoning,
+            JSON.stringify(signals.slice(0, 5).map(s => ({ headline: s.headline?.substring(0, 200), source: s.source, sentiment: s.sentiment }))),
+            risk.approved]);
+
+        signalsGenerated++;
+
+        if (risk.approved && confidence > 0.7) {
+          console.log(`[Ken] SIGNAL: ${direction} ${ticker} — edge ${(mcEdge * 100).toFixed(1)}%, conf ${(confidence * 100).toFixed(0)}%, MC:${mcResult.edgeConsistent ? 'consistent' : 'mixed'}${signal.aiEnhanced ? ', AI-enhanced' : ''}, size ${risk.sizeRecommendation}`);
+        }
+
+        // Auto-trade across all 50 paper portfolios
+        if (risk.approved) {
+          autoPortfolioTrade({
+            ...signal, marketId: ticker, market_title: market.title,
+            num_articles: numArticles, signal_type: signals[0]?.category,
+            reasoning, expected_edge: mcEdge,
+          }).catch(e => console.error('[Ken/Portfolio] Error:', e.message));
+        }
+      }
+    }
+
+    const duration = Date.now() - pipelineStart;
+    console.log(`[Ken] Signal Pipeline: ${signalsGenerated} signals from ${Object.keys(marketSignals).length} markets | ${duration}ms`);
+    return signalsGenerated;
+
+  } catch (e) {
+    console.error('[Ken] Signal Pipeline error:', e.message);
+    return 0;
+  }
+}
+
+// ── Auto-mapping: Build market mappings from active Kalshi markets ──
+async function updateMarketMappings(activeMarkets) {
+  try {
+    let mapped = 0;
+    for (const m of activeMarkets) {
+      const titleLower = `${m.title} ${m.subtitle || ''}`.toLowerCase();
+      let category = 'other';
+      const keywords = [];
+
+      for (const [cat, kws] of Object.entries(MARKET_CATEGORY_KEYWORDS)) {
+        for (const kw of kws) {
+          if (titleLower.includes(kw)) {
+            category = cat;
+            keywords.push(kw);
+          }
+        }
+      }
+
+      if (keywords.length > 0) {
+        await kenQ(`
+          INSERT INTO ken_market_mappings (market_id, keywords, category, contract_type)
+          VALUES ($1, $2, $3, 'binary')
+          ON CONFLICT (id) DO NOTHING
+        `, [m.ticker, keywords, category]);
+        mapped++;
+      }
+    }
+    return mapped;
+  } catch (e) {
+    console.log(`[Ken] Market mapping update failed: ${e.message}`);
+    return 0;
+  }
+}
+
+// ══════════════════════════════════════════════
+// POLYMARKET CROSS-REFERENCE (fuzzy matching)
+// ══════════════════════════════════════════════
+const STOP_WORDS = new Set(['will', 'the', 'this', 'that', 'what', 'when', 'where', 'which', 'with', 'from', 'have', 'been', 'before', 'after', 'than', 'more', 'less', 'next', 'last', 'does', 'would', 'could', 'should', 'about', 'into', 'over', 'under', 'between', 'through', 'during', 'each', 'every', 'other', 'some', 'most', 'many', 'much', 'also', 'just', 'only', 'very', 'still', 'even', 'back', 'first', 'year', 'years']);
+
+function extractKeywords(text) {
+  return (text || '').toLowerCase()
+    .replace(/[^a-z0-9\s]/g, ' ')
+    .split(/\s+/)
+    .filter(w => w.length > 2 && !STOP_WORDS.has(w));
+}
+
+function fuzzyMatchScore(textA, textB) {
+  const wordsA = extractKeywords(textA);
+  const wordsB = new Set(extractKeywords(textB));
+  if (wordsA.length === 0 || wordsB.size === 0) return 0;
+  let matches = 0;
+  for (const w of wordsA) {
+    if (wordsB.has(w)) matches++;
+    // Partial match for longer words (e.g. "greenland" matches "greenlands")
+    else if (w.length > 5) {
+      for (const wb of wordsB) {
+        if (wb.length > 5 && (wb.startsWith(w.slice(0, 5)) || w.startsWith(wb.slice(0, 5)))) { matches += 0.5; break; }
+      }
+    }
+  }
+  return matches / Math.min(wordsA.length, wordsB.size);
+}
+
+async function polymarketCrossRef(kalshiMarkets) {
+  const crossRefs = [];
+  try {
+    const polyEvents = await scrapePolymarketEvents(100);
+    if (polyEvents.length === 0) return [];
+    console.log(`[Ken] Polymarket: ${polyEvents.length} markets from top events`);
+
+    // Build index of Kalshi market text for faster matching
+    const kalshiIndex = kalshiMarkets.map(km => ({
+      ...km,
+      searchText: ((km.title || '') + ' ' + (km.subtitle || '')).toLowerCase(),
+    }));
+
+    for (const pm of polyEvents) {
+      const pmText = pm.event_title + ' ' + pm.question;
+      let bestMatch = null;
+      let bestScore = 0;
+
+      for (const km of kalshiIndex) {
+        const score = fuzzyMatchScore(pmText, km.searchText);
+        if (score > bestScore) {
+          bestScore = score;
+          bestMatch = km;
+        }
+      }
+
+      // Require at least 30% keyword overlap for a match
+      if (bestScore >= 0.3 && bestMatch && bestMatch.yes_ask > 0) {
+        const priceDiff = pm.price - bestMatch.yes_ask;
+        if (Math.abs(priceDiff) >= 3) { // 3¢+ divergence
+          crossRefs.push({
+            kalshi_ticker: bestMatch.ticker,
+            event_ticker: bestMatch.event_ticker,
+            kalshi_title: bestMatch.title,
+            kalshi_subtitle: bestMatch.subtitle,
+            kalshi_price: bestMatch.yes_ask,
+            poly_question: pm.question,
+            poly_event: pm.event_title,
+            poly_price: pm.price,
+            poly_volume: pm.volume,
+            poly_slug: pm.slug,
+            price_diff: priceDiff,
+            match_score: Math.round(bestScore * 100),
+            arb_side: priceDiff > 0 ? 'BUY on Kalshi (cheaper)' : 'SHORT on Kalshi (overpriced)',
+          });
+        }
+      }
+    }
+    // Dedupe: keep best match per Kalshi ticker
+    const seen = new Map();
+    for (const ref of crossRefs) {
+      const key = ref.kalshi_ticker;
+      if (!seen.has(key) || Math.abs(ref.price_diff) > Math.abs(seen.get(key).price_diff)) {
+        seen.set(key, ref);
+      }
+    }
+    return [...seen.values()].sort((a, b) => Math.abs(b.price_diff) - Math.abs(a.price_diff));
+  } catch (e) {
+    console.log(`[Ken] Polymarket cross-ref error: ${e.message}`);
+    return [];
+  }
+}
+
+// ══════════════════════════════════════════════
+// LNG / ENERGY CROSS-MARKET ARBITRAGE
+// Compares energy-related predictions across Kalshi, Polymarket, and Manifold
+// ══════════════════════════════════════════════
+const ENERGY_SEARCH_TERMS = [
+  'oil price', 'crude oil', 'natural gas', 'LNG', 'OPEC',
+  'energy price', 'gas price', 'heating oil', 'petroleum',
+  'electricity price', 'EIA', 'oil production', 'pipeline',
+  'renewable energy', 'solar', 'wind energy', 'nuclear',
+  'carbon price', 'emissions', 'electric vehicle', 'EV',
+  'barrel', 'WTI', 'Brent', 'refinery',
+];
+
+async function energyArbitrageScan(kalshiMarkets) {
+  const arbs = [];
+  try {
+    // 1. Get energy markets from Manifold
+    const manifoldEnergy = await scrapeManifold(ENERGY_SEARCH_TERMS.slice(0, 8));
+    console.log(`[Ken/Energy] Manifold: ${manifoldEnergy.length} energy-related markets`);
+
+    // 2. Get energy events from Polymarket
+    let polyEnergy = [];
+    try {
+      const polyAll = await scrapePolymarketEvents(200);
+      polyEnergy = polyAll.filter(p => {
+        const t = (p.event_title + ' ' + p.question).toLowerCase();
+        return /oil|gas|lng|opec|energy|crude|barrel|petroleum|electric|solar|wind|nuclear|carbon|emission|ev market|renewable/i.test(t);
+      });
+      console.log(`[Ken/Energy] Polymarket: ${polyEnergy.length} energy-related markets`);
+    } catch {}
+
+    // 3. Filter Kalshi markets for energy/commodities
+    const kalshiEnergy = kalshiMarkets.filter(m => {
+      const t = ((m.title || '') + ' ' + (m.subtitle || '') + ' ' + (m.category || '')).toLowerCase();
+      return /oil|gas|lng|opec|energy|crude|barrel|petroleum|electric|solar|wind|nuclear|carbon|emission|ev |renewable|consumption|climate/i.test(t);
+    });
+    console.log(`[Ken/Energy] Kalshi: ${kalshiEnergy.length} energy-related markets`);
+
+    // 4. Cross-reference Manifold vs Kalshi
+    for (const mf of manifoldEnergy) {
+      for (const km of kalshiEnergy) {
+        const score = fuzzyMatchScore(mf.title, (km.title + ' ' + (km.subtitle || '')).toLowerCase());
+        if (score >= 0.25) {
+          const priceDiff = mf.probability - km.yes_ask;
+          if (Math.abs(priceDiff) >= 3) {
+            arbs.push({
+              type: 'manifold_kalshi',
+              kalshi_ticker: km.ticker,
+              event_ticker: km.event_ticker,
+              kalshi_title: km.title,
+              kalshi_price: km.yes_ask,
+              other_title: mf.title,
+              other_price: mf.probability,
+              other_source: 'Manifold',
+              other_url: mf.url,
+              other_volume: mf.volume,
+              price_diff: priceDiff,
+              match_score: Math.round(score * 100),
+              category: 'energy',
+            });
+          }
+        }
+      }
+    }
+
+    // 5. Cross-reference Polymarket vs Kalshi (energy-specific)
+    for (const pm of polyEnergy) {
+      for (const km of kalshiEnergy) {
+        const score = fuzzyMatchScore(pm.event_title + ' ' + pm.question, (km.title + ' ' + (km.subtitle || '')).toLowerCase());
+        if (score >= 0.25) {
+          const priceDiff = pm.price - km.yes_ask;
+          if (Math.abs(priceDiff) >= 3) {
+            arbs.push({
+              type: 'poly_kalshi_energy',
+              kalshi_ticker: km.ticker,
+              event_ticker: km.event_ticker,
+              kalshi_title: km.title,
+              kalshi_price: km.yes_ask,
+              other_title: pm.question,
+              other_price: pm.price,
+              other_source: 'Polymarket',
+              other_url: `https://polymarket.com/event/${pm.slug}`,
+              other_volume: pm.volume,
+              price_diff: priceDiff,
+              match_score: Math.round(score * 100),
+              category: 'energy',
+            });
+          }
+        }
+      }
+    }
+
+    // 6. Cross-reference Manifold vs Polymarket (energy only, no Kalshi needed)
+    for (const mf of manifoldEnergy) {
+      for (const pm of polyEnergy) {
+        const score = fuzzyMatchScore(mf.title, pm.event_title + ' ' + pm.question);
+        if (score >= 0.3) {
+          const priceDiff = mf.probability - pm.price;
+          if (Math.abs(priceDiff) >= 5) {
+            arbs.push({
+              type: 'manifold_poly_energy',
+              kalshi_ticker: null,
+              event_ticker: null,
+              kalshi_title: `${mf.title} (cross-platform)`,
+              kalshi_price: mf.probability,
+              other_title: pm.question,
+              other_price: pm.price,
+              other_source: 'Manifold↔Polymarket',
+              other_url: mf.url,
+              other_volume: Math.max(mf.volume, pm.volume),
+              price_diff: priceDiff,
+              match_score: Math.round(score * 100),
+              category: 'energy',
+            });
+          }
+        }
+      }
+    }
+
+    // Dedupe by best price diff per Kalshi ticker
+    const seen = new Map();
+    for (const arb of arbs) {
+      const key = arb.kalshi_ticker || arb.other_title;
+      if (!seen.has(key) || Math.abs(arb.price_diff) > Math.abs(seen.get(key).price_diff)) {
+        seen.set(key, arb);
+      }
+    }
+    const result = [...seen.values()].sort((a, b) => Math.abs(b.price_diff) - Math.abs(a.price_diff));
+    if (result.length > 0) {
+      console.log(`[Ken/Energy] Found ${result.length} energy arbitrage opportunities (top: ${result[0].kalshi_title} ${result[0].price_diff > 0 ? '+' : ''}${result[0].price_diff}¢)`);
+    }
+    return result;
+  } catch (e) {
+    console.log(`[Ken/Energy] Arbitrage scan error: ${e.message}`);
+    return [];
+  }
+}
+
+// ══════════════════════════════════════════════
+// AUTO-TRADER ($20 budget, risk-managed)
+// ══════════════════════════════════════════════
+const TRADE_CONFIG = {
+  enabled: true,               // Master kill switch
+  budget_cents: 2000,          // $20 total budget
+  max_position_cents: 200,     // $2 max per trade
+  max_open_positions: 5,       // Max concurrent positions
+  daily_loss_limit_cents: 400, // $4 daily stop-loss
+  min_confidence: 80,          // Only trade 80%+ confidence signals
+  min_sources: 2,              // Must have 2+ corroborating sources
+  take_profit_pct: 80,         // Take profit at 80% of max (e.g. sell YES at 90+ if bought at 50)
+  stop_loss_pct: 50,           // Stop loss: exit if position loses 50% of cost
+  fee_rate: 0.07,              // Kalshi ~7% fee on winnings
+  cooldown_minutes: 30,        // Don't re-trade same ticker within 30 min
+};
+
+// ── TRADE_CONFIG Persistence ──
+async function initConfigTable() {
+  try {
+    await kenQ(`
+      CREATE TABLE IF NOT EXISTS ken_config (
+        key TEXT PRIMARY KEY,
+        value JSONB NOT NULL,
+        updated_at TIMESTAMPTZ DEFAULT NOW()
+      )
+    `);
+  } catch (e) {
+    console.log(`[Ken] Config table init error: ${e.message}`);
+  }
+}
+
+async function loadTradeConfig() {
+  try {
+    await initConfigTable();
+    const { rows } = await kenQ("SELECT value FROM ken_config WHERE key = 'trade_config'");
+    if (rows.length > 0) {
+      const saved = rows[0].value;
+      // Merge saved values into TRADE_CONFIG (preserves new keys added in code)
+      for (const [k, v] of Object.entries(saved)) {
+        if (k in TRADE_CONFIG) TRADE_CONFIG[k] = v;
+      }
+      console.log(`[Ken] Loaded TRADE_CONFIG from DB: budget=$${(TRADE_CONFIG.budget_cents/100).toFixed(0)}, conf=${TRADE_CONFIG.min_confidence}%, enabled=${TRADE_CONFIG.enabled}`);
+    }
+  } catch (e) {
+    console.log(`[Ken] Failed to load trade config: ${e.message}`);
+  }
+}
+
+async function saveTradeConfig() {
+  try {
+    await kenQ(`
+      INSERT INTO ken_config (key, value, updated_at) VALUES ('trade_config', $1, NOW())
+      ON CONFLICT (key) DO UPDATE SET value = $1, updated_at = NOW()
+    `, [JSON.stringify(TRADE_CONFIG)]);
+  } catch (e) {
+    console.log(`[Ken] Failed to save trade config: ${e.message}`);
+  }
+}
+
+async function getTraderState() {
+  try {
+    const openTrades = await kenQ('SELECT * FROM ken_trades WHERE status = $1 ORDER BY created_at DESC', ['open']);
+    const today = new Date().toISOString().split('T')[0];
+    const todayPnL = await kenQ("SELECT COALESCE(SUM(pnl_cents), 0) as total FROM ken_trades WHERE closed_at::date = $1::date", [today]);
+    const totalInvested = (openTrades.rows || []).reduce((sum, t) => sum + (t.cost_cents || 0), 0);
+    const dailyLoss = Math.abs(Math.min(0, todayPnL.rows[0]?.total || 0));
+    return {
+      openPositions: openTrades.rows || [],
+      openCount: (openTrades.rows || []).length,
+      totalInvested,
+      dailyLoss,
+      available: TRADE_CONFIG.budget_cents - totalInvested,
+    };
+  } catch (e) {
+    console.log(`[Ken] Trader state error: ${e.message}`);
+    return { openPositions: [], openCount: 0, totalInvested: 0, dailyLoss: 0, available: 0 };
+  }
+}
+
+async function autoTrader(signals) {
+  if (!TRADE_CONFIG.enabled) return;
+
+  try {
+    // Check if API keys are configured
+    const cfgRow = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+    const cfg = cfgRow.rows[0]?.config || {};
+    if (!cfg.kalshi_api_key || !cfg.kalshi_private_key) {
+      return; // Silent - no keys configured yet
+    }
+
+    const state = await getTraderState();
+
+    // Daily loss limit check
+    if (state.dailyLoss >= TRADE_CONFIG.daily_loss_limit_cents) {
+      console.log(`[Ken] TRADER: Daily loss limit hit ($${(state.dailyLoss/100).toFixed(2)}). No new trades today.`);
+      return;
+    }
+
+    // ── 1. CHECK EXISTING POSITIONS FOR EXIT ──
+    for (const trade of state.openPositions) {
+      try {
+        // Get current market price
+        const mktData = await kalshiPublicFetch('GET', `/markets/${encodeURIComponent(trade.ticker)}`);
+        const mkt = mktData.market || mktData;
+        const currentPrice = trade.side === 'yes' ? (mkt.yes_ask || mkt.last_price || 0) : (mkt.no_ask || mkt.last_price || 0);
+
+        // Market resolved?
+        if (mkt.status === 'settled' || mkt.result) {
+          const won = (mkt.result === trade.side);
+          const pnl = won ? (100 - trade.price_cents) * trade.count : -(trade.cost_cents);
+          const feeAdjusted = won ? Math.round(pnl * (1 - TRADE_CONFIG.fee_rate)) : pnl;
+          await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW() WHERE id=$4',
+            ['resolved', won ? 100 : 0, feeAdjusted, trade.id]);
+          const emoji = feeAdjusted > 0 ? ':moneybag:' : ':chart_with_downwards_trend:';
+          await sendSlack(`${emoji} *Ken Trade Resolved*\n*${trade.title}* (${trade.ticker})\nSide: ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Result: ${mkt.result?.toUpperCase()}\nP&L: *${feeAdjusted > 0 ? '+' : ''}${(feeAdjusted/100).toFixed(2)}* (after fees)`);
+          continue;
+        }
+
+        // Take profit check
+        if (currentPrice >= TRADE_CONFIG.take_profit_pct) {
+          try {
+            const sellOrder = await kalshiFetch('POST', '/portfolio/orders', {
+              ticker: trade.ticker, side: trade.side, action: 'sell', type: 'market', count: trade.count,
+            });
+            const sellPrice = currentPrice;
+            const pnl = Math.round((sellPrice - trade.price_cents) * trade.count * (1 - TRADE_CONFIG.fee_rate));
+            await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW(), order_id=$4 WHERE id=$5',
+              ['profit', sellPrice, pnl, sellOrder.order?.order_id || trade.order_id, trade.id]);
+            await sendSlack(`:rocket: *Ken Take Profit!*\n*${trade.title}* (${trade.ticker})\nBought ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Sold @ ${sellPrice}¢\nP&L: *+$${(pnl/100).toFixed(2)}* :moneybag:`);
+          } catch (e) {
+            console.log(`[Ken] TRADER: Take profit failed for ${trade.ticker}: ${e.message}`);
+          }
+          continue;
+        }
+
+        // Stop loss check
+        const unrealizedLoss = (trade.price_cents - currentPrice) * trade.count;
+        if (unrealizedLoss > trade.cost_cents * (TRADE_CONFIG.stop_loss_pct / 100)) {
+          try {
+            const sellOrder = await kalshiFetch('POST', '/portfolio/orders', {
+              ticker: trade.ticker, side: trade.side, action: 'sell', type: 'market', count: trade.count,
+            });
+            const sellPrice = currentPrice;
+            const pnl = (sellPrice - trade.price_cents) * trade.count;
+            await kenQ('UPDATE ken_trades SET status=$1, exit_price=$2, pnl_cents=$3, closed_at=NOW() WHERE id=$4',
+              ['stopped', sellPrice, pnl, trade.id]);
+            await sendSlack(`:stop_sign: *Ken Stop Loss*\n*${trade.title}* (${trade.ticker})\nBought ${trade.side.toUpperCase()} @ ${trade.price_cents}¢ → Stopped @ ${sellPrice}¢\nP&L: *-$${(Math.abs(pnl)/100).toFixed(2)}*`);
+          } catch (e) {
+            console.log(`[Ken] TRADER: Stop loss failed for ${trade.ticker}: ${e.message}`);
+          }
+          continue;
+        }
+      } catch (e) {
+        console.log(`[Ken] TRADER: Position check failed for ${trade.ticker}: ${e.message}`);
+      }
+    }
+
+    // ── 2. LOOK FOR NEW TRADES ──
+    // Refresh state after exits
+    const freshState = await getTraderState();
+    if (freshState.openCount >= TRADE_CONFIG.max_open_positions) {
+      console.log(`[Ken] TRADER: Max positions (${freshState.openCount}/${TRADE_CONFIG.max_open_positions}). Waiting for exits.`);
+      return;
+    }
+
+    // Filter signals: high confidence, multiple sources, tradeable price
+    const tradeable = signals.filter(s =>
+      s.confidence >= TRADE_CONFIG.min_confidence &&
+      (s.sources?.length || 0) >= TRADE_CONFIG.min_sources &&
+      s.entry_price > 5 && s.entry_price < 90 && // Don't trade near extremes
+      s.ticker
+    ).sort((a, b) => b.confidence - a.confidence);
+
+    // Check cooldown: don't re-trade same ticker
+    const recentTickers = new Set();
+    try {
+      const recent = await kenQ("SELECT ticker FROM ken_trades WHERE created_at > NOW() - INTERVAL '" + TRADE_CONFIG.cooldown_minutes + " minutes'");
+      for (const r of (recent.rows || [])) recentTickers.add(r.ticker);
+    } catch {}
+    // Also skip tickers we already have open positions on
+    for (const t of freshState.openPositions) recentTickers.add(t.ticker);
+
+    let tradesPlaced = 0;
+    for (const sig of tradeable) {
+      if (freshState.openCount + tradesPlaced >= TRADE_CONFIG.max_open_positions) break;
+      if (recentTickers.has(sig.ticker)) continue;
+
+      // Calculate position size: max $2, but respect available budget
+      const priceCents = sig.entry_price;
+      const costPerContract = priceCents; // Cost = price in cents for YES; (100 - price) for NO
+      const maxContracts = Math.floor(Math.min(TRADE_CONFIG.max_position_cents, freshState.available) / costPerContract);
+      if (maxContracts < 1) continue;
+      const count = Math.min(maxContracts, 3); // Max 3 contracts per trade
+      const totalCost = count * costPerContract;
+
+      try {
+        const side = sig.side === 'NO' ? 'no' : 'yes';
+        const order = await kalshiFetch('POST', '/portfolio/orders', {
+          ticker: sig.ticker,
+          side: side,
+          action: 'buy',
+          type: 'limit',
+          count: count,
+          yes_price: side === 'yes' ? priceCents : undefined,
+          no_price: side === 'no' ? (100 - priceCents) : undefined,
+          time_in_force: 'gtc',
+        });
+
+        const orderId = order.order?.order_id || `ken_${Date.now()}`;
+        await kenQ(`INSERT INTO ken_trades (ticker, event_ticker, title, side, action, price_cents, count, cost_cents, order_id, signal_type, confidence, reasoning, status)
+          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 'open')`,
+          [sig.ticker, sig.event_ticker || '', sig.market || '', side, 'buy', priceCents, count, totalCost,
+           orderId, sig.signal_type || '', sig.confidence, sig.reason || '']);
+
+        recentTickers.add(sig.ticker);
+        tradesPlaced++;
+        freshState.available -= totalCost;
+
+        const kLink = kalshiLink(sig.ticker, sig.event_ticker);
+        await sendSlack(`:chart_with_upwards_trend: *Ken Auto-Trade Placed!*\n${confBadge(sig.confidence)} *${sig.market}*\n${side.toUpperCase()} x${count} @ ${priceCents}¢ = $${(totalCost/100).toFixed(2)}\nSignal: ${sig.signal_type} | ${sig.reason}\n${kLink}\n_Budget remaining: $${(freshState.available/100).toFixed(2)}_`);
+        console.log(`[Ken] TRADER: ${side.toUpperCase()} x${count} ${sig.ticker} @ ${priceCents}¢ ($${(totalCost/100).toFixed(2)}) — ${sig.signal_type} ${sig.confidence}%`);
+
+      } catch (e) {
+        console.log(`[Ken] TRADER: Order failed for ${sig.ticker}: ${e.message}`);
+      }
+    }
+
+    if (tradesPlaced > 0) {
+      console.log(`[Ken] TRADER: ${tradesPlaced} new trades placed. Open: ${freshState.openCount + tradesPlaced}/${TRADE_CONFIG.max_open_positions}`);
+    }
+  } catch (e) {
+    console.log(`[Ken] TRADER ERROR: ${e.message}`);
+  }
+}
+
+// ══════════════════════════════════════════════
+// MAIN AUTONOMOUS SCAN (every 15 min, 24/7)
+// ══════════════════════════════════════════════
+async function autonomousScan() {
+  const scanStart = Date.now();
+  console.log('[Ken] ═══ AUTONOMOUS SCAN STARTING ═══');
+  try {
+    // 1. Refresh weather data
+    const cities = ['New York', 'Chicago', 'Miami', 'Denver', 'Los Angeles', 'Seattle', 'Houston', 'Phoenix', 'Minneapolis', 'Boston'];
+    const weatherData = generateSyntheticWeather(cities);
+
+    // Cache in DB
+    const row = (await q('SELECT id, config FROM risk_state ORDER BY updated_at DESC LIMIT 1')).rows[0];
+    if (row) {
+      const newCfg = { ...(row.config || {}), weather_cache: { results: weatherData, fetched_at: new Date().toISOString(), synthetic: true } };
+      await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newCfg), row.id]);
+    }
+
+    // 2. Fetch ALL live Kalshi events with nested markets (retry + fallback to prod)
+    let allEvents = [];
+    let weatherEvents = [];
+    let allMarkets = [];
+    const apiUrls = [KALSHI_BASE_URL, 'https://api.elections.kalshi.com/trade-api/v2', 'https://demo-api.kalshi.co/trade-api/v2'];
+    for (let attempt = 0; attempt < apiUrls.length; attempt++) {
+      try {
+        const url = apiUrls[attempt] + '/events?status=open&limit=200&with_nested_markets=true';
+        const controller = new AbortController();
+        const timeout = setTimeout(() => controller.abort(), 15000);
+        const res = await fetch(url, { headers: { 'Accept': 'application/json' }, signal: controller.signal });
+        clearTimeout(timeout);
+        if (!res.ok) throw new Error(`HTTP ${res.status}`);
+        const evData = await res.json();
+        allEvents = evData.events || [];
+        if (allEvents.length > 0) {
+          if (attempt > 0) console.log(`[Ken] Fallback API ${attempt + 1} worked`);
+          break;
+        }
+      } catch (e) {
+        console.log(`[Ken] API ${attempt + 1}/${apiUrls.length} failed: ${e.message}`);
+        if (attempt < apiUrls.length - 1) await new Promise(r => setTimeout(r, 3000));
+      }
+    }
+    // Build market list from events
+    for (const evt of allEvents) {
+      if (evt.markets) {
+        for (const m of evt.markets) {
+          allMarkets.push({
+            ticker: m.ticker,
+            event_ticker: evt.event_ticker,
+            title: evt.title,
+            subtitle: m.subtitle || m.ticker,
+            volume: m.volume || 0,
+            volume_24h: m.volume_24h || 0,
+            open_interest: m.open_interest || 0,
+            yes_bid: m.yes_bid || 0,
+            yes_ask: m.yes_ask || 0,
+            no_bid: m.no_bid || 0,
+            no_ask: m.no_ask || 0,
+            last_price: m.last_price || 0,
+            prev_price: m.previous_price || m.last_price || 0,
+            status: m.status,
+            category: evt.category || '',
+          });
+        }
+      }
+    }
+    // Filter weather events for model-based predictions
+    weatherEvents = allEvents.filter(e => {
+      const t = ((e.category || '') + ' ' + (e.title || '')).toLowerCase();
+      return t.includes('weather') || t.includes('temperature') || t.includes('temp ') ||
+             t.includes('rain') || t.includes('snow') || t.includes('precipitation') ||
+             t.includes('hurricane') || t.includes('wind') || t.includes('°f');
+    });
+    console.log(`[Ken] Kalshi: ${allEvents.length} events, ${allMarkets.length} markets, ${weatherEvents.length} weather`);
+
+    const month = new Date().getMonth() + 1;
+    const predictions = [];
+
+    // 3. Score weather events against our weather model
+    for (const evt of weatherEvents) {
+      const city = extractCity(evt.title);
+      if (!city) continue;
+      const spec = parseWeatherEvent(evt.title);
+      if (!spec || spec.variable === 'unknown') continue;
+      const cityWeather = weatherData.find(w => w.city === city);
+      if (!cityWeather) continue;
+      const features = cityWeather[spec.variable === 'snow' ? 'precipitation' : spec.variable] || cityWeather.temperature;
+      if (!features?.values?.length) continue;
+
+      const rawProb = ensembleExceedance(features, spec.threshold, spec.operator);
+      const prior = climatologicalPrior(spec.variable, spec.threshold, month);
+      const combined = bayesianUpdate(prior, rawProb ?? 0.5);
+      const pYes = Math.max(0.01, Math.min(0.99, combined));
+
+      let marketPrice = 50;
+      const mkt = (evt.markets || [])[0];
+      if (mkt) {
+        const p = mkt.yes_ask || mkt.last_price || 50;
+        if (p > 0 && p < 100) marketPrice = p;
+      }
+
+      const edgeBps = Math.round((pYes * 100 - marketPrice) * 100);
+      predictions.push({
+        market: evt.title,
+        city,
+        side: edgeBps > 0 ? 'yes' : 'no',
+        p_yes: Math.round(pYes * 1000) / 10,
+        price_cents: marketPrice,
+        edge_bps: edgeBps,
+        variable: spec.variable,
+        threshold: spec.threshold,
+        type: 'weather_model',
+      });
+    }
+
+    // 4. Scan ALL high-volume markets for volume-based signals
+    const volumeIdeas = [];
+
+    // Sort by 24h volume to find what's hot
+    const activeMarkets = allMarkets
+      .filter(m => m.yes_bid > 0 && m.yes_ask > 0 && m.yes_ask < 100 && m.last_price > 0)
+      .sort((a, b) => b.volume_24h - a.volume_24h);
+
+    // Cache for 5-minute signal pipeline
+    cachedActiveMarkets = activeMarkets;
+
+    // Load price momentum from DB (compare current vs 2h ago and 6h ago)
+    let momentumMap = {};
+    // Also update cached version for signal pipeline MC simulations
+    try {
+      const { rows: momentumRows } = await kenQ(`
+        WITH latest AS (
+          SELECT DISTINCT ON (ticker) ticker, last_price as current_price, yes_bid, yes_ask, captured_at
+          FROM ken_market_snapshots ORDER BY ticker, captured_at DESC
+        ),
+        two_hours AS (
+          SELECT DISTINCT ON (ticker) ticker, last_price as price_2h
+          FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '2 hours'
+          ORDER BY ticker, captured_at DESC
+        ),
+        six_hours AS (
+          SELECT DISTINCT ON (ticker) ticker, last_price as price_6h
+          FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '6 hours'
+          ORDER BY ticker, captured_at DESC
+        )
+        SELECT l.ticker, l.current_price, t.price_2h, s.price_6h,
+               l.current_price - COALESCE(t.price_2h, l.current_price) as delta_2h,
+               l.current_price - COALESCE(s.price_6h, l.current_price) as delta_6h
+        FROM latest l LEFT JOIN two_hours t ON l.ticker = t.ticker LEFT JOIN six_hours s ON l.ticker = s.ticker
+      `);
+      for (const r of momentumRows) {
+        momentumMap[r.ticker] = { delta_2h: parseInt(r.delta_2h) || 0, delta_6h: parseInt(r.delta_6h) || 0, price_2h: r.price_2h, price_6h: r.price_6h };
+      }
+    } catch (e) { console.log('[Ken] Momentum query error:', e.message); }
+    cachedMomentumMap = momentumMap; // Update shared cache for signal pipeline
+
+    // ═══ INSTANT SLACK ALERT: Major Price Movements (>=10¢ in 2h or >=15¢ in 6h) ═══
+    try {
+      const bigMovers = Object.entries(momentumMap)
+        .filter(([, m]) => Math.abs(m.delta_2h) >= 10 || Math.abs(m.delta_6h) >= 15)
+        .sort((a, b) => Math.abs(b[1].delta_2h) - Math.abs(a[1].delta_2h));
+      if (bigMovers.length > 0) {
+        const moverLines = bigMovers.slice(0, 8).map(([ticker, m]) => {
+          const mkt = activeMarkets.find(am => am.ticker === ticker);
+          const title = mkt ? mkt.title : ticker;
+          const d2 = m.delta_2h > 0 ? `+${m.delta_2h}` : `${m.delta_2h}`;
+          const d6 = m.delta_6h > 0 ? `+${m.delta_6h}` : `${m.delta_6h}`;
+          const icon = Math.abs(m.delta_2h) >= 10 ? (m.delta_2h > 0 ? ':chart_with_upwards_trend:' : ':chart_with_downwards_trend:') : ':left_right_arrow:';
+          const et = mkt?.event_ticker || ticker?.replace(/-[^-]+$/, '') || ticker;
+          const link = `<https://kalshi.com/markets/${et}/${ticker}|View>`;
+          return `  ${icon} *${title}*\n     2h: *${d2}¢* | 6h: *${d6}¢* | Now: ${mkt?.last_price || '?'}¢ | ${link}`;
+        }).join('\n');
+        const timeStr = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles', hour: 'numeric', minute: '2-digit', hour12: true });
+        await sendSlack(`:rotating_light: *Major Price Movements* (${timeStr} PT)\n_${bigMovers.length} market(s) moved 10+¢ in 2h or 15+¢ in 6h_\n\n${moverLines}\n\nDashboard: http://45.61.58.125:7810`);
+        console.log(`[Ken] SLACK ALERT: ${bigMovers.length} major price movement(s) sent`);
+      }
+    } catch (e) { console.log('[Ken] Price movement alert error:', e.message); }
+
+    for (const m of activeMarkets.slice(0, 200)) {
+      const spread = m.yes_ask - m.yes_bid;
+      const noPrice = 100 - m.yes_ask;
+      const momentum = momentumMap[m.ticker] || { delta_2h: 0, delta_6h: 0 };
+
+      // ── SHORT: YES >= 75¢ (buy NO cheap, wider net than before) ──
+      if (m.yes_bid >= 75 && noPrice <= 25) {
+        let shortConf = 40;
+        if (noPrice <= 5) shortConf += 15;      // dirt cheap NO
+        else if (noPrice <= 10) shortConf += 10;
+        else if (noPrice <= 15) shortConf += 5;
+        if (momentum.delta_2h < 0) shortConf += 10;  // price dropping = good short
+        if (momentum.delta_6h < -3) shortConf += 10;  // strong downtrend
+        if (m.volume_24h > 5000) shortConf += 5;
+        if (m.open_interest > 100000) shortConf += 5;
+        volumeIdeas.push({
+          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
+          reason: `YES at ${m.yes_bid}¢ → NO at ${noPrice}¢${momentum.delta_2h ? ` (${momentum.delta_2h > 0 ? '+' : ''}${momentum.delta_2h}¢/2h)` : ''}`,
+          volume_24h: m.volume_24h, open_interest: m.open_interest,
+          yes_price: m.yes_ask, no_price: noPrice, spread,
+          type: 'short_high_price', confidence: Math.min(90, shortConf),
+        });
+      }
+
+      // ── MOMENTUM SHORT: price dropping fast (was 50+, fell 5+ in 2h) ──
+      if (momentum.delta_2h <= -5 && m.last_price >= 30 && m.last_price <= 80) {
+        volumeIdeas.push({
+          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
+          reason: `Dropping ${momentum.delta_2h}¢/2h (${momentum.delta_6h || '?'}¢/6h) — momentum sell`,
+          volume_24h: m.volume_24h, open_interest: m.open_interest,
+          yes_price: m.yes_ask, no_price: noPrice, spread,
+          type: 'momentum_short', confidence: Math.min(80, 50 + Math.abs(momentum.delta_2h) * 2),
+        });
+      }
+
+      // ── MOMENTUM LONG: price rising fast (was low, jumped 5+ in 2h) ──
+      if (momentum.delta_2h >= 5 && m.last_price <= 70 && m.last_price >= 10) {
+        volumeIdeas.push({
+          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'YES',
+          reason: `Rising +${momentum.delta_2h}¢/2h (+${momentum.delta_6h || '?'}¢/6h) — momentum buy`,
+          volume_24h: m.volume_24h, open_interest: m.open_interest,
+          yes_price: m.yes_ask, no_price: noPrice, spread,
+          type: 'momentum_long', confidence: Math.min(80, 50 + momentum.delta_2h * 2),
+        });
+      }
+
+      // ── LONG CHEAP: YES <= 20¢ with volume (wider net) ──
+      if (m.yes_ask <= 20 && m.volume_24h > 100) {
+        let longConf = 35;
+        if (m.yes_ask <= 5) longConf += 5;        // lottery ticket
+        if (m.volume_24h > 1000) longConf += 10;  // lots of interest
+        if (m.volume_24h > 5000) longConf += 10;
+        if (momentum.delta_2h > 0) longConf += 10; // price rising
+        if (momentum.delta_6h > 2) longConf += 10;
+        volumeIdeas.push({
+          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'YES',
+          reason: `YES at ${m.yes_ask}¢, vol ${m.volume_24h.toLocaleString()}${momentum.delta_2h ? ` (${momentum.delta_2h > 0 ? '+' : ''}${momentum.delta_2h}¢/2h)` : ''}`,
+          volume_24h: m.volume_24h, open_interest: m.open_interest,
+          yes_price: m.yes_ask, no_price: noPrice, spread,
+          type: 'long_cheap', confidence: Math.min(80, longConf),
+        });
+      }
+
+      // ── CONTRARIAN SHORT: YES at 60-84¢ with negative momentum ──
+      if (m.yes_bid >= 60 && m.yes_bid < 85 && momentum.delta_2h < -2 && m.volume_24h > 200) {
+        volumeIdeas.push({
+          market: m.title, subtitle: m.subtitle, ticker: m.ticker, event_ticker: m.event_ticker, side: 'NO',
+          reason: `YES fading from ${(m.last_price + Math.abs(momentum.delta_2h))}¢→${m.last_price}¢, NO at ${noPrice}¢`,
+          volume_24h: m.volume_24h, open_interest: m.open_interest,
+          yes_price: m.yes_ask, no_price: noPrice, spread,
+          type: 'contrarian_short', confidence: Math.min(75, 45 + Math.abs(momentum.delta_2h) * 3),
+        });
+      }
+
+      // ── VOLUME SPIKE (kept but enhanced with momentum) ──
+      if (m.volume > 0 && m.volume_24h / m.volume > 0.05 && m.volume_24h > 500) {
+        volumeIdeas.push({
+          market: m.title,
+          subtitle: m.subtitle,
+          ticker: m.ticker,
+          event_ticker: m.event_ticker,
+          side: m.last_price > m.prev_price ? 'YES momentum' : 'NO momentum',
+          reason: `24h vol ${m.volume_24h.toLocaleString()} = ${Math.round(m.volume_24h / m.volume * 100)}% of all-time`,
+          volume_24h: m.volume_24h,
+          open_interest: m.open_interest,
+          yes_price: m.yes_ask,
+          no_price: noPrice,
+          spread,
+          type: 'volume_spike',
+        });
+      }
+
+      // Spread opportunity: wide spread with volume = market-making opportunity
+      if (spread >= 5 && m.volume_24h > 100 && m.open_interest > 1000) {
+        volumeIdeas.push({
+          market: m.title,
+          subtitle: m.subtitle,
+          ticker: m.ticker,
+          event_ticker: m.event_ticker,
+          side: 'SPREAD',
+          reason: `${spread}¢ spread, bid ${m.yes_bid}¢ / ask ${m.yes_ask}¢`,
+          volume_24h: m.volume_24h,
+          open_interest: m.open_interest,
+          yes_price: m.yes_ask,
+          no_price: noPrice,
+          spread,
+          type: 'wide_spread',
+        });
+      }
+    }
+
+    // Deduplicate volume ideas by ticker (keep highest confidence)
+    const deduped = new Map();
+    for (const idea of volumeIdeas) {
+      const existing = deduped.get(idea.ticker);
+      if (!existing || (idea.confidence || 50) > (existing.confidence || 50)) {
+        deduped.set(idea.ticker, idea);
+      }
+    }
+    const uniqueVolumeIdeas = [...deduped.values()];
+
+    logMemory('scan-after-signals');
+
+    // 5. Store market snapshots in DB (batch of 150 to capture more markets)
+    for (const m of activeMarkets.slice(0, 150)) {
+      try {
+        await kenQ(`
+          INSERT INTO ken_market_snapshots (ticker, event_ticker, title, subtitle, category, yes_bid, yes_ask, no_bid, no_ask, last_price, volume, volume_24h, open_interest, spread)
+          VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
+        `, [m.ticker, m.event_ticker, m.title, m.subtitle, m.category, m.yes_bid, m.yes_ask, m.no_bid, m.no_ask, m.last_price, m.volume, m.volume_24h, m.open_interest, m.yes_ask - m.yes_bid]);
+      } catch {}
+    }
+
+    // 6. Get sentiment data from last 2 hours for signal boosting
+    let sentimentBoosts = {};
+    try {
+      const { rows: recentSentiment } = await kenQ(`
+        SELECT relevance_ticker, AVG(sentiment_score) as avg_sent, COUNT(*) as post_count, SUM(score) as total_score
+        FROM ken_sentiment
+        WHERE captured_at > NOW() - INTERVAL '2 hours' AND relevance_ticker IS NOT NULL
+        GROUP BY relevance_ticker
+      `);
+      for (const s of recentSentiment) {
+        sentimentBoosts[s.relevance_ticker] = {
+          avgSentiment: parseFloat(s.avg_sent) || 0,
+          postCount: parseInt(s.post_count) || 0,
+          totalScore: parseInt(s.total_score) || 0,
+        };
+      }
+    } catch {}
+
+    // Get recent news sentiment (non-ticker-specific)
+    let newsSentiment = {};
+    try {
+      const { rows: newsRows } = await kenQ(`
+        SELECT keyword, AVG(sentiment_score) as avg_sent, COUNT(*) as cnt
+        FROM ken_sentiment
+        WHERE source = 'google_news' AND captured_at > NOW() - INTERVAL '4 hours'
+        GROUP BY keyword
+      `);
+      for (const n of newsRows) {
+        newsSentiment[n.keyword] = { avg: parseFloat(n.avg_sent) || 0, count: parseInt(n.cnt) || 0 };
+      }
+    } catch {}
+
+    // 7. Polymarket cross-reference for arbitrage
+    let polyArbs = [];
+    try {
+      polyArbs = await polymarketCrossRef(activeMarkets);
+    } catch (e) {
+      console.log('[Ken] Polymarket cross-ref failed:', e.message);
+    }
+
+    // 7b. Energy/LNG cross-market arbitrage scan
+    let energyArbs = [];
+    try {
+      energyArbs = await energyArbitrageScan(activeMarkets);
+    } catch (e) {
+      console.log('[Ken] Energy arb scan failed:', e.message);
+    }
+
+    // 8. Combine ALL signals with confidence scoring
+    const buySignals = predictions.filter(p => p.edge_bps > 200);
+    const shortSignals = predictions.filter(p => p.edge_bps < -200);
+    const longIdeas = uniqueVolumeIdeas.filter(v => v.side === 'YES' || v.side === 'YES momentum');
+    const shortIdeas = uniqueVolumeIdeas.filter(v => ['short_high_price', 'momentum_short', 'contrarian_short'].includes(v.type));
+    const spikeIdeas = uniqueVolumeIdeas.filter(v => v.type === 'volume_spike');
+    const momentumIdeas = uniqueVolumeIdeas.filter(v => v.type === 'momentum_long' || v.type === 'momentum_short');
+    const spreadIdeas = uniqueVolumeIdeas.filter(v => v.type === 'wide_spread');
+
+    // 9. Composite confidence with sentiment + momentum
+    const allSignals = [];
+    for (const p of [...buySignals, ...shortSignals]) {
+      let confidence = 50 + Math.min(30, Math.abs(p.edge_bps) / 50);
+      const sentBoost = sentimentBoosts[p.market] || {};
+      if (sentBoost.postCount > 3) confidence += sentBoost.avgSentiment > 0 ? 10 : -5;
+      const mom = momentumMap[p.ticker] || {};
+      if (p.edge_bps > 0 && mom.delta_2h > 0) confidence += 8;   // momentum confirms long
+      if (p.edge_bps < 0 && mom.delta_2h < 0) confidence += 8;   // momentum confirms short
+      const sources = ['weather_model'];
+      if (sentBoost.postCount) sources.push('reddit');
+      if (mom.delta_2h) sources.push('momentum');
+      allSignals.push({ ...p, confidence: Math.min(95, Math.round(confidence)), sources, signal_type: p.edge_bps > 0 ? 'long' : 'short' });
+    }
+    for (const v of uniqueVolumeIdeas) {
+      let confidence = v.confidence || 45; // Use pre-calculated confidence from signal detection
+      // Boost with sentiment
+      const sentKey = Object.keys(sentimentBoosts).find(k => v.ticker?.startsWith(k));
+      const sentBoost = sentKey ? sentimentBoosts[sentKey] : null;
+      if (sentBoost && sentBoost.postCount > 3) {
+        if (v.side === 'YES' && sentBoost.avgSentiment > 0.05) confidence += 10;
+        if (v.side === 'NO' && sentBoost.avgSentiment < -0.05) confidence += 10;
+      }
+      // Boost with news
+      for (const [nk, nv] of Object.entries(newsSentiment)) {
+        const titleLower = (v.market || '').toLowerCase();
+        if (nk.split(' ').some(w => w.length > 4 && titleLower.includes(w))) {
+          if (v.side === 'YES' && nv.avg > 0.05) confidence += 5;
+          if (v.side === 'NO' && nv.avg < -0.05) confidence += 5;
+          break;
+        }
+      }
+      const sources = ['volume_analysis'];
+      if (v.type?.includes('momentum')) sources.push('momentum');
+      if (sentBoost) sources.push('reddit');
+      const isShort = v.side === 'NO' || ['short_high_price', 'momentum_short', 'contrarian_short'].includes(v.type);
+      allSignals.push({
+        market: v.market, ticker: v.ticker, event_ticker: v.event_ticker, subtitle: v.subtitle,
+        side: isShort ? 'NO' : v.side === 'SPREAD' ? 'YES' : 'YES',
+        confidence: Math.min(95, Math.round(confidence)),
+        entry_price: isShort ? v.no_price : v.yes_price,
+        edge_bps: isShort ? Math.round((100 - (v.yes_price || 50)) * 100) : 0,
+        signal_type: v.type || 'volume',
+        reason: v.reason, sources, volume_24h: v.volume_24h,
+      });
+    }
+    for (const arb of polyArbs) {
+      allSignals.push({
+        market: arb.kalshi_title, ticker: arb.kalshi_ticker, event_ticker: arb.event_ticker,
+        side: arb.price_diff > 0 ? 'YES' : 'NO',
+        confidence: Math.min(90, 55 + Math.abs(arb.price_diff) * 2),
+        entry_price: arb.kalshi_price,
+        edge_bps: arb.price_diff * 100,
+        signal_type: 'polymarket_arb',
+        reason: `Kalshi ${arb.kalshi_price}¢ vs Poly ${arb.poly_price}¢ (${arb.price_diff > 0 ? '+' : ''}${arb.price_diff}¢ gap | ${arb.match_score}% match)`,
+        sources: ['polymarket_crossref'],
+      });
+    }
+    // Energy/LNG cross-market arb signals
+    for (const arb of energyArbs) {
+      if (arb.kalshi_ticker) {
+        allSignals.push({
+          market: arb.kalshi_title, ticker: arb.kalshi_ticker, event_ticker: arb.event_ticker,
+          side: arb.price_diff > 0 ? 'YES' : 'NO',
+          confidence: Math.min(92, 60 + Math.abs(arb.price_diff) * 2),
+          entry_price: arb.kalshi_price,
+          edge_bps: arb.price_diff * 100,
+          signal_type: 'energy_arb',
+          reason: `ENERGY ARB: Kalshi ${arb.kalshi_price}¢ vs ${arb.other_source} ${arb.other_price}¢ (${arb.price_diff > 0 ? '+' : ''}${arb.price_diff}¢ gap) — BUY ${arb.price_diff > 0 ? 'Kalshi' : arb.other_source}, SELL ${arb.price_diff > 0 ? arb.other_source : 'Kalshi'}`,
+          sources: ['energy_crossmarket_arb', arb.other_source.toLowerCase()],
+        });
+      }
+    }
+
+    // 10. Store predictions in DB
+    for (const sig of allSignals) {
+      try {
+        await kenQ(`
+          INSERT INTO ken_predictions (ticker, event_ticker, title, signal_type, side, confidence, entry_price, edge_bps, reasoning, sources)
+          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
+        `, [
+          sig.ticker || '', sig.event_ticker || '', sig.market || '', sig.signal_type,
+          sig.side, sig.confidence, sig.entry_price || sig.price_cents || 0, sig.edge_bps || 0,
+          sig.reason || sig.reasoning || `${sig.signal_type} signal`, JSON.stringify(sig.sources || []),
+        ]);
+      } catch {}
+    }
+
+    // 10b. AUTO-TRADER: Execute trades on high-confidence signals
+    try {
+      await autoTrader(allSignals);
+    } catch (e) {
+      console.log(`[Ken] Auto-trader error: ${e.message}`);
+    }
+
+    // 10c. PORTFOLIO TRADER: Feed scan signals into paper portfolios
+    let portfolioTradesPlaced = 0;
+    for (const sig of allSignals.filter(s => s.confidence >= 40 && s.ticker)) {
+      try {
+        await autoPortfolioTrade({
+          marketId: sig.ticker,
+          market_title: sig.market || sig.ticker,
+          direction: sig.side === 'YES' ? 'BUY_YES' : 'BUY_NO',
+          expectedEdge: (sig.edge_bps || 0) / 10000,
+          confidence: (sig.confidence || 50) / 100,
+          reasoning: sig.reason || sig.reasoning || sig.signal_type,
+          signal_type: sig.signal_type,
+          aiEnhanced: (sig.sources || []).includes('gemini'),
+          mcSimulation: { consistent: true, stdDev: 0.05 },
+          num_articles: (sig.sources || []).length,
+          agreement: (sig.confidence || 50) / 100,
+        });
+        portfolioTradesPlaced++;
+      } catch {}
+    }
+    if (portfolioTradesPlaced > 0) console.log(`[Ken] Portfolio trades: ${portfolioTradesPlaced} signals fed to paper portfolios`);
+
+    const scanDuration = Date.now() - scanStart;
+
+    // 11. Log performance
+    try {
+      await kenQ(`
+        INSERT INTO ken_performance (total_markets, active_markets, predictions_made, long_signals, short_signals, sentiment_items, scan_duration_ms, slack_sent)
+        VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
+      `, [
+        allMarkets.length, activeMarkets.length, allSignals.length,
+        allSignals.filter(s => s.side === 'YES').length,
+        allSignals.filter(s => s.side === 'NO').length,
+        Object.keys(sentimentBoosts).length,
+        scanDuration, false,
+      ]);
+    } catch {}
+
+    console.log(`[Ken] Scan complete: ${allMarkets.length} markets, ${activeMarkets.length} active, ${allSignals.length} signals (${allSignals.filter(s=>s.side==='YES').length} long, ${allSignals.filter(s=>s.side==='NO').length} short), ${polyArbs.length} arbs, ${Object.keys(sentimentBoosts).length} sentiment boosts | ${scanDuration}ms`);
+
+    // 12a. Run Signal Pipeline (News → Sentiment → Probability → Signals → Risk)
+    try {
+      const pipelineSignals = await signalPipeline(activeMarkets);
+      if (pipelineSignals > 0) {
+        console.log(`[Ken] Signal Pipeline produced ${pipelineSignals} risk-assessed signals`);
+      }
+    } catch (e) {
+      console.log(`[Ken] Signal Pipeline error: ${e.message}`);
+    }
+
+    // 12b. Update market mappings
+    try {
+      await updateMarketMappings(activeMarkets);
+    } catch {}
+
+    // 12. Build and send Slack alert (max once per hour to not spam)
+    const now = Date.now();
+    const shouldSlack = (now - lastSlackTime > SLACK_COOLDOWN_MS) || allSignals.some(s => s.confidence >= 75);
+
+    if (shouldSlack && allSignals.length > 0) {
+      lastSlackTime = now;
+      const timeStr = new Date().toLocaleString('en-US', { timeZone: 'America/New_York', hour: 'numeric', minute: '2-digit', hour12: true });
+      const parts = [];
+
+      parts.push(`:dog: *Ken — Autonomous Scan* (${timeStr} ET)`);
+      parts.push(`_${allMarkets.length} markets | ${activeMarkets.length} active | ${allSignals.length} signals | ${KALSHI_ENV}_\n`);
+
+      // Helper: build Kalshi market link
+      const kalshiLink = (ticker, eventTicker) => {
+        const et = eventTicker || ticker?.replace(/-[^-]+$/, '') || ticker;
+        return `<https://kalshi.com/markets/${et}/${ticker}|Trade on Kalshi>`;
+      };
+
+      // Helper: confidence badge with color (green→yellow→orange→red)
+      const confBadge = (conf) => {
+        if (conf >= 80) return `:large_green_circle: *${conf}%*`;
+        if (conf >= 65) return `:large_yellow_circle: *${conf}%*`;
+        if (conf >= 50) return `:large_orange_circle: *${conf}%*`;
+        return `:red_circle: *${conf}%*`;
+      };
+
+      // Confidence legend
+      parts.push(`_:large_green_circle: 80%+ Strong  :large_yellow_circle: 65-79% Good  :large_orange_circle: 50-64% Moderate  :red_circle: <50% Speculative_`);
+
+      // High-confidence signals first (>= 70)
+      const highConf = allSignals.filter(s => s.confidence >= 70).sort((a, b) => b.confidence - a.confidence);
+      if (highConf.length > 0) {
+        const lines = highConf.slice(0, 5).map((s, i) => {
+          const icon = s.side === 'YES' ? ':chart_with_upwards_trend:' : ':small_red_triangle_down:';
+          const link = kalshiLink(s.ticker, s.event_ticker);
+          return `  ${icon} ${confBadge(s.confidence)} *${s.market}*\n     ${s.side} | ${s.reason || `Edge ${s.edge_bps}bps`}\n     :link: ${link}`;
+        }).join('\n');
+        parts.push(`:rotating_light: *High Confidence Signals*\n${lines}`);
+      }
+
+      // Short ideas
+      const shorts = allSignals.filter(s => s.side === 'NO' && s.confidence >= 50).sort((a, b) => b.confidence - a.confidence);
+      if (shorts.length > 0 && highConf.filter(s => s.side === 'NO').length === 0) {
+        const lines = shorts.slice(0, 3).map((s, i) => {
+          const link = kalshiLink(s.ticker, s.event_ticker);
+          return `  ${confBadge(s.confidence)} *${s.market}* — BUY NO\n     ${s.reason || ''}\n     :link: ${link}`;
+        }).join('\n');
+        parts.push(`:moneybag: *Short Ideas (Buy NO)*\n${lines}`);
+      }
+
+      // Volume spikes
+      if (spikeIdeas.length > 0) {
+        const lines = spikeIdeas.sort((a, b) => b.volume_24h - a.volume_24h).slice(0, 3)
+          .map((p, i) => {
+            const link = kalshiLink(p.ticker, p.event_ticker);
+            const conf = p.confidence || 45;
+            return `  ${confBadge(conf)} *${p.market}*\n     ${p.reason}\n     :link: ${link}`;
+          }).join('\n');
+        parts.push(`:fire: *Volume Spikes*\n${lines}`);
+      }
+
+      // Momentum moves
+      if (momentumIdeas.length > 0) {
+        const lines = momentumIdeas.sort((a, b) => (b.confidence || 0) - (a.confidence || 0)).slice(0, 4)
+          .map((p, i) => {
+            const icon = p.type === 'momentum_long' ? ':arrow_up:' : ':arrow_down:';
+            const link = kalshiLink(p.ticker, p.event_ticker);
+            const conf = p.confidence || 50;
+            return `  ${icon} ${confBadge(conf)} *${p.market}*\n     ${p.subtitle || ''} — ${p.reason}\n     :link: ${link}`;
+          }).join('\n');
+        parts.push(`:rocket: *Momentum Moves*\n${lines}`);
+      }
+
+      // Polymarket arbitrage
+      if (polyArbs.length > 0) {
+        const lines = polyArbs.slice(0, 5)
+          .map(a => {
+            const conf = Math.min(90, 55 + Math.abs(a.price_diff) * 2);
+            const kLink = kalshiLink(a.kalshi_ticker, a.event_ticker);
+            const pLink = `<https://polymarket.com/event/${a.poly_slug}|Polymarket>`;
+            return `  ${confBadge(conf)} *${a.kalshi_title}* (${a.kalshi_subtitle || ''})\n     Kalshi: ${a.kalshi_price}¢ vs Poly: ${a.poly_price}¢ → *${a.arb_side}*\n     ${kLink} | ${pLink} | Match: ${a.match_score}%`;
+          }).join('\n');
+        parts.push(`:scales: *Polymarket Arbitrage (${polyArbs.length} found)*\n${lines}`);
+      }
+
+      // Reddit buzz
+      const buzzKeys = Object.entries(sentimentBoosts).filter(([, v]) => v.postCount >= 3).sort((a, b) => b[1].totalScore - a[1].totalScore);
+      if (buzzKeys.length > 0) {
+        const lines = buzzKeys.slice(0, 3).map(([ticker, v]) => {
+          const sentIcon = v.avgSentiment > 0.1 ? ':large_green_circle:' : v.avgSentiment < -0.1 ? ':red_circle:' : ':large_yellow_circle:';
+          return `  ${sentIcon} ${ticker}: ${v.postCount} posts, sentiment ${v.avgSentiment > 0 ? '+' : ''}${v.avgSentiment.toFixed(2)}, karma ${v.totalScore}`;
+        }).join('\n');
+        parts.push(`:speech_balloon: *Reddit Buzz*\n${lines}`);
+      }
+
+      // Auto-trader portfolio summary
+      try {
+        const traderState = await getTraderState();
+        const closed = await kenQ("SELECT COUNT(*) as total, SUM(CASE WHEN pnl_cents > 0 THEN 1 ELSE 0 END) as wins, SUM(CASE WHEN pnl_cents <= 0 THEN 1 ELSE 0 END) as losses, COALESCE(SUM(pnl_cents), 0) as pnl FROM ken_trades WHERE status != 'open'");
+        const cs = closed.rows[0] || {};
+        const pnl = parseInt(cs.pnl || 0);
+        const pnlStr = `${pnl >= 0 ? '+' : ''}$${(pnl/100).toFixed(2)}`;
+        const pnlEmoji = pnl > 0 ? ':moneybag:' : pnl < 0 ? ':chart_with_downwards_trend:' : ':neutral_face:';
+        const openLines = traderState.openPositions.map(t =>
+          `  • ${t.side.toUpperCase()} ${t.ticker} x${t.count} @ ${t.price_cents}¢ ($${(t.cost_cents/100).toFixed(2)})`
+        ).join('\n');
+        parts.push(`:bank: *Auto-Trader Portfolio*\n` +
+          `Budget: $${(TRADE_CONFIG.budget_cents/100).toFixed(2)} | Invested: $${(traderState.totalInvested/100).toFixed(2)} | Available: $${(traderState.available/100).toFixed(2)}\n` +
+          `Open: ${traderState.openCount}/${TRADE_CONFIG.max_open_positions} positions` +
+          (openLines ? `\n${openLines}` : '') + `\n` +
+          `${pnlEmoji} P&L: *${pnlStr}* (${cs.wins || 0}W/${cs.losses || 0}L) | Today loss: $${(traderState.dailyLoss/100).toFixed(2)}/$${(TRADE_CONFIG.daily_loss_limit_cents/100).toFixed(2)}`);
+      } catch {}
+
+      // Performance stats
+      try {
+        const { rows: perfRows } = await kenQ(`
+          SELECT COUNT(*) as total_preds,
+            SUM(CASE WHEN outcome='win' THEN 1 ELSE 0 END) as wins,
+            SUM(CASE WHEN outcome='loss' THEN 1 ELSE 0 END) as losses
+          FROM ken_predictions WHERE created_at > NOW() - INTERVAL '24 hours'
+        `);
+        const perf = perfRows[0];
+        if (perf && parseInt(perf.total_preds) > 0) {
+          parts.push(`_24h stats: ${perf.total_preds} predictions, ${perf.wins || 0}W/${perf.losses || 0}L_`);
+        }
+      } catch {}
+
+      parts.push(`\nDashboard: http://45.61.58.125:7810`);
+
+      await sendSlack(parts.join('\n\n'));
+
+      // Update performance row
+      try {
+        await kenQ(`UPDATE ken_performance SET slack_sent = true WHERE id = (SELECT MAX(id) FROM ken_performance)`);
+      } catch {}
+    } else if (allSignals.length > 0) {
+      const cooldownLeft = Math.max(0, Math.round((SLACK_COOLDOWN_MS - (now - lastSlackTime)) / 60000));
+      console.log(`[Ken] ${allSignals.length} signals found, Slack cooldown: ${cooldownLeft}min left`);
+    } else {
+      console.log(`[Ken] No signals this cycle (${allMarkets.length} markets scanned)`);
+    }
+
+  } catch (err) {
+    console.error('[Ken] Autonomous scan error:', err.message);
+    try { await sendSlack(`:warning: *Ken Error* — Scan failed: ${err.message}`); } catch {}
+  }
+}
+
+// ══════════════════════════════════════════════
+// AUTO-PORTFOLIO TRADING ENGINE — 10 personalities, $20/day each
+// ══════════════════════════════════════════════
+
+// Portfolio strategy filters — each personality decides differently
+// ═══ Portfolio Strategy Filters ═══
+// Each strategy is deliberately exclusive — a signal should match 1-3 portfolios, not all 10.
+// Strategies use edge/confidence/direction/article count/MC/AI to create real diversification.
+const PORTFOLIO_STRATEGIES = {
+  // Alpha: High edge (>15%) with LOWER confidence (<70%). Big edges, less certainty — speculative.
+  aggressive: (sig) => Math.abs(sig.edge) >= 0.15 && sig.confidence < 0.70,
+
+  // Beta: High confidence (>70%) with ANY edge. The "safe" bet — trusts confidence over edge size.
+  conservative: (sig) => sig.confidence >= 0.70 && Math.abs(sig.edge) >= 0.05,
+
+  // Storm: Weather/climate markets identified by market_id patterns or reasoning keywords
+  weather: (sig) => /weather|temp|rain|snow|wind|hurricane|precip|storm|°f|heat|cold|frost|drought|flood|tornado|KXTEMP|KXRAIN|KXSNOW|KXHURR/i.test((sig.reasoning || '') + ' ' + (sig.market_id || '')),
+
+  // Momentum: Only trades when edge increased (high agreement = many articles agree).
+  // Needs 3+ articles with >65% agreement — evidence of growing consensus.
+  momentum: (sig) => (sig.agreement || 0) >= 0.65 && (sig.num_articles || 0) >= 3 && Math.abs(sig.edge) >= 0.05,
+
+  // Contrarian: ONLY BUY_NO trades. Bets against the crowd. Never buys YES.
+  contrarian: (sig) => sig.direction === 'BUY_NO',
+
+  // Nova: Only Gemini AI-enhanced signals. If AI didn't weigh in, skip.
+  ai_enhanced: (sig) => sig.ai_enhanced === true,
+
+  // Oracle: MC-validated WITH tight spread (stdDev < 8%) AND moderate edge. The "careful" bot.
+  mc_validated: (sig) => sig.mc_consistent === true && (sig.mc_stddev || 99) < 0.08 && Math.abs(sig.edge) >= 0.05 && Math.abs(sig.edge) < 0.20,
+
+  // Shark: Small edges (3-10%), rapid fire. Takes quantity over quality. Rejects big edges.
+  scalper: (sig) => Math.abs(sig.edge) >= 0.03 && Math.abs(sig.edge) < 0.10 && sig.confidence >= 0.30,
+
+  // Turtle: Ultra-patient. Needs EVERYTHING: 5+ articles, MC consistent, confidence >80%, moderate edge.
+  patient: (sig) => sig.confidence >= 0.80 && sig.mc_consistent === true && (sig.num_articles || 0) >= 5 && Math.abs(sig.edge) >= 0.05,
+
+  // Wildcard: Random 20% of signals — true control group. Lower chance than before to create separation.
+  random: (sig) => Math.random() < 0.20,
+
+  // ═══ RISKY PORTFAUXLIOS: Micro → Macro ═══
+
+  // Penny: Any edge >= 1%. Catches everything. Micro bets, max volume.
+  penny_pincher: (sig) => Math.abs(sig.edge) >= 0.01,
+
+  // Spray: Random 50% of ALL signals. Shotgun approach — tests luck vs skill.
+  spray_pray: (sig) => Math.random() < 0.50,
+
+  // Nickel: Low bar (edge >= 2%, conf >= 20%). Catches most signals.
+  nickel_slots: (sig) => Math.abs(sig.edge) >= 0.02 && sig.confidence >= 0.20,
+
+  // Degen: Edge >= 5%, ignores confidence entirely. Pure edge chaser.
+  degen_lite: (sig) => Math.abs(sig.edge) >= 0.05,
+
+  // Politics: Only political markets. KXPRES, KXSEN, KXGOV, KXCONGRESS, KXPRESPERSON, etc.
+  politics_junkie: (sig) => /KXPRES|KXSEN|KXGOV|KXCONG|KXHOUSE|KXELEC|KXPARTY|KXPRESPERSON|political|election|congress|senate|president|governor/i.test((sig.market_id || '') + ' ' + (sig.reasoning || '')),
+
+  // Double: Only high-edge signals (>20%). Goes big when edge is massive.
+  double_down: (sig) => Math.abs(sig.edge) >= 0.20,
+
+  // Night: Only signals with high MC stddev (>8%) — volatile markets others reject.
+  volatility_rider: (sig) => (sig.mc_stddev || 0) >= 0.08 && Math.abs(sig.edge) >= 0.05,
+
+  // Whale: Edge >25% AND confidence >60%. Rare but massive. The sniper.
+  whale: (sig) => Math.abs(sig.edge) >= 0.25 && sig.confidence >= 0.60,
+
+  // YOLO: Edge >30%. Doesn't care about anything else. Max degen.
+  yolo: (sig) => Math.abs(sig.edge) >= 0.30,
+
+  // Full Send: Takes EVERY signal. Max exposure. Tests whether the signal pipeline itself is profitable.
+  full_send: (sig) => true,
+
+  // ═══ CPU-POWERED: Computational / Multi-Factor Strategies ═══
+
+  // Kelly: Only trades when Kelly fraction (edge / odds) > 0.10. Classic bankroll math.
+  // Kelly f* = (bp - q) / b where b=odds, p=prob_win, q=prob_lose. Simplified: edge/(1+edge).
+  kelly_criterion: (sig) => {
+    const e = Math.abs(sig.edge);
+    const kelly = e / (1 + e); // simplified Kelly fraction
+    return kelly >= 0.10 && sig.confidence >= 0.40;
+  },
+
+  // Sharpe: Signal-to-noise ratio. edge / mc_stddev > 2.0. High SNR = clean signal, low noise.
+  sharpe_sniper: (sig) => {
+    const snr = Math.abs(sig.edge) / Math.max(sig.mc_stddev, 0.01);
+    return snr >= 2.0 && sig.mc_consistent;
+  },
+
+  // Bayesian: Combines confidence with article count as evidence weight.
+  // More articles = stronger prior. posterior = conf * (1 - 1/(1+articles)) * edge_factor.
+  bayesian_blend: (sig) => {
+    const evidenceWeight = 1 - 1 / (1 + (sig.num_articles || 0));
+    const posterior = sig.confidence * evidenceWeight * (1 + Math.abs(sig.edge));
+    return posterior >= 0.55;
+  },
+
+  // Ensemble: ALL factors must align — AI + MC + confidence + edge. Maximum consensus required.
+  ensemble_lock: (sig) => sig.ai_enhanced && sig.mc_consistent && sig.confidence >= 0.60 && Math.abs(sig.edge) >= 0.10 && (sig.num_articles || 0) >= 2,
+
+  // Mean Revert: High MC volatility (stddev > 10%) + moderate edge. Bets on reversion to mean.
+  mean_revert: (sig) => (sig.mc_stddev || 0) >= 0.10 && Math.abs(sig.edge) >= 0.08 && Math.abs(sig.edge) <= 0.25,
+
+  // Info Ratio: (articles * agreement) / max(mc_stddev, 0.01). Quality of information per unit noise.
+  info_ratio: (sig) => {
+    const ir = ((sig.num_articles || 0) * (sig.agreement || 0)) / Math.max(sig.mc_stddev, 0.01);
+    return ir >= 15 && Math.abs(sig.edge) >= 0.05;
+  },
+
+  // Anti-FOMO: Small edge (<12%) but HIGH confidence (>75%). Calm, certain, steady plays.
+  anti_fomo: (sig) => Math.abs(sig.edge) >= 0.03 && Math.abs(sig.edge) <= 0.12 && sig.confidence >= 0.75,
+
+  // Nightcrawler: BUY_NO only + AI enhanced + MC consistent. Sophisticated short-bias strategy.
+  nightcrawler: (sig) => sig.direction === 'BUY_NO' && sig.ai_enhanced && sig.mc_consistent,
+
+  // Heatseeker: Composite multi-factor score. Weighted blend of all signal dimensions.
+  // score = edge*40 + conf*30 + agreement*20 + min(articles/10,1)*10. Trade when score > 35.
+  heatseeker: (sig) => {
+    const score = Math.abs(sig.edge) * 40 + sig.confidence * 30 + (sig.agreement || 0) * 20 + Math.min((sig.num_articles || 0) / 10, 1) * 10;
+    return score >= 35;
+  },
+
+  // Quant: Geometric mean of edge and confidence, weighted by MC consistency. Pure math bot.
+  // geoMean = sqrt(edge * conf). Trade when geoMean > 0.15 and MC agrees.
+  quant_core: (sig) => {
+    const geo = Math.sqrt(Math.abs(sig.edge) * Math.max(sig.confidence, 0.001));
+    return geo >= 0.15 && (sig.mc_consistent || sig.confidence >= 0.80);
+  },
+
+  // ═══ ETF CATEGORY SPECIALISTS: Sector-Focused Portfolios ═══
+
+  etf_presidential: (sig) => /KXPRESPERSON|KXPRESPARTY/i.test(sig.market_id) ||
+    /president|2028 election|presidential race|white house|oval office|nominee/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_cabinet: (sig) => /KXCABOUT|KXCABLEAVE/i.test(sig.market_id) ||
+    /cabinet|secretary of|confirmation hearing|appointee|gabbard|hegseth|rubio|noem|rfk|bessent|lutnick/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_congress: (sig) => /KXCONG|KXHOUSE|SENATE|KXNEXTSPEAKER|KXCAPCONTROL|KXVETOOVERRIDE/i.test(sig.market_id) ||
+    /congress|senate|house of rep|speaker|legislation|bill pass|shutdown|filibuster|debt ceiling/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_scotus: (sig) => /KXSCOURT/i.test(sig.market_id) ||
+    /supreme court|scotus|justice|ruling|overturn|constitutional|judicial review/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_fed: (sig) => /KXFEDCHAIRNOM|KXBALANCE/i.test(sig.market_id) ||
+    /federal reserve|fed chair|interest rate|fomc|rate cut|rate hike|powell|warsh|monetary policy|quantitative/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_economy: (sig) => /KXGDP/i.test(sig.market_id) ||
+    /\bgdp\b|recession|economic growth|jobs report|unemployment|nonfarm payroll|consumer spending|retail sales|pce\b|cpi\b/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_geopolitics: (sig) => /KXZELENSKYPUTIN|KXNEXTUKPM|KXNEXTIRANLEADER|KXEUEXIT|KXTAIWANLVL|KXFTA/i.test(sig.market_id) ||
+    /geopolit|war\b|invasion|sanction|nato|treaty|ceasefire|diplomat|putin|zelensky|china|taiwan|iran|tariff|trade war/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_territory: (sig) => /KXGREENLAND|KXGREENTERRITORY|KXGREENLANDPRICE|KXGTAPRICE|KXSTATE51|KXUSAEXPANDTERRITORY/i.test(sig.market_id) ||
+    /greenland|statehood|territory|annex|state 51|panama canal|territorial/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_climate: (sig) => /KXWARMING|KXTEMP|KXRAIN|KXSNOW|KXHURR/i.test(sig.market_id) ||
+    /warming|climate|temperature|hurricane|tornado|flood|drought|polar vortex|heat wave|cold snap|freeze|precipitation|storm/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_energy: (sig) => /KXPRIMEENGCONSUMPTION|KXENERG/i.test(sig.market_id) ||
+    /\benergy\b|lng\b|natural gas|oil price|petroleum|electricity|power grid|blackout|eia\b|opec|crude|pipeline|refinery|utility|heating bill/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_tech: (sig) => /KXOAIANTH|KXMUSKTRILLION|KXFUSION/i.test(sig.market_id) ||
+    /artificial intelligence|\bai\b|openai|anthropic|chatgpt|gpt-5|machine learning|musk.*trillion|tesla|nvidia|semiconductor|quantum computing|fusion/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_space: (sig) => /KXMOONMAN|KXCOLONIZEMARS|KXSPACEX/i.test(sig.market_id) ||
+    /spacex|nasa|rocket|orbit|mars|moon|starship|falcon|launch|artemis|blue origin|astronaut|satellite/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_crypto: (sig) => /KXCRYPTO|KXBTC|KXETH/i.test(sig.market_id) ||
+    /bitcoin|btc|ethereum|eth|crypto|blockchain|defi|stablecoin|sec.*crypto|digital asset|web3|binance|coinbase/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_sports: (sig) => /KXNBA|KXNFL|KXNBASEATTLE|KXCANADACUP/i.test(sig.market_id) ||
+    /\bnba\b|\bnfl\b|\bmlb\b|\bnhl\b|sports|playoff|championship|super bowl|world series|draft|mvp|coach/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_entertainment: (sig) => /KXSWIFTKELCE|KXTAYLORSWIFT|KXACTORSONNYCROCKETT|KXLIVENATION/i.test(sig.market_id) ||
+    /taylor swift|celebrity|movie|oscar|grammy|concert|entertainment|viral|streaming|netflix|album|tour/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_legal: (sig) => /KXTRUMPPARDONS|KXINSURRECTION|KXIMPEACH|KXTRUMPREMOVE|KXTRUMPRESIGN/i.test(sig.market_id) ||
+    /pardon|impeach|indictment|trial|guilty|conviction|sentencing|insurrection|special counsel|subpoena|contempt/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_financials: (sig) => /KXBOND/i.test(sig.market_id) ||
+    /bond yield|treasury|10-year|yield curve|s&p|nasdaq|dow jones|stock market|bear market|bull market|correction|ipo|banking/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_govreform: (sig) => /KXAGENCYELIM|KXGOVTCUTS|KXWITHDRAW/i.test(sig.market_id) ||
+    /\bdoge\b|government efficiency|agency.*elim|department.*cut|federal workforce|deregulat|budget cut|spending cut|executive order/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_global_social: (sig) => /KXNEWPOPE|KXALBERTAREFYES/i.test(sig.market_id) ||
+    /pope|vatican|catholic|referendum|independence|separatist|social movement|protest|migration|refugee/i.test(sig.reasoning + ' ' + sig.market_title),
+
+  etf_broad_market: (sig) => Math.abs(sig.edge) >= 0.08 && sig.confidence >= 0.50 && (sig.mc_consistent || sig.ai_enhanced),
+
+  // ═══ SECTOR-BASED PORTFOLIOS (IDs 51-100): Granular sub-sector filters ═══
+
+  // — Energy & Commodities (51-55) —
+  lng_exports: (sig) => /lng|liquefied natural gas|natural gas export|gas terminal|lng shipment|henry hub/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  oil_price_band: (sig) => /\boil\b|crude|brent|wti|petroleum|barrel|oil price|oil production/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  natgas_seasonal: (sig) => /natural gas|natgas|gas price|gas storage|heating season|gas futures/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  opec_decisions: (sig) => /opec|oil cartel|production cut|oil output|saudi.*oil|oil supply/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  power_grid: (sig) => /power grid|blackout|electricity|energy grid|brownout|utility|power outage|grid fail/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — AI & Tech Stocks (56-60) —
+  semiconductor: (sig) => /semiconductor|chip|nvidia|tsmc|intel|amd|chip act|wafer|fab\b|gpu shortage/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  ai_regulation: (sig) => /ai regulation|ai safety|ai executive order|ai bill|regulate.*ai|ai governance|ai act/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  tech_earnings: (sig) => /tech.*earning|earning.*tech|apple.*revenue|google.*revenue|meta.*revenue|amazon.*revenue|magnificent seven|big tech.*report/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  autonomous_vehicles: (sig) => /autonomous|self.driving|robotaxi|waymo|cruise|lidar|adas|driverless/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  cloud_market: (sig) => /cloud computing|aws|azure|google cloud|cloud revenue|saas|cloud market|cloud spend/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — Crypto & Digital (61-65) —
+  btc_price_bands: (sig) => /bitcoin|btc|bitcoin price|btc.*\$|bitcoin.*\$|bitcoin.*above|bitcoin.*below/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  eth_upgrades: (sig) => /ethereum|eth\b|eth.*upgrade|ethereum.*upgrade|ethereum.*merge|eth.*fork|ether.*price/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  crypto_regulation: (sig) => /crypto.*regulat|sec.*crypto|crypto.*ban|crypto.*bill|digital asset.*law|crypto.*legal|stablecoin.*act/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  stablecoin_policy: (sig) => /stablecoin|usdt|usdc|tether|circle|stablecoin.*regulat|stablecoin.*bill|cbdc/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  defi_events: (sig) => /defi|decentralized finance|dex|uniswap|aave|compound|liquidity pool|yield farm|tvl/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — Macro Economics (66-70) —
+  cpi_monthly: (sig) => /\bcpi\b|consumer price|inflation.*rate|inflation.*report|core inflation|price index/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  nfp_monthly: (sig) => /nonfarm|non.farm|payroll|jobs report|jobs.*added|employment.*report|unemployment.*rate|jobless.*claim/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  housing_data: (sig) => /housing|home.*sale|home.*price|mortgage|existing home|housing start|case.shiller|real estate.*data/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  gdp_quarterly: (sig) => /\bgdp\b|gross domestic|economic growth|gdp.*quarter|gdp.*annual|recession.*gdp/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  yield_spread: (sig) => /yield curve|yield spread|inverted yield|2.year.*10.year|treasury spread|curve inversion/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — Finance & Markets (71-75) —
+  sp500_levels: (sig) => /s&p|s&p 500|sp500|spx|s&p.*above|s&p.*below|stock market.*level/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  fed_rate_path: (sig) => /fed.*rate|interest rate|fomc|rate cut|rate hike|federal funds|fed.*meeting|monetary policy|powell.*rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  bank_stress: (sig) => /bank.*stress|bank.*fail|banking crisis|bank run|fdic|bank.*collapse|regional bank|svb|silicon valley bank/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  bond_yields: (sig) => /bond.*yield|treasury.*yield|10.year.*yield|bond.*market|treasury.*rate|bond.*price/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  volatility_index: (sig) => /\bvix\b|volatility index|volatility.*spike|cboe|fear.*index|market.*volatility/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — Politics & Policy (76-80) —
+  state_elections: (sig) => /KXSEN|KXGOV|state election|governor.*race|senate.*race|state house|state legislature|primary.*election/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  congress_votes: (sig) => /KXCONG|KXHOUSE|congress.*vote|house.*vote|senate.*vote|bill.*pass|legislation.*vote|floor vote/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  tariff_policy: (sig) => /tariff|trade war|import duty|trade deficit|trade policy|customs duty|trade agreement|wto/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  immigration_policy: (sig) => /immigration|border|migrant|asylum|deportation|visa|daca|immigration.*reform|border.*wall/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  tax_legislation: (sig) => /tax.*bill|tax.*reform|tax.*cut|tax.*hike|tax.*code|irs|estate tax|capital gains.*tax|corporate tax/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — Sports (81-85) —
+  nba_outcomes: (sig) => /KXNBA|\bnba\b|basketball|nba.*final|nba.*champion|nba.*mvp|nba.*draft/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  nfl_outcomes: (sig) => /KXNFL|\bnfl\b|football|super bowl|nfl.*draft|nfl.*champion|quarterback|touchdown/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  soccer_global: (sig) => /soccer|football.*world|world cup|premier league|champions league|mls|fifa|la liga|bundesliga/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  mlb_outcomes: (sig) => /KXMLB|\bmlb\b|baseball|world series|mlb.*draft|home run|batting|pitcher/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  combat_sports: (sig) => /\bufc\b|\bmma\b|boxing|fight.*night|ufc.*champion|knockout|title fight|heavyweight/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — Health & Science (86-90) —
+  fda_approvals: (sig) => /fda.*approv|fda.*reject|drug.*approv|new drug|biologic.*approv|fda.*review|pdufa/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  disease_tracking: (sig) => /outbreak|pandemic|epidemic|covid|h5n1|bird flu|mpox|disease.*spread|cdc.*alert|who.*emergency/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  biotech_catalysts: (sig) => /biotech|clinical trial|phase 3|gene therapy|mrna|crispr|drug.*trial|biotech.*stock/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  health_metrics: (sig) => /life expectancy|obesity|opioid|drug overdose|health.*metric|public health|mortality rate|vaccination rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  space_milestones: (sig) => /spacex|nasa|rocket.*launch|mars|moon.*landing|starship|artemis|blue origin|orbit|satellite.*launch/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — Media & Culture (91-95) —
+  box_office: (sig) => /box office|movie.*gross|film.*revenue|opening weekend|blockbuster|cinema.*sales/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  awards_shows: (sig) => /oscar|grammy|emmy|golden globe|academy award|best picture|best actor|awards.*ceremony/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  streaming_subs: (sig) => /netflix.*subscriber|disney.*subscriber|streaming.*war|subscriber.*count|streaming.*growth|hulu|paramount/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  social_trends: (sig) => /viral|tiktok|social media.*trend|influencer|twitter.*trend|internet.*culture|meme/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  reality_outcomes: (sig) => /reality tv|bachelor|survivor|big brother|reality.*show|dancing.*stars|masked singer/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+
+  // — Global & Geopolitics (96-100) —
+  global_conflicts: (sig) => /war\b|invasion|ceasefire|military.*conflict|armed conflict|escalation|airstrike|troops.*deploy/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  sanctions_policy: (sig) => /sanction|embargo|trade restriction|asset freeze|sanctions.*russia|sanctions.*china|sanctions.*iran/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  climate_extremes: (sig) => /climate.*extreme|extreme weather|heat.*record|cold.*record|flood|wildfire|hurricane.*category|tornado|drought/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  forex_events: (sig) => /forex|currency.*crisis|dollar.*index|yen|euro.*dollar|fx.*rate|currency.*devaluation|exchange rate/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+  geopolitical_shock: (sig) => /black swan|geopolitical.*shock|coup|assassination|regime.*change|emergency.*declaration|martial law|unexpected.*event/i.test((sig.market_id || '') + ' ' + (sig.market_title || '') + ' ' + (sig.reasoning || '')),
+};
+
+// Position sizing per strategy — differentiated risk profiles
+const PORTFOLIO_SIZING = {
+  aggressive: (edge, conf) => Math.max(2, Math.min(5, Math.round(Math.abs(edge) * 30))),     // 2-5 contracts, edge-weighted
+  conservative: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),               // 1-3 contracts, confidence-weighted
+  weather: (edge, conf) => 2,                                                                  // Fixed 2 contracts
+  momentum: (edge, conf) => Math.max(1, Math.min(4, Math.round((conf + Math.abs(edge)) * 5))),// 1-4, blended
+  contrarian: (edge, conf) => 1,                                                               // Always 1 contract (high risk)
+  ai_enhanced: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),                // 1-3, conservative
+  mc_validated: (edge, conf) => Math.max(2, Math.min(4, Math.round(conf * 5))),               // 2-4, confidence-driven
+  scalper: (edge, conf) => 1,                                                                  // Always 1 (high volume, small bets)
+  patient: (edge, conf) => Math.max(3, Math.min(5, Math.round(conf * 6))),                    // 3-5, big when confident
+  random: (edge, conf) => Math.max(1, Math.min(3, Math.ceil(Math.random() * 3))),             // 1-3 random
+
+  // ═══ RISKY SIZING: Micro → Macro ═══
+  penny_pincher: () => 1,                                                                       // Always 1 — micro
+  spray_pray: () => Math.ceil(Math.random() * 2),                                               // 1-2 random
+  nickel_slots: () => 1,                                                                         // Fixed 1
+  degen_lite: (edge) => Math.max(1, Math.min(3, Math.round(Math.abs(edge) * 20))),             // 1-3, edge-scaled
+  politics_junkie: (edge, conf) => Math.max(2, Math.min(3, Math.round(conf * 4))),             // 2-3, confidence
+  double_down: (edge) => Math.max(4, Math.min(8, Math.round(Math.abs(edge) * 30))),            // 4-8, big edge bets
+  volatility_rider: (edge) => Math.max(2, Math.min(4, Math.round(Math.abs(edge) * 25))),       // 2-4, edge-weighted
+  whale: (edge, conf) => Math.max(5, Math.min(10, Math.round(conf * 12))),                     // 5-10, confidence-heavy
+  yolo: (edge) => Math.max(8, Math.min(15, Math.round(Math.abs(edge) * 40))),                  // 8-15, MAX edge scale
+  full_send: (edge, conf) => Math.max(1, Math.min(25, Math.round(Math.abs(edge) * conf * 50))),// 1-25, edge*conf scaled
+
+  // ═══ CPU-POWERED SIZING: Computational approaches ═══
+  kelly_criterion: (edge, conf) => Math.max(1, Math.min(8, Math.round((edge / (1 + edge)) * 20))),       // Kelly fraction scaled to 1-8
+  sharpe_sniper: (edge, conf) => Math.max(2, Math.min(6, Math.round(conf * 8))),                         // 2-6, conf-weighted (high SNR = trust it)
+  bayesian_blend: (edge, conf) => Math.max(1, Math.min(5, Math.round(Math.sqrt(edge * conf) * 15))),     // 1-5, geometric scaling
+  ensemble_lock: (edge, conf) => Math.max(3, Math.min(7, Math.round((edge + conf) * 5))),                // 3-7, sum-scaled (all factors aligned)
+  mean_revert: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),                          // 1-4, moderate sizing for reversion plays
+  info_ratio: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),                            // 2-5, confidence-driven
+  anti_fomo: (edge, conf) => Math.max(2, Math.min(4, Math.round(conf * 5))),                             // 2-4, steady sizing for calm plays
+  nightcrawler: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),                          // 1-3, conservative shorts
+  heatseeker: (edge, conf) => Math.max(2, Math.min(6, Math.round((edge * 20 + conf * 3) / 2))),         // 2-6, composite score-driven
+  quant_core: (edge, conf) => Math.max(2, Math.min(8, Math.round(Math.sqrt(edge * conf) * 25))),         // 2-8, geometric mean heavy
+
+  // ═══ ETF CATEGORY SIZING ═══
+  etf_presidential: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
+  etf_cabinet: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  etf_congress: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  etf_scotus: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+  etf_fed: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 20 + conf * 2))),
+  etf_economy: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
+  etf_geopolitics: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  etf_territory: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+  etf_climate: () => 2,
+  etf_energy: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 18))),
+  etf_tech: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
+  etf_space: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+  etf_crypto: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 20))),
+  etf_sports: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  etf_entertainment: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+  etf_legal: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  etf_financials: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
+  etf_govreform: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+  etf_global_social: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
+  etf_broad_market: (edge, conf) => Math.max(2, Math.min(6, Math.round(edge * 15 + conf * 3))),
+
+  // ═══ SECTOR-BASED SIZING (IDs 51-100): Conservative niche bets ═══
+
+  // — Energy & Commodities (51-55) — Moderate sizing, energy can be volatile
+  lng_exports: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  oil_price_band: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
+  natgas_seasonal: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+  opec_decisions: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
+  power_grid: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+
+  // — AI & Tech Stocks (56-60) — Slightly larger, higher conviction sector
+  semiconductor: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
+  ai_regulation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  tech_earnings: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 18))),
+  autonomous_vehicles: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+  cloud_market: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+
+  // — Crypto & Digital (61-65) — Volatile, keep positions smaller
+  btc_price_bands: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 16))),
+  eth_upgrades: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+  crypto_regulation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  stablecoin_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+  defi_events: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 10))),
+
+  // — Macro Economics (66-70) — Data-driven, moderate confidence sizing
+  cpi_monthly: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
+  nfp_monthly: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
+  housing_data: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  gdp_quarterly: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 15))),
+  yield_spread: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+
+  // — Finance & Markets (71-75) — Established signals, moderate-to-large
+  sp500_levels: (edge, conf) => Math.max(2, Math.min(5, Math.round(edge * 20))),
+  fed_rate_path: (edge, conf) => Math.max(2, Math.min(5, Math.round(conf * 6))),
+  bank_stress: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
+  bond_yields: (edge, conf) => Math.max(1, Math.min(4, Math.round(conf * 5))),
+  volatility_index: (edge, conf) => Math.max(1, Math.min(4, Math.round(edge * 18))),
+
+  // — Politics & Policy (76-80) — Binary outcomes, confidence-weighted
+  state_elections: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  congress_votes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  tariff_policy: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
+  immigration_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+  tax_legislation: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+
+  // — Sports (81-85) — Event-driven, fixed small sizing
+  nba_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  nfl_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  soccer_global: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+  mlb_outcomes: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 3))),
+  combat_sports: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+
+  // — Health & Science (86-90) — High-impact but rare, small positions
+  fda_approvals: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  disease_tracking: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
+  biotech_catalysts: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 10))),
+  health_metrics: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
+  space_milestones: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+
+  // — Media & Culture (91-95) — Fun bets, keep small
+  box_office: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+  awards_shows: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+  streaming_subs: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
+  social_trends: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 8))),
+  reality_outcomes: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 2))),
+
+  // — Global & Geopolitics (96-100) — High uncertainty, conservative
+  global_conflicts: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 12))),
+  sanctions_policy: (edge, conf) => Math.max(1, Math.min(2, Math.round(conf * 3))),
+  climate_extremes: (edge, conf) => Math.max(1, Math.min(3, Math.round(edge * 10))),
+  forex_events: (edge, conf) => Math.max(1, Math.min(3, Math.round(conf * 4))),
+  geopolitical_shock: (edge, conf) => Math.max(1, Math.min(2, Math.round(edge * 8))),
+};
+
+let tradeCounter = 0; // For wildcard portfolio
+
+async function autoPortfolioTrade(signal) {
+  try {
+    const { rows: portfolios } = await kenQ('SELECT * FROM ken_portfolios');
+    const today = new Date().toISOString().split('T')[0];
+
+    for (const pf of portfolios) {
+      const strategy = PORTFOLIO_STRATEGIES[pf.strategy];
+      if (!strategy) continue;
+
+      // Check balance: bankrupt portfolios can't trade (balance = 0 → move to bottom)
+      if (pf.balance_cents <= 0) continue; // BANKRUPT — no more trades
+
+      // Check if strategy accepts this signal
+      const sigData = {
+        edge: signal.expectedEdge || signal.expected_edge || 0,
+        confidence: signal.confidence || 0,
+        direction: signal.direction,
+        reasoning: signal.reasoning || '',
+        signal_type: signal.signal_type || '',
+        market_id: signal.marketId || signal.market_id || '',
+        ai_enhanced: signal.aiEnhanced || signal.ai_enhanced || false,
+        mc_consistent: signal.mcSimulation?.consistent || signal.mc_consistent || false,
+        mc_stddev: signal.mcSimulation?.stdDev || 0.99,
+        volume24h: signal.volume24h || signal.volume_24h || 0,
+        num_articles: signal.num_articles || 0,
+        agreement: signal.agreement || 0,
+        market_title: signal.market_title || signal.marketTitle || '',
+      };
+
+      if (!strategy(sigData)) continue;
+
+      // Hyper-trade: max 10 OPEN positions per market per portfolio (no daily cap)
+      const { rows: dupes } = await kenQ(`
+        SELECT id FROM ken_portfolio_trades
+        WHERE portfolio_id = $1 AND market_id = $2 AND status = 'open'
+      `, [pf.id, signal.marketId || signal.market_id]);
+      if (dupes.length >= 10) continue;
+
+      // Per-strategy position sizing
+      const absEdge = Math.abs(parseFloat(sigData.edge));
+      const conf = parseFloat(sigData.confidence);
+      const sizeFn = PORTFOLIO_SIZING[pf.strategy] || ((e, c) => Math.max(1, Math.min(3, Math.round(e * c * 15))));
+      const contracts = sizeFn(absEdge, conf);
+      const entryPrice = sigData.direction === 'BUY_YES'
+        ? Math.round((0.5 - absEdge / 2) * 100) // rough market mid
+        : Math.round((0.5 + absEdge / 2) * 100);
+      const clampedPrice = Math.max(5, Math.min(95, entryPrice));
+      const cost = clampedPrice * contracts;
+
+      if (cost > pf.balance_cents) continue; // Not enough balance for this trade
+
+      await kenQ(`
+        INSERT INTO ken_portfolio_trades (portfolio_id, signal_id, market_id, market_title, direction, entry_price_cents, contracts, cost_cents, reasoning, confidence, ai_enhanced, mc_consistent)
+        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
+      `, [pf.id, signal.id || null, signal.marketId || signal.market_id, signal.market_title || signal.marketTitle || '',
+          sigData.direction, clampedPrice, contracts, cost, (sigData.reasoning || '').substring(0, 500),
+          conf, sigData.ai_enhanced, sigData.mc_consistent]);
+
+      // Deduct from balance + update stats
+      await kenQ(`UPDATE ken_portfolios SET balance_cents = balance_cents - $1, total_invested_cents = total_invested_cents + $1, last_trade_at = NOW() WHERE id = $2`, [cost, pf.id]);
+      pf.balance_cents -= cost; // Update in-memory too for subsequent iterations
+
+      console.log(`[Ken/Portfolio] ${pf.name} (${pf.strategy}): ${sigData.direction} ${(signal.marketId || signal.market_id || '').substring(0, 30)} @ ${clampedPrice}¢ x${contracts} = $${(cost/100).toFixed(2)}`);
+    }
+  } catch (e) {
+    console.error('[Ken/Portfolio] Auto-trade error:', e.message);
+  }
+}
+
+// Capture NAV snapshots for charting — runs every 30 minutes
+async function capturePortfolioSnapshots() {
+  try {
+    const { rows: portfolios } = await kenQ('SELECT * FROM ken_portfolios');
+    for (const pf of portfolios) {
+      const { rows: openTrades } = await kenQ(`
+        SELECT entry_price_cents, contracts, direction, market_id
+        FROM ken_portfolio_trades WHERE portfolio_id = $1 AND status = 'open'
+      `, [pf.id]);
+
+      let openValue = 0;
+      for (const t of openTrades) {
+        const { rows: snaps } = await kenQ(`
+          SELECT last_price FROM ken_market_snapshots WHERE ticker = $1 ORDER BY captured_at DESC LIMIT 1
+        `, [t.market_id]);
+        if (snaps.length > 0) {
+          const curr = parseInt(snaps[0].last_price);
+          openValue += t.direction === 'BUY_YES'
+            ? (curr - t.entry_price_cents) * t.contracts
+            : (t.entry_price_cents - curr) * t.contracts;
+        }
+      }
+
+      const nav = (parseInt(pf.total_returned_cents) || 0) - (parseInt(pf.total_invested_cents) || 0) + openValue;
+      await kenQ(`
+        INSERT INTO ken_portfolio_snapshots (portfolio_id, nav_cents, open_positions) VALUES ($1, $2, $3)
+      `, [pf.id, nav, openTrades.length]);
+    }
+    console.log(`[Ken/Charts] NAV snapshots captured for ${portfolios.length} portfolios`);
+  } catch (e) {
+    console.error('[Ken/Charts] Snapshot error:', e.message);
+  }
+}
+
+// Resolve portfolio trades — HYPER MODE: quick take-profit/stop-loss + aging out
+async function resolvePortfolioTrades() {
+  try {
+    // Check ALL open trades (no minimum age for hyper-trading)
+    const { rows: openTrades } = await kenQ(`
+      SELECT pt.*, p.name as portfolio_name, p.strategy as portfolio_strategy
+      FROM ken_portfolio_trades pt
+      JOIN ken_portfolios p ON p.id = pt.portfolio_id
+      WHERE pt.status = 'open'
+      ORDER BY pt.created_at ASC
+      LIMIT 500
+    `);
+
+    for (const trade of openTrades) {
+      // Get latest price from snapshots
+      const { rows: snaps } = await kenQ(`
+        SELECT last_price FROM ken_market_snapshots
+        WHERE ticker = $1 ORDER BY captured_at DESC LIMIT 1
+      `, [trade.market_id]);
+
+      if (snaps.length === 0) continue;
+      const currentPrice = parseInt(snaps[0].last_price);
+      const ageMinutes = (Date.now() - new Date(trade.created_at).getTime()) / 60000;
+
+      let pnl = 0;
+      if (trade.direction === 'BUY_YES') {
+        pnl = (currentPrice - trade.entry_price_cents) * trade.contracts;
+      } else {
+        pnl = (trade.entry_price_cents - currentPrice) * trade.contracts;
+      }
+
+      // Hyper-trade exit rules:
+      // 1. Take profit: +3¢ per contract (minimum 5 min hold)
+      // 2. Stop loss: -5¢ per contract (minimum 5 min hold)
+      // 3. Age out: close after 2 hours regardless
+      let status = null;
+      const profitPerContract = pnl / Math.max(1, trade.contracts);
+
+      if (ageMinutes >= 5 && profitPerContract >= 3) {
+        status = 'won'; // Take profit
+      } else if (ageMinutes >= 5 && profitPerContract <= -5) {
+        status = 'lost'; // Stop loss
+      } else if (ageMinutes >= 120) {
+        // Age out after 2 hours — mark based on current P&L
+        status = pnl > 0 ? 'won' : pnl < 0 ? 'lost' : 'expired';
+      }
+
+      if (!status) continue; // Hold position
+
+      await kenQ(`
+        UPDATE ken_portfolio_trades SET status = $1, exit_price_cents = $2, pnl_cents = $3, resolved_at = NOW()
+        WHERE id = $4
+      `, [status, currentPrice, pnl, trade.id]);
+
+      // Update portfolio running totals
+      const streakVal = status === 'won' ? 1 : status === 'lost' ? -1 : 0;
+      // total_returned = cost_cents + pnl for winning trades (you get your money back + profit)
+      const returned = pnl > 0 ? trade.cost_cents + pnl : (pnl === 0 ? trade.cost_cents : 0);
+      const wonInc = status === 'won' ? 1 : 0;
+      const lostInc = status === 'lost' ? 1 : 0;
+      const expInc = status === 'expired' ? 1 : 0;
+      await kenQ(`
+        UPDATE ken_portfolios SET
+          balance_cents = balance_cents + $1,
+          total_returned_cents = total_returned_cents + $1,
+          trades_won = trades_won + $2,
+          trades_lost = trades_lost + $3,
+          trades_expired = trades_expired + $4,
+          streak = CASE WHEN $5 > 0 AND streak > 0 THEN streak + 1 WHEN $5 < 0 AND streak < 0 THEN streak - 1 ELSE $5 END,
+          best_trade_cents = GREATEST(best_trade_cents, $6),
+          worst_trade_cents = LEAST(worst_trade_cents, $6)
+        WHERE id = $7
+      `, [returned, wonInc, lostInc, expInc, streakVal, pnl, trade.portfolio_id]);
+
+      // Update daily P&L
+      const tradeDate = new Date(trade.created_at).toISOString().split('T')[0];
+      await kenQ(`
+        INSERT INTO ken_daily_pnl (portfolio_id, date, closed_trades, wins, losses, daily_pnl_cents, cumulative_pnl_cents)
+        VALUES ($1, $2, 1, $3, $4, $5, $5)
+        ON CONFLICT (portfolio_id, date) DO UPDATE SET
+          closed_trades = ken_daily_pnl.closed_trades + 1,
+          wins = ken_daily_pnl.wins + $3,
+          losses = ken_daily_pnl.losses + $4,
+          daily_pnl_cents = ken_daily_pnl.daily_pnl_cents + $5
+      `, [trade.portfolio_id, tradeDate, status === 'won' ? 1 : 0, status === 'lost' ? 1 : 0, pnl]);
+
+      // Update learning table
+      const category = extractSignalCategory(trade.reasoning || '');
+      await kenQ(`
+        INSERT INTO ken_learning (category, signal_type, win_rate, avg_pnl_cents, total_trades, last_updated)
+        VALUES ($1, $2, $3, $4, 1, NOW())
+        ON CONFLICT DO NOTHING
+      `, [category, trade.direction, status === 'won' ? 1 : 0, pnl]);
+    }
+
+    if (openTrades.length > 0) console.log(`[Ken/Portfolio] Resolved ${openTrades.length} trades across portfolios`);
+  } catch (e) {
+    console.error('[Ken/Portfolio] Resolution error:', e.message);
+  }
+}
+
+function extractSignalCategory(reasoning) {
+  const r = reasoning.toLowerCase();
+  if (/heating|gas bill|utility bill|electric bill|energy cost|propane|furnace|hvac/.test(r)) return 'heating_energy';
+  if (/weather|temp|rain|snow|wind|hurricane|polar vortex|cold snap|freeze|frost/.test(r)) return 'weather';
+  if (/crypto|bitcoin|btc|eth/.test(r)) return 'crypto';
+  if (/politic|elect|congress|senate|trump|biden/.test(r)) return 'politics';
+  if (/econom|rate|inflation|fed|gdp|job/.test(r)) return 'economics';
+  if (/momentum|volume|spike|spread/.test(r)) return 'momentum';
+  if (/sport|game|nfl|nba|mlb/.test(r)) return 'sports';
+  if (/lng|natural gas|oil|commodity|corn|wheat|soy|cattle|crop/.test(r)) return 'commodities';
+  return 'general';
+}
+
+// ══════════════════════════════════════════════
+// LNG & AGRICULTURAL COMMODITY INTELLIGENCE
+// ══════════════════════════════════════════════
+const COMMODITY_FEEDS = [
+  // ── LNG & Natural Gas (Government) ──
+  { url: 'https://www.eia.gov/rss/naturalgas_rss.xml', source: 'EIA Natural Gas', category: 'lng' },
+  { url: 'https://www.eia.gov/rss/petroleum_rss.xml', source: 'EIA Petroleum', category: 'oil' },
+  { url: 'https://www.eia.gov/rss/todayinenergy_rss.xml', source: 'EIA Today in Energy', category: 'energy' },
+  { url: 'https://www.eia.gov/rss/electricity_rss.xml', source: 'EIA Electricity', category: 'electricity' },
+  // ── Agriculture (Government) ──
+  { url: 'https://www.usda.gov/rss/home.xml', source: 'USDA', category: 'agriculture' },
+  { url: 'https://www.nass.usda.gov/rss/index.php', source: 'USDA NASS', category: 'agriculture' },
+  // ── NOAA Weather (Government) ──
+  { url: 'https://alerts.weather.gov/cap/us.php?x=0', source: 'NWS National Alerts', category: 'weather' },
+  { url: 'https://www.weather.gov/rss_page.php?site_name=nws', source: 'NWS News', category: 'weather' },
+  { url: 'https://www.cpc.ncep.noaa.gov/products/stratosphere/strat-trop/rss.xml', source: 'NOAA CPC', category: 'weather' },
+  // ── FERC & Energy Regulation (Government) ──
+  { url: 'https://www.ferc.gov/rss/news-releases.xml', source: 'FERC News', category: 'energy_regulation' },
+  // ── Weather Risk ──
+  { url: 'https://rfrx.rfrz.io/weatherrisk', source: 'Weather Risk', category: 'weather_commodity' },
+];
+
+// Government Open APIs for Energy & Weather Data
+const GOV_API_ENDPOINTS = {
+  // EIA Open Data API (no key needed for RSS, key for JSON)
+  eia_ng_spot: 'https://api.eia.gov/v2/natural-gas/pri/fut/data/?frequency=daily&data[0]=value&sort[0][column]=period&sort[0][direction]=desc&length=5&api_key=',
+  eia_elec_price: 'https://api.eia.gov/v2/electricity/retail-sales/data/?frequency=monthly&data[0]=price&sort[0][column]=period&sort[0][direction]=desc&length=10&api_key=',
+  // NOAA Weather API (free, no key)
+  noaa_alerts: 'https://api.weather.gov/alerts/active?status=actual&message_type=alert',
+  noaa_forecast_nyc: 'https://api.weather.gov/gridpoints/OKX/33,37/forecast',
+  noaa_forecast_chi: 'https://api.weather.gov/gridpoints/LOT/75,72/forecast',
+  noaa_forecast_dal: 'https://api.weather.gov/gridpoints/FWD/87,108/forecast',
+  noaa_forecast_den: 'https://api.weather.gov/gridpoints/BOU/62,60/forecast',
+  noaa_forecast_msp: 'https://api.weather.gov/gridpoints/MPX/107,71/forecast',
+  // FRED Economic Data (key needed but some RSS free)
+  fred_natgas: 'https://fred.stlouisfed.org/graph/fredgraph.csv?id=DHHNGSP',
+};
+
+// Fetch NOAA weather alerts for extreme cold/heat events
+async function fetchNOAAWeatherIntel() {
+  let found = 0;
+  try {
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 10000);
+    const res = await fetch(GOV_API_ENDPOINTS.noaa_alerts, {
+      signal: controller.signal,
+      headers: { 'User-Agent': 'KenInc/1.0 Weather Intelligence', Accept: 'application/geo+json' },
+    });
+    clearTimeout(timeout);
+    if (!res.ok) return found;
+    const data = await res.json();
+
+    const heatingKeywords = /cold|freeze|frost|ice|blizzard|winter storm|wind chill|extreme cold|heating|arctic|polar vortex|below zero|record low|dangerous cold/i;
+    const energyKeywords = /power outage|grid|electricity|utility|energy emergency|rolling blackout|brownout|load shed/i;
+
+    for (const feature of (data.features || []).slice(0, 50)) {
+      const props = feature.properties || {};
+      const headline = props.headline || props.event || '';
+      const desc = props.description || '';
+      const combined = headline + ' ' + desc;
+
+      if (heatingKeywords.test(combined) || energyKeywords.test(combined)) {
+        const areas = (props.areaDesc || '').substring(0, 200);
+        const severity = props.severity || 'Unknown';
+        const certainty = props.certainty || 'Unknown';
+
+        await kenQ(`
+          INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+          VALUES ('noaa_alert', $1, 'weather_energy', $2, $3, $4, 0, $5, $6)
+          ON CONFLICT DO NOTHING
+        `, [
+          areas,
+          headline.substring(0, 500),
+          props.id || `noaa-${Date.now()}`,
+          severity === 'Extreme' ? 100 : severity === 'Severe' ? 80 : severity === 'Moderate' ? 50 : 20,
+          severity === 'Extreme' ? -0.9 : severity === 'Severe' ? -0.7 : -0.4,
+          `[${severity}/${certainty}] ${areas}: ${desc.substring(0, 400)}`
+        ]);
+        found++;
+      }
+    }
+  } catch (e) {
+    // NOAA API can be slow, don't log every failure
+  }
+
+  // Fetch regional forecasts for major metro areas (heating demand proxy)
+  const metros = [
+    { name: 'NYC', url: GOV_API_ENDPOINTS.noaa_forecast_nyc },
+    { name: 'Chicago', url: GOV_API_ENDPOINTS.noaa_forecast_chi },
+    { name: 'Dallas', url: GOV_API_ENDPOINTS.noaa_forecast_dal },
+    { name: 'Denver', url: GOV_API_ENDPOINTS.noaa_forecast_den },
+    { name: 'Minneapolis', url: GOV_API_ENDPOINTS.noaa_forecast_msp },
+  ];
+
+  for (const metro of metros) {
+    try {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 8000);
+      const res = await fetch(metro.url, {
+        signal: controller.signal,
+        headers: { 'User-Agent': 'KenInc/1.0', Accept: 'application/geo+json' },
+      });
+      clearTimeout(timeout);
+      if (!res.ok) continue;
+      const data = await res.json();
+
+      for (const period of (data.properties?.periods || []).slice(0, 4)) {
+        const tempF = period.temperature;
+        const wind = period.windSpeed || '';
+        const forecast = period.detailedForecast || period.shortForecast || '';
+
+        // Flag extreme cold (below 20F) or heat waves (above 100F) — heating demand signals
+        if (tempF <= 20 || tempF >= 100) {
+          await kenQ(`
+            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+            VALUES ('noaa_forecast', $1, 'extreme_temp', $2, $3, $4, 0, $5, $6)
+            ON CONFLICT DO NOTHING
+          `, [
+            metro.name,
+            `${metro.name}: ${tempF}°F ${period.name} — ${period.shortForecast}`,
+            `noaa-forecast-${metro.name}-${period.number}-${new Date().toISOString().split('T')[0]}`,
+            Math.abs(tempF <= 20 ? (20 - tempF) * 5 : (tempF - 100) * 5),
+            tempF <= 20 ? -0.8 : -0.6,
+            `${metro.name} ${period.name}: ${tempF}°F, Wind ${wind}. ${forecast.substring(0, 300)}`
+          ]);
+          found++;
+        }
+      }
+      await new Promise(r => setTimeout(r, 500));
+    } catch (e) { /* NOAA timeout */ }
+  }
+
+  if (found > 0) console.log(`[Ken/NOAA] ${found} weather/energy alerts from government APIs`);
+  return found;
+}
+
+// Additional weather-impacted commodity keywords for market scanning
+const COMMODITY_KEYWORDS = [
+  'natural gas', 'lng', 'henry hub', 'gas price', 'heating degree',
+  'corn', 'wheat', 'soybeans', 'soybean', 'cotton', 'cattle', 'hogs',
+  'crop', 'harvest', 'drought', 'frost', 'freeze', 'growing season',
+  'el nino', 'la nina', 'commodity', 'futures', 'agriculture',
+  'ethanol', 'fertilizer', 'grain', 'livestock', 'usda', 'crop report',
+];
+
+// ── HEATING BILL & ENERGY COST INTELLIGENCE ──
+const HEATING_KEYWORDS = [
+  'heating bill', 'gas bill', 'electric bill', 'utility bill', 'energy bill',
+  'heating cost', 'energy cost', 'propane', 'furnace', 'thermostat',
+  'polar vortex', 'cold snap', 'wind chill', 'below zero', 'record cold',
+  'heating oil', 'natural gas price', 'electricity price', 'rate hike',
+  'winter storm', 'power outage', 'grid strain', 'rolling blackout',
+  'hvac', 'insulation', 'heat pump', 'space heater', 'energy assistance',
+];
+
+const HEATING_SUBS = [
+  'personalfinance', 'Frugal', 'povertyfinance', 'homeowners',
+  'HVAC', 'energy', 'NaturalGas', 'HomeImprovement', 'utilities',
+  'weather', 'climate', 'TropicalWeather', 'preppers',
+];
+
+async function fetchHeatingBillIntel() {
+  let found = 0;
+  const heatingRegex = new RegExp(HEATING_KEYWORDS.join('|'), 'i');
+
+  for (let i = 0; i < HEATING_SUBS.length; i += 3) {
+    const batch = HEATING_SUBS.slice(i, i + 3);
+    const results = await Promise.allSettled(batch.map(sub => scrapeSubredditHot(sub, 15)));
+    for (let j = 0; j < results.length; j++) {
+      if (results[j].status !== 'fulfilled') continue;
+      const sub = batch[j];
+      for (const post of results[j].value) {
+        const text = (post.title + ' ' + (post.selftext || '')).toLowerCase();
+        if (heatingRegex.test(text) && post.score > 10) {
+          const sentiment = scoreSentiment(post.title);
+          await kenQ(`
+            INSERT INTO ken_sentiment (source, subreddit, keyword, post_title, post_url, score, num_comments, sentiment_score, raw_text)
+            VALUES ('heating_intel', $1, 'heating', $2, $3, $4, $5, $6, $7)
+            ON CONFLICT DO NOTHING
+          `, [sub, post.title, post.url, post.score, post.num_comments, sentiment, text.substring(0, 500)]);
+          found++;
+        }
+      }
+    }
+    await new Promise(r => setTimeout(r, 2000));
+  }
+
+  if (found > 0) console.log(`[Ken/Heating] Found ${found} heating bill/energy cost discussions`);
+  return found;
+}
+
+async function fetchCommodityIntel() {
+  let total = 0;
+  for (const feed of COMMODITY_FEEDS) {
+    try {
+      const controller = new AbortController();
+      const timeout = setTimeout(() => controller.abort(), 8000);
+      const res = await fetch(feed.url, {
+        signal: controller.signal,
+        headers: { 'User-Agent': 'KenInc/1.0 Commodity Intelligence' },
+      });
+      clearTimeout(timeout);
+      if (!res.ok) continue;
+      const text = await res.text();
+
+      // Extract items from RSS
+      const items = text.match(/<item>[\s\S]*?<\/item>/gi) || [];
+      for (const item of items.slice(0, 15)) {
+        const title = (item.match(/<title><!\[CDATA\[(.*?)\]\]>|<title>(.*?)<\/title>/)?.[1] || item.match(/<title>(.*?)<\/title>/)?.[1] || '').trim();
+        const link = (item.match(/<link>(.*?)<\/link>/)?.[1] || '').trim();
+        if (!title) continue;
+
+        // Score commodity relevance
+        const score = scoreEventSentiment(title);
+
+        try {
+          await kenQ(`
+            INSERT INTO ken_sentiment (source, keyword, post_title, post_url, sentiment_score, captured_at)
+            VALUES ($1, $2, $3, $4, $5, NOW())
+            ON CONFLICT DO NOTHING
+          `, [feed.source, feed.category, title.substring(0, 500), link.substring(0, 500), score.sentiment || 0]);
+          total++;
+        } catch(e) {} // skip dupes
+      }
+    } catch(e) {}
+  }
+  if (total > 0) console.log(`[Ken/Commodities] ${total} articles from ${COMMODITY_FEEDS.length} commodity feeds`);
+  return total;
+}
+
+// ══════════════════════════════════════════════
+// PERFORMANCE TRACKER — resolve old predictions
+// ══════════════════════════════════════════════
+async function resolveOldPredictions() {
+  try {
+    // Get pending predictions older than 6 hours
+    const { rows } = await kenQ(`
+      SELECT id, ticker, side, entry_price FROM ken_predictions
+      WHERE outcome = 'pending' AND created_at < NOW() - INTERVAL '6 hours'
+      LIMIT 50
+    `);
+    for (const pred of rows) {
+      // Get latest snapshot for this ticker
+      const { rows: snaps } = await kenQ(`
+        SELECT last_price FROM ken_market_snapshots
+        WHERE ticker = $1 ORDER BY captured_at DESC LIMIT 1
+      `, [pred.ticker]);
+      if (snaps.length > 0) {
+        const currentPrice = snaps[0].last_price;
+        const entryPrice = pred.entry_price || 50;
+        let outcome = 'expired';
+        let pnl = 0;
+        if (pred.side === 'YES') {
+          pnl = currentPrice - entryPrice;
+          outcome = pnl > 0 ? 'win' : pnl < 0 ? 'loss' : 'expired';
+        } else {
+          pnl = entryPrice - currentPrice; // NO side profits when price drops
+          outcome = pnl > 0 ? 'win' : pnl < 0 ? 'loss' : 'expired';
+        }
+        await kenQ(`
+          UPDATE ken_predictions SET outcome = $1, exit_price = $2, pnl_cents = $3, resolved_at = NOW()
+          WHERE id = $4
+        `, [outcome, currentPrice, pnl, pred.id]);
+      }
+    }
+    if (rows.length > 0) console.log(`[Ken] Resolved ${rows.length} old predictions`);
+  } catch (e) {
+    console.error('[Ken] Prediction resolution error:', e.message);
+  }
+}
+
+// ══════════════════════════════════════════════
+// CLEANUP — prune old data (daily)
+// ══════════════════════════════════════════════
+async function cleanupOldData() {
+  try {
+    await kenQ(`DELETE FROM ken_market_snapshots WHERE captured_at < NOW() - INTERVAL '7 days'`);
+    await kenQ(`DELETE FROM ken_sentiment WHERE captured_at < NOW() - INTERVAL '14 days'`);
+    await kenQ(`DELETE FROM ken_performance WHERE scan_time < NOW() - INTERVAL '30 days'`);
+    await kenQ(`DELETE FROM ken_portfolio_snapshots WHERE snapshot_at < NOW() - INTERVAL '60 days'`);
+    console.log('[Ken] Old data cleaned up (including portfolio snapshots 60d retention)');
+  } catch (e) {
+    console.error('[Ken] Cleanup error:', e.message);
+  }
+}
+
+// ── Start Server ──
+server.listen(PORT, '0.0.0.0', () => {
+  console.log(`[Ken] Server running on http://0.0.0.0:${PORT}`);
+  console.log(`[Ken] Kalshi environment: ${KALSHI_ENV.toUpperCase()} (${KALSHI_BASE_URL})`);
+  console.log(`[Ken] Static files: ${DIST}`);
+  console.log(`[Ken] Auth: session cookie + IP whitelist + basic auth`);
+  console.log(`[Ken] ═══════════════════════════════════════`);
+  console.log(`[Ken] AUTONOMOUS MODE: 24/7 MARKET INTELLIGENCE`);
+  console.log(`[Ken] Market scan: every 8 minutes (CPU-boosted, was 15m)`);
+  console.log(`[Ken] Signal pipeline: every 2 minutes (MC simulation + Gemini AI enhanced)`);
+  console.log(`[Ken] Reddit/news: every 15 minutes (doubled, was 30m)`);
+  console.log(`[Ken] CPU cores: ${os.cpus().length} | MC iterations: 15,000 per signal`);
+  console.log(`[Ken] Gemini AI: ${GEMINI_MODEL} (daily budget: ${GEMINI_DAILY_LIMIT} calls)`);
+  console.log(`[Ken] Slack alerts: every 4 hours (high-conf bypass)`);
+  console.log(`[Ken] Data sources: Kalshi | Reddit (80+) | ${NEWS_FEEDS.length} news feeds | HN | GDELT TV | YouTube | Manifold | r/Kalshi | r/Polymarket | Lemmy | Lobsters | Tildes | Bluesky | Mastodon | Metaculus | Polymarket | PredictIt | FRED Economic | NWS Alerts | NOAA Forecasts | EIA Energy | FERC | GovTrack | USDA | Twitter/Nitter | Discord`);
+  console.log(`[Ken] Heating intel: ${HEATING_SUBS.length} subreddits | ${HEATING_KEYWORDS.length} keywords | NOAA 5 metro forecasts | EIA electricity/gas prices`);
+  console.log(`[Ken] ═══════════════════════════════════════`);
+
+  // Separate mutexes: sweep (heavy Reddit/news) and scan (light Kalshi API) can run concurrently
+  let sweepRunning = false;
+  let scanRunning = false;
+
+  async function runSweepSafe(name, fn) {
+    if (sweepRunning) { console.log(`[Ken] Skipping ${name} — another sweep in progress`); return; }
+    sweepRunning = true;
+    try { await fn(); } catch (e) { console.error(`[Ken] ${name} error:`, e.message); }
+    sweepRunning = false;
+    if (global.gc) global.gc();
+  }
+
+  async function runScanSafe(fn) {
+    if (scanRunning) { console.log('[Ken] Skipping Scan — another scan in progress'); return; }
+    scanRunning = true;
+    try { await fn(); } catch (e) { console.error(`[Ken] Scan error:`, e.message); }
+    scanRunning = false;
+    if (global.gc) global.gc();
+  }
+
+  // Load stored Kalshi env preference, trade config, and scan config from DB
+  refreshKalshiEnv().then(() => {
+    console.log(`[Ken] Kalshi environment: ${KALSHI_ENV.toUpperCase()} (${KALSHI_BASE_URL})`);
+  });
+  loadTradeConfig();
+  loadScanConfig();
+
+  // Initial: run reddit sweep and market scan (scan uses separate mutex, can overlap)
+  setTimeout(async () => {
+    runSweepSafe('Reddit init', redditIntelligenceLoop);
+    setTimeout(() => runScanSafe(autonomousScan), 10000);
+  }, 5000);
+
+  // Start all recurring intervals via stored handles (restartIntervals can update later)
+  scanIntervalId = setInterval(() => runScanSafe(autonomousScan), SCAN_INTERVAL_MS);
+  redditIntervalId = setInterval(() => runSweepSafe('Reddit', redditIntelligenceLoop), REDDIT_INTERVAL_MS);
+  signalIntervalId = setInterval(async () => {
+    if (cachedActiveMarkets.length > 0) {
+      try { await signalPipeline(cachedActiveMarkets); } catch (e) { console.log(`[Ken] Fast pipeline error: ${e.message}`); }
+    }
+  }, SIGNAL_PIPELINE_MS);
+
+  // Resolve old predictions every hour
+  setInterval(() => {
+    resolveOldPredictions().catch(e => console.error('[Ken] Resolution error:', e.message));
+  }, 60 * 60 * 1000);
+
+  // HYPER-TRADE: Resolve portfolio trades (interval stored in handle for UI control)
+  resolveIntervalId = setInterval(() => {
+    resolvePortfolioTrades().catch(e => console.error('[Ken] Portfolio resolution error:', e.message));
+  }, RESOLVE_INTERVAL_MS);
+  setTimeout(() => resolvePortfolioTrades().catch(() => {}), 30000);
+
+  // Portfolio NAV snapshots every 30 minutes for charting
+  setInterval(() => capturePortfolioSnapshots().catch(e => console.error('[Ken/Charts] Error:', e.message)), 30 * 60 * 1000);
+  setTimeout(() => capturePortfolioSnapshots().catch(() => {}), 25000); // Initial after startup
+
+  // Commodity + heating bill + NOAA intelligence every 20 minutes
+  setInterval(() => {
+    fetchCommodityIntel().catch(e => console.error('[Ken] Commodity error:', e.message));
+    fetchHeatingBillIntel().catch(e => console.error('[Ken] Heating intel error:', e.message));
+    fetchNOAAWeatherIntel().catch(e => console.error('[Ken] NOAA intel error:', e.message));
+  }, 20 * 60 * 1000);
+  // Initial commodity + heating + NOAA fetch
+  setTimeout(() => {
+    fetchCommodityIntel().catch(() => {});
+    fetchHeatingBillIntel().catch(() => {});
+    fetchNOAAWeatherIntel().catch(() => {});
+  }, 15000);
+
+  // Weekly portfolio auto-refill: $100/portfolio every Monday (4-week trial)
+  // Checks every hour, only refills on Mondays between 0:00-0:59 UTC
+  const WEEKLY_REFILL_CENTS = 10000; // $100
+  const REFILL_WEEKS_REMAINING_KEY = 'ken_refill_weeks';
+  setInterval(async () => {
+    try {
+      const now = new Date();
+      if (now.getUTCDay() !== 1 || now.getUTCHours() !== 0) return; // Monday 0:00 UTC only
+      // Check if we already refilled this week
+      const { rows: [config] } = await kenQ(
+        `SELECT value FROM ken_config WHERE key = 'last_refill_date'`
+      ).catch(() => ({ rows: [] }));
+      const today = now.toISOString().split('T')[0];
+      if (config && config.value === today) return; // Already refilled today
+      // Check weeks remaining
+      const { rows: [weeksRow] } = await kenQ(
+        `SELECT value FROM ken_config WHERE key = $1`, [REFILL_WEEKS_REMAINING_KEY]
+      ).catch(() => ({ rows: [] }));
+      const weeksLeft = weeksRow ? parseInt(weeksRow.value) : 4;
+      if (weeksLeft <= 0) { console.log('[Ken] Weekly refill: 0 weeks remaining, skipping'); return; }
+      // Refill all portfolios
+      const { rowCount } = await kenQ(
+        `UPDATE ken_portfolios SET balance_cents = balance_cents + $1`, [WEEKLY_REFILL_CENTS]
+      );
+      // Update tracking (value column is jsonb, so wrap in JSON.stringify)
+      await kenQ(`INSERT INTO ken_config (key, value) VALUES ('last_refill_date', $1::jsonb)
+        ON CONFLICT (key) DO UPDATE SET value = $1::jsonb, updated_at = NOW()`, [JSON.stringify(today)]);
+      await kenQ(`INSERT INTO ken_config (key, value) VALUES ($1, $2::jsonb)
+        ON CONFLICT (key) DO UPDATE SET value = $2::jsonb, updated_at = NOW()`, [REFILL_WEEKS_REMAINING_KEY, JSON.stringify(String(weeksLeft - 1))]);
+      console.log(`[Ken] Weekly refill: +$${WEEKLY_REFILL_CENTS/100} to ${rowCount} portfolios. ${weeksLeft - 1} weeks remaining.`);
+    } catch (e) { console.error('[Ken] Weekly refill error:', e.message); }
+  }, 60 * 60 * 1000); // Check every hour
+
+  // Cleanup old data every 24 hours
+  setInterval(() => {
+    cleanupOldData().catch(e => console.error('[Ken] Cleanup error:', e.message));
+  }, 24 * 60 * 60 * 1000);
+});
diff --git a/kalshi-dash/src/Layout.jsx b/kalshi-dash/src/Layout.jsx
new file mode 100644
index 0000000..81c76f4
--- /dev/null
+++ b/kalshi-dash/src/Layout.jsx
@@ -0,0 +1,355 @@
+import React, { useState, useEffect } from 'react';
+import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom';
+import { api, checkSession, logout } from './api';
+import TickerTape from './components/TickerTape';
+
+const NAV = [
+  { to: '/', label: 'Dashboard', d: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
+  { to: '/markets', label: 'Markets', d: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
+  { to: '/signals', label: 'Signals', d: 'M13 10V3L4 14h7v7l9-11h-7z' },
+  { to: '/intelligence', label: 'Intel Feed', d: 'M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z' },
+  { to: '/trading', label: 'Trading', d: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
+  { to: '/positions', label: 'Positions', d: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
+  { to: '/orders', label: 'Orders', d: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4' },
+  { to: '/heatmap', label: 'Heatmap', d: 'M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z' },
+  { to: '/orbit', label: 'Orbit', d: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9' },
+  { to: '/pulse', label: 'Pulse', d: 'M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
+  { to: '/weather', label: 'Weather', d: 'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z' },
+  { to: '/risk', label: 'Portfauxlios', d: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4' },
+  { to: '/settings', label: 'Settings', d: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' },
+];
+
+function StatusDot({ active, warn }) {
+  const color = warn
+    ? 'var(--orange)'
+    : active
+    ? 'var(--green)'
+    : 'var(--red)';
+  return (
+    <span
+      style={{
+        display: 'inline-block',
+        width: 6,
+        height: 6,
+        borderRadius: '50%',
+        background: color,
+        flexShrink: 0,
+        boxShadow: `0 0 4px ${color}`,
+      }}
+    />
+  );
+}
+
+export default function Layout() {
+  const loc = useLocation();
+  const nav = useNavigate();
+  const [h, setH] = useState(null);
+  const [col, setCol] = useState(false);
+  const [authChecked, setAuthChecked] = useState(false);
+
+  useEffect(() => {
+    checkSession().then(ok => {
+      if (!ok) { nav('/login'); return; }
+      setAuthChecked(true);
+      const load = () => api('/health').then(setH).catch(() => {});
+      load();
+      const i = setInterval(load, 15000);
+      return () => clearInterval(i);
+    });
+  }, []);
+
+  if (!authChecked) {
+    return (
+      <div className="flex items-center justify-center min-h-screen" style={{ background: 'var(--bg)' }}>
+        <div
+          className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
+          style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+        />
+      </div>
+    );
+  }
+
+  const sidebarW = col ? 64 : 220;
+
+  return (
+    <div className="flex min-h-screen" style={{ background: 'var(--bg)' }}>
+      {/* ── Sidebar ── */}
+      {/* ── CRT Scan Line Overlay ── */}
+      <div className="scan-overlay" aria-hidden="true" />
+
+      <aside
+        className="sidebar-mesh"
+        style={{
+          width: sidebarW,
+          minWidth: sidebarW,
+          borderRight: '1px solid var(--border)',
+          display: 'flex',
+          flexDirection: 'column',
+          transition: 'width 200ms ease, min-width 200ms ease',
+          overflow: 'hidden',
+          position: 'sticky',
+          top: 0,
+          height: '100vh',
+        }}
+      >
+        {/* Brand */}
+        <div
+          style={{
+            padding: col ? '16px 12px' : '16px 18px',
+            borderBottom: '1px solid var(--border)',
+            display: 'flex',
+            alignItems: 'center',
+            gap: 12,
+            minHeight: 64,
+            flexShrink: 0,
+          }}
+        >
+          {/* Orange gradient K badge */}
+          <div
+            className="logo-animated"
+            style={{
+              width: 34,
+              height: 34,
+              borderRadius: 8,
+              background: 'linear-gradient(135deg, var(--orange), #E5690D)',
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              flexShrink: 0,
+              boxShadow: '0 2px 10px rgba(247,147,26,0.3)',
+            }}
+          >
+            <span style={{ color: '#0E0E10', fontWeight: 900, fontSize: 16, fontFamily: 'var(--font-heading)' }}>K</span>
+          </div>
+
+          {!col && (
+            <span
+              className="kalshi-gradient heading"
+              style={{ fontSize: 13, fontWeight: 700, letterSpacing: '0.06em', whiteSpace: 'nowrap', filter: 'drop-shadow(0 0 8px rgba(0,240,255,0.3))' }}
+            >
+              KEN TRADER
+            </span>
+          )}
+        </div>
+
+        {/* Nav items */}
+        <nav style={{ flex: 1, padding: '10px 8px', overflowY: 'auto', overflowX: 'hidden' }}>
+          {NAV.map((n, idx) => {
+            const active = loc.pathname === n.to || (n.to !== '/' && loc.pathname.startsWith(n.to));
+            const isSettings = n.to === '/settings';
+            return (
+              <React.Fragment key={n.to}>
+                {isSettings && (
+                  <div style={{ margin: '8px 4px', height: 1, background: 'linear-gradient(90deg, transparent, var(--border), transparent)' }} />
+                )}
+              <Link
+                to={n.to}
+                title={col ? n.label : undefined}
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  gap: col ? 0 : 10,
+                  padding: col ? '9px 0' : '9px 10px',
+                  justifyContent: col ? 'center' : 'flex-start',
+                  borderRadius: 8,
+                  marginBottom: 2,
+                  textDecoration: 'none',
+                  fontSize: 13,
+                  fontWeight: active ? 600 : 400,
+                  color: active ? 'var(--cyan)' : 'var(--text-dim)',
+                  background: active ? 'rgba(0,240,255,0.08)' : 'transparent',
+                  borderLeft: active ? '3px solid var(--cyan)' : '3px solid transparent',
+                  boxShadow: active ? 'inset 0 0 16px rgba(0,240,255,0.06), inset 3px 0 8px rgba(0,240,255,0.1)' : undefined,
+                  textShadow: active ? '0 0 8px rgba(0,240,255,0.4)' : undefined,
+                  transition: 'all 150ms ease',
+                  whiteSpace: 'nowrap',
+                  overflow: 'hidden',
+                }}
+                onMouseEnter={e => {
+                  if (!active) {
+                    e.currentTarget.style.color = 'var(--text)';
+                    e.currentTarget.style.background = 'rgba(0,240,255,0.04)';
+                    e.currentTarget.style.borderLeftColor = 'rgba(0,240,255,0.3)';
+                  }
+                }}
+                onMouseLeave={e => {
+                  if (!active) {
+                    e.currentTarget.style.color = 'var(--text-dim)';
+                    e.currentTarget.style.background = 'transparent';
+                    e.currentTarget.style.borderLeftColor = 'transparent';
+                  }
+                }}
+              >
+                <svg
+                  style={{ width: 17, height: 17, flexShrink: 0, opacity: active ? 1 : 0.7 }}
+                  fill="none"
+                  stroke="currentColor"
+                  strokeWidth="1.6"
+                  viewBox="0 0 24 24"
+                >
+                  <path strokeLinecap="round" strokeLinejoin="round" d={n.d} />
+                </svg>
+                {!col && (
+                  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{n.label}</span>
+                )}
+              </Link>
+              </React.Fragment>
+            );
+          })}
+        </nav>
+
+        {/* Bottom status + controls */}
+        <div
+          style={{
+            borderTop: '1px solid var(--border)',
+            padding: col ? '12px 8px' : '12px 14px',
+            flexShrink: 0,
+          }}
+        >
+          {/* Status indicators — hidden when collapsed */}
+          {!col && h && (
+            <div style={{ marginBottom: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
+              {[
+                { label: 'Env', value: (h.env || 'demo').toUpperCase(), warn: h.env === 'demo', active: h.env === 'prod' },
+                { label: 'API', value: h.kalshi_connected ? 'CONNECTED' : 'OFF', active: h.kalshi_connected },
+                { label: 'DB', value: h.db_connected ? 'OK' : 'DOWN', active: h.db_connected },
+                { label: 'Safe Mode', value: h.safe_mode ? 'ON' : 'OFF', warn: h.safe_mode, active: !h.safe_mode },
+              ].map(s => (
+                <div key={s.label} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+                  <span style={{ fontSize: 10, color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }}>{s.label}</span>
+                  <div style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
+                    <StatusDot active={s.active} warn={s.warn} />
+                    <span
+                      style={{
+                        fontSize: 10,
+                        fontFamily: 'var(--font-mono)',
+                        fontWeight: 600,
+                        color: s.warn ? 'var(--orange)' : s.active ? 'var(--green)' : 'var(--red)',
+                      }}
+                    >
+                      {s.value}
+                    </span>
+                  </div>
+                </div>
+              ))}
+            </div>
+          )}
+
+          {/* Collapse + Logout */}
+          <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
+            <button
+              onClick={() => setCol(!col)}
+              title={col ? 'Expand sidebar' : 'Collapse sidebar'}
+              style={{
+                flex: 1,
+                display: 'flex',
+                alignItems: 'center',
+                justifyContent: 'center',
+                padding: '7px 0',
+                background: 'transparent',
+                border: '1px solid var(--border)',
+                borderRadius: 7,
+                cursor: 'pointer',
+                color: 'var(--text-muted)',
+                transition: 'all 150ms ease',
+              }}
+              onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--cyan)'; e.currentTarget.style.color = 'var(--cyan)'; }}
+              onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--text-muted)'; }}
+            >
+              <svg
+                style={{ width: 14, height: 14, transition: 'transform 200ms ease', transform: col ? 'rotate(180deg)' : 'none' }}
+                fill="none"
+                stroke="currentColor"
+                viewBox="0 0 24 24"
+              >
+                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
+              </svg>
+            </button>
+
+            {!col && (
+              <button
+                onClick={logout}
+                style={{
+                  padding: '7px 10px',
+                  background: 'transparent',
+                  border: '1px solid var(--border)',
+                  borderRadius: 7,
+                  cursor: 'pointer',
+                  color: 'var(--text-muted)',
+                  fontSize: 11,
+                  fontFamily: 'var(--font-body)',
+                  fontWeight: 500,
+                  transition: 'all 150ms ease',
+                  whiteSpace: 'nowrap',
+                }}
+                onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--red)'; e.currentTarget.style.color = 'var(--red)'; }}
+                onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--text-muted)'; }}
+              >
+                Logout
+              </button>
+            )}
+          </div>
+        </div>
+      </aside>
+
+      {/* ── Main content ── */}
+      <main style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', display: 'flex', flexDirection: 'column' }}>
+        {/* Ticker Tape */}
+        <TickerTape />
+
+        {/* Market Status Bar */}
+        <div className="market-status-bar">
+          <span style={{ color: 'var(--cyan)', fontWeight: 700, letterSpacing: '0.06em' }}>KEN TRADER</span>
+          <span style={{ color: 'var(--border-light)' }}>|</span>
+          <span>
+            KALSHI{' '}
+            <span style={{ color: h?.kalshi_connected ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>
+              {h?.kalshi_connected ? 'LIVE' : 'OFFLINE'}
+            </span>
+          </span>
+          <span style={{ color: 'var(--border-light)' }}>|</span>
+          <span>
+            DB{' '}
+            <span style={{ color: h?.db_connected ? 'var(--green)' : 'var(--red)', fontWeight: 700 }}>
+              {h?.db_connected ? 'OK' : 'DOWN'}
+            </span>
+          </span>
+          <span style={{ color: 'var(--border-light)' }}>|</span>
+          <span>
+            ENV{' '}
+            <span style={{ color: h?.env === 'prod' ? 'var(--green)' : 'var(--orange)', fontWeight: 700 }}>
+              {(h?.env || 'DEMO').toUpperCase()}
+            </span>
+          </span>
+          {h?.safe_mode && (
+            <>
+              <span style={{ color: 'var(--border-light)' }}>|</span>
+              <span style={{ color: 'var(--orange)', fontWeight: 700 }}>SAFE MODE ON</span>
+            </>
+          )}
+          <span style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 12 }}>
+            <span style={{ color: 'var(--border-light)' }}>
+              {new Date().toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles', hour: '2-digit', minute: '2-digit', second: '2-digit' })} PT
+            </span>
+            {/* Notification Bell */}
+            <button
+              className="notification-bell"
+              aria-label="Alerts (7 unread)"
+              title="7 unread alerts"
+              style={{ background: 'none', border: 'none', display: 'flex', alignItems: 'center' }}
+            >
+              <svg fill="none" stroke="currentColor" strokeWidth="1.8" viewBox="0 0 24 24" aria-hidden="true">
+                <path strokeLinecap="round" strokeLinejoin="round" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
+              </svg>
+              <span className="notification-count" aria-label="7 alerts">7</span>
+            </button>
+          </span>
+        </div>
+
+        <div style={{ maxWidth: 1600, margin: '0 auto', padding: '24px 28px', flex: 1, width: '100%' }}>
+          <Outlet />
+        </div>
+      </main>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/Login.jsx b/kalshi-dash/src/Login.jsx
new file mode 100644
index 0000000..28281ef
--- /dev/null
+++ b/kalshi-dash/src/Login.jsx
@@ -0,0 +1,343 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { login, checkSession, googleLogin, getGoogleConfig } from './api';
+
+/* CSS-only animated dot background is defined inline via keyframes injected once */
+const DOT_STYLE = `
+@keyframes floatDot {
+  0%, 100% { transform: translateY(0px) scale(1); opacity: 0.18; }
+  50% { transform: translateY(-18px) scale(1.1); opacity: 0.35; }
+}
+.login-dot {
+  position: absolute;
+  border-radius: 50%;
+  background: var(--cyan);
+  animation: floatDot linear infinite;
+  pointer-events: none;
+}
+`;
+
+const DOTS = Array.from({ length: 22 }, (_, i) => ({
+  size: 2 + Math.random() * 4,
+  top: Math.random() * 100,
+  left: Math.random() * 100,
+  duration: 4 + Math.random() * 6,
+  delay: -(Math.random() * 8),
+}));
+
+export default function Login() {
+  const [user, setUser] = useState('');
+  const [pass, setPass] = useState('');
+  const [showPass, setShowPass] = useState(false);
+  const [err, setErr] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [checking, setChecking] = useState(true);
+  const [googleReady, setGoogleReady] = useState(false);
+  const [googleClientId, setGoogleClientId] = useState(null);
+  const googleBtnRef = useRef(null);
+  const nav = useNavigate();
+
+  useEffect(() => {
+    checkSession().then(ok => { if (ok) nav('/'); else setChecking(false); });
+    getGoogleConfig().then(cfg => {
+      if (cfg.configured && cfg.client_id) {
+        setGoogleClientId(cfg.client_id);
+        loadGoogleScript(cfg.client_id);
+      }
+    });
+  }, []);
+
+  function loadGoogleScript(clientId) {
+    if (document.getElementById('google-gsi')) { initGoogle(clientId); return; }
+    const s = document.createElement('script');
+    s.id = 'google-gsi'; s.src = 'https://accounts.google.com/gsi/client';
+    s.async = true; s.defer = true;
+    s.onload = () => initGoogle(clientId);
+    document.head.appendChild(s);
+  }
+
+  function initGoogle(clientId) {
+    if (!window.google?.accounts?.id) return;
+    window.google.accounts.id.initialize({ client_id: clientId, callback: handleGoogleResponse, auto_select: false });
+    setGoogleReady(true);
+  }
+
+  useEffect(() => {
+    if (googleReady && googleBtnRef.current && window.google?.accounts?.id) {
+      window.google.accounts.id.renderButton(googleBtnRef.current, {
+        type: 'standard', theme: 'filled_black', size: 'large', text: 'signin_with', shape: 'pill', width: 360,
+      });
+    }
+  }, [googleReady]);
+
+  async function handleGoogleResponse(response) {
+    setErr(''); setLoading(true);
+    try {
+      const r = await googleLogin(response.credential);
+      if (r.success) nav('/'); else setErr(r.error || 'Google login failed');
+    } catch { setErr('Connection error'); }
+    setLoading(false);
+  }
+
+  async function submit(e) {
+    e.preventDefault(); setErr(''); setLoading(true);
+    try {
+      const ok = await login(user, pass);
+      if (ok) nav('/'); else setErr('Invalid credentials');
+    } catch { setErr('Connection error'); }
+    setLoading(false);
+  }
+
+  if (checking) {
+    return (
+      <div
+        className="flex items-center justify-center min-h-screen"
+        style={{ background: 'var(--bg)' }}
+      >
+        <div
+          className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
+          style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+        />
+      </div>
+    );
+  }
+
+  return (
+    <div
+      className="min-h-screen flex items-center justify-center p-4"
+      style={{ background: 'var(--bg)', position: 'relative', overflow: 'hidden' }}
+    >
+      {/* Inject dot animation styles once */}
+      <style>{DOT_STYLE}</style>
+
+      {/* CSS-only animated background dots */}
+      {DOTS.map((dot, i) => (
+        <span
+          key={i}
+          className="login-dot"
+          style={{
+            width: dot.size,
+            height: dot.size,
+            top: `${dot.top}%`,
+            left: `${dot.left}%`,
+            animationDuration: `${dot.duration}s`,
+            animationDelay: `${dot.delay}s`,
+          }}
+        />
+      ))}
+
+      {/* Subtle radial glow behind card */}
+      <div
+        style={{
+          position: 'absolute',
+          top: '50%',
+          left: '50%',
+          transform: 'translate(-50%, -50%)',
+          width: 600,
+          height: 600,
+          borderRadius: '50%',
+          background: 'radial-gradient(ellipse, rgba(0,240,255,0.04) 0%, transparent 70%)',
+          pointerEvents: 'none',
+        }}
+      />
+
+      {/* Login card */}
+      <div
+        className="card-glow fade-in w-full"
+        style={{ maxWidth: 400, position: 'relative', zIndex: 1 }}
+      >
+        {/* Logo + heading */}
+        <div className="flex flex-col items-center mb-7">
+          {/* Large K badge */}
+          <div
+            style={{
+              width: 72,
+              height: 72,
+              borderRadius: 18,
+              background: 'linear-gradient(135deg, var(--orange), #C45A08)',
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              marginBottom: 16,
+              boxShadow: '0 4px 24px rgba(247,147,26,0.35)',
+            }}
+          >
+            <span
+              style={{
+                color: '#0E0E10',
+                fontWeight: 900,
+                fontSize: 32,
+                fontFamily: 'var(--font-heading)',
+                lineHeight: 1,
+              }}
+            >
+              K
+            </span>
+          </div>
+
+          <h1
+            className="kalshi-gradient heading"
+            style={{ fontSize: 26, fontWeight: 800, letterSpacing: '-0.02em', marginBottom: 6 }}
+          >
+            KEN TRADER
+          </h1>
+          <p style={{ fontSize: 13, color: 'var(--text-muted)', marginBottom: 4 }}>
+            Event Contracts Trading Platform
+          </p>
+          <span className="badge bg-green" style={{ fontSize: 10 }}>
+            CFTC-Regulated DCM
+          </span>
+        </div>
+
+        {/* Error alert */}
+        {err && (
+          <div
+            className="fade-in"
+            style={{
+              display: 'flex',
+              alignItems: 'center',
+              gap: 8,
+              background: 'var(--red-dim)',
+              border: '1px solid rgba(255,92,92,0.25)',
+              borderRadius: 8,
+              padding: '10px 12px',
+              marginBottom: 16,
+              color: 'var(--red)',
+              fontSize: 13,
+            }}
+          >
+            <svg style={{ width: 15, height: 15, flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
+            </svg>
+            {err}
+          </div>
+        )}
+
+        {/* Google Sign-In */}
+        {googleClientId && (
+          <div style={{ marginBottom: 4 }}>
+            <div ref={googleBtnRef} style={{ display: 'flex', justifyContent: 'center', marginBottom: 16 }} />
+            <div style={{ position: 'relative', display: 'flex', alignItems: 'center', marginBottom: 20 }}>
+              <div style={{ flex: 1, height: 1, background: 'var(--border)' }} />
+              <span
+                style={{
+                  padding: '0 12px',
+                  fontSize: 11,
+                  color: 'var(--text-muted)',
+                  background: 'var(--surface)',
+                  whiteSpace: 'nowrap',
+                }}
+              >
+                or sign in with password
+              </span>
+              <div style={{ flex: 1, height: 1, background: 'var(--border)' }} />
+            </div>
+          </div>
+        )}
+
+        {/* Password form */}
+        <form onSubmit={submit}>
+          <div style={{ marginBottom: 14 }}>
+            <label
+              style={{
+                display: 'block',
+                fontSize: 11,
+                fontWeight: 600,
+                color: 'var(--text-dim)',
+                marginBottom: 6,
+                textTransform: 'uppercase',
+                letterSpacing: '0.06em',
+              }}
+            >
+              Username
+            </label>
+            <input
+              type="text"
+              value={user}
+              onChange={e => setUser(e.target.value)}
+              required
+              autoComplete="username"
+              placeholder="admin"
+              className="input"
+            />
+          </div>
+
+          <div style={{ marginBottom: 20 }}>
+            <label
+              style={{
+                display: 'block',
+                fontSize: 11,
+                fontWeight: 600,
+                color: 'var(--text-dim)',
+                marginBottom: 6,
+                textTransform: 'uppercase',
+                letterSpacing: '0.06em',
+              }}
+            >
+              Password
+            </label>
+            <div style={{ position: 'relative' }}>
+              <input
+                type={showPass ? 'text' : 'password'}
+                value={pass}
+                onChange={e => setPass(e.target.value)}
+                required
+                autoComplete="current-password"
+                placeholder="Enter password"
+                className="input"
+                style={{ paddingRight: '2.5rem' }}
+              />
+              <button
+                type="button"
+                onClick={() => setShowPass(!showPass)}
+                style={{
+                  position: 'absolute',
+                  right: 12,
+                  top: '50%',
+                  transform: 'translateY(-50%)',
+                  background: 'none',
+                  border: 'none',
+                  cursor: 'pointer',
+                  color: 'var(--text-muted)',
+                  padding: 0,
+                  display: 'flex',
+                }}
+              >
+                <svg style={{ width: 16, height: 16 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={showPass ? 'M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.879L21 21' : 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z'} />
+                </svg>
+              </button>
+            </div>
+          </div>
+
+          <button
+            type="submit"
+            disabled={loading}
+            className="btn btn-p btn-lg"
+            style={{ width: '100%', opacity: loading ? 0.6 : 1 }}
+          >
+            {loading && (
+              <span
+                className="w-4 h-4 rounded-full border-2 border-t-transparent animate-spin"
+                style={{ borderColor: '#0E0E10', borderTopColor: 'transparent' }}
+              />
+            )}
+            {loading ? 'Signing in...' : 'Sign In'}
+          </button>
+        </form>
+
+        <p
+          style={{
+            textAlign: 'center',
+            marginTop: 20,
+            fontSize: 10,
+            color: 'var(--text-muted)',
+            letterSpacing: '0.02em',
+          }}
+        >
+          Ken operates as a CFTC-regulated Designated Contract Market
+        </p>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/api.js b/kalshi-dash/src/api.js
new file mode 100644
index 0000000..4674d9e
--- /dev/null
+++ b/kalshi-dash/src/api.js
@@ -0,0 +1,45 @@
+const API = '/api';
+
+export async function api(path, opts = {}) {
+  const res = await fetch(`${API}${path}`, {
+    ...opts,
+    credentials: 'same-origin',
+    headers: { 'Content-Type': 'application/json', ...opts.headers },
+  });
+  if (res.status === 401 && !path.startsWith('/auth/')) {
+    window.location.href = '/login';
+    return {};
+  }
+  return res.json();
+}
+
+export async function post(path, body) {
+  return api(path, { method: 'POST', body: JSON.stringify(body) });
+}
+
+export async function checkSession() {
+  try {
+    const data = await api('/auth/session');
+    return data.authenticated === true;
+  } catch { return false; }
+}
+
+export async function login(username, password) {
+  const data = await post('/auth/login', { username, password });
+  return data.success === true;
+}
+
+export async function googleLogin(credential) {
+  const data = await post('/auth/google', { credential });
+  return data;
+}
+
+export async function getGoogleConfig() {
+  try { return await api('/auth/google-config'); }
+  catch { return { configured: false }; }
+}
+
+export async function logout() {
+  await post('/auth/logout', {});
+  window.location.href = '/login';
+}
diff --git a/kalshi-dash/src/components/MiniCandlestick.jsx b/kalshi-dash/src/components/MiniCandlestick.jsx
new file mode 100644
index 0000000..5c35e95
--- /dev/null
+++ b/kalshi-dash/src/components/MiniCandlestick.jsx
@@ -0,0 +1,117 @@
+/**
+ * MiniCandlestick — tiny SVG candlestick chart
+ *
+ * Props:
+ *   data      — array of { o, h, l, c } (open, high, low, close)
+ *   width     — SVG width in px (default 80)
+ *   height    — SVG height in px (default 32)
+ *   bullColor — color for bullish candles (c > o)
+ *   bearColor — color for bearish candles (c <= o)
+ *
+ * Usage:
+ *   <MiniCandlestick data={[{o:45,h:52,l:42,c:50}, ...]} />
+ *   <MiniCandlestick data={genCandles(seed, 9)} width={80} height={32} />
+ */
+
+/**
+ * Generate fake OHLC data that trends in a given direction.
+ * @param {number} seed  — deterministic seed
+ * @param {number} count — number of candles
+ * @param {'up'|'down'|'flat'} trend
+ */
+export function genCandles(seed, count = 9, trend = 'flat') {
+  let s = seed;
+  const rng = () => {
+    s = (s * 1664525 + 1013904223) & 0xffffffff;
+    return (Math.abs(s) % 1000) / 1000; // 0..1
+  };
+
+  const trendDelta = trend === 'up' ? 0.8 : trend === 'down' ? -0.8 : 0;
+  let price = 45 + rng() * 10;
+  const candles = [];
+
+  for (let i = 0; i < count; i++) {
+    const move = (rng() - 0.45 + trendDelta * 0.1) * 6;
+    const open = price;
+    const close = Math.max(5, Math.min(95, price + move));
+    const highExtra = rng() * 3;
+    const lowExtra = rng() * 3;
+    const high = Math.max(open, close) + highExtra;
+    const low = Math.min(open, close) - lowExtra;
+    candles.push({ o: open, h: high, l: low, c: close });
+    price = close;
+  }
+  return candles;
+}
+
+export default function MiniCandlestick({
+  data,
+  width = 80,
+  height = 32,
+  bullColor = 'var(--green)',
+  bearColor = 'var(--red)',
+}) {
+  if (!data || data.length === 0) return null;
+
+  const allLows  = data.map(d => d.l);
+  const allHighs = data.map(d => d.h);
+  const minVal   = Math.min(...allLows);
+  const maxVal   = Math.max(...allHighs);
+  const range    = maxVal - minVal || 1;
+
+  const pad    = 2;
+  const drawH  = height - pad * 2;
+  const drawW  = width - pad * 2;
+
+  // Normalize a value to SVG y coordinate (inverted — higher = lower y)
+  const yOf = (v) => pad + drawH - ((v - minVal) / range) * drawH;
+
+  const n       = data.length;
+  const slotW   = drawW / n;
+  const bodyW   = Math.max(2, slotW * 0.55);
+  const wickW   = 1;
+
+  return (
+    <svg
+      width={width}
+      height={height}
+      viewBox={`0 0 ${width} ${height}`}
+      style={{ display: 'block', flexShrink: 0 }}
+      aria-hidden="true"
+    >
+      {data.map((candle, i) => {
+        const isBull  = candle.c >= candle.o;
+        const color   = isBull ? bullColor : bearColor;
+        const cx      = pad + i * slotW + slotW / 2;
+        const bodyTop = yOf(Math.max(candle.o, candle.c));
+        const bodyBot = yOf(Math.min(candle.o, candle.c));
+        const bodyHt  = Math.max(1, bodyBot - bodyTop);
+
+        return (
+          <g key={i}>
+            {/* Wick */}
+            <line
+              x1={cx}
+              y1={yOf(candle.h)}
+              x2={cx}
+              y2={yOf(candle.l)}
+              stroke={color}
+              strokeWidth={wickW}
+              strokeOpacity={0.7}
+            />
+            {/* Body */}
+            <rect
+              x={cx - bodyW / 2}
+              y={bodyTop}
+              width={bodyW}
+              height={bodyHt}
+              fill={color}
+              fillOpacity={isBull ? 0.85 : 0.75}
+              rx={0.5}
+            />
+          </g>
+        );
+      })}
+    </svg>
+  );
+}
diff --git a/kalshi-dash/src/components/RingGauge.jsx b/kalshi-dash/src/components/RingGauge.jsx
new file mode 100644
index 0000000..69ce4be
--- /dev/null
+++ b/kalshi-dash/src/components/RingGauge.jsx
@@ -0,0 +1,138 @@
+import React, { useEffect, useRef, useState } from 'react';
+
+/**
+ * RingGauge — animated SVG circular progress ring
+ *
+ * Props:
+ *   value       — number 0-100 (percentage to fill)
+ *   size        — px diameter (default: 48)
+ *   strokeWidth — px ring width (default: 4)
+ *   color       — CSS color (default: var(--cyan))
+ *   label       — optional text below the value
+ *   showValue   — whether to show percentage in center (default: true)
+ *
+ * Usage:
+ *   <RingGauge value={73} size={56} color="var(--green)" label="Win Rate" />
+ */
+export default function RingGauge({
+  value = 0,
+  size = 48,
+  strokeWidth = 4,
+  color = 'var(--cyan)',
+  label,
+  showValue = true,
+}) {
+  const [animated, setAnimated] = useState(0);
+  const rafRef = useRef(null);
+  const startRef = useRef(null);
+  const duration = 900; // ms
+
+  useEffect(() => {
+    const target = Math.min(100, Math.max(0, value));
+    const from = animated;
+
+    const animate = (ts) => {
+      if (!startRef.current) startRef.current = ts;
+      const elapsed = ts - startRef.current;
+      const t = Math.min(1, elapsed / duration);
+      // ease-out cubic
+      const eased = 1 - Math.pow(1 - t, 3);
+      setAnimated(from + (target - from) * eased);
+      if (t < 1) {
+        rafRef.current = requestAnimationFrame(animate);
+      }
+    };
+
+    startRef.current = null;
+    rafRef.current = requestAnimationFrame(animate);
+    return () => cancelAnimationFrame(rafRef.current);
+  }, [value]); // eslint-disable-line react-hooks/exhaustive-deps
+
+  const radius = (size - strokeWidth) / 2;
+  const circumference = 2 * Math.PI * radius;
+  const offset = circumference - (animated / 100) * circumference;
+  const center = size / 2;
+  const fontSize = size <= 40 ? size * 0.22 : size * 0.2;
+
+  return (
+    <div
+      style={{
+        display: 'inline-flex',
+        flexDirection: 'column',
+        alignItems: 'center',
+        gap: 4,
+      }}
+    >
+      <div style={{ position: 'relative', width: size, height: size }}>
+        <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} style={{ display: 'block' }}>
+          {/* Track ring */}
+          <circle
+            cx={center}
+            cy={center}
+            r={radius}
+            fill="none"
+            stroke="var(--border)"
+            strokeWidth={strokeWidth}
+          />
+          {/* Progress ring */}
+          <circle
+            cx={center}
+            cy={center}
+            r={radius}
+            fill="none"
+            stroke={color}
+            strokeWidth={strokeWidth}
+            strokeLinecap="round"
+            strokeDasharray={circumference}
+            strokeDashoffset={offset}
+            style={{
+              transformOrigin: `${center}px ${center}px`,
+              transform: 'rotate(-90deg)',
+              filter: `drop-shadow(0 0 3px ${color}80)`,
+            }}
+          />
+        </svg>
+
+        {showValue && (
+          <div
+            style={{
+              position: 'absolute',
+              inset: 0,
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+            }}
+          >
+            <span
+              style={{
+                fontFamily: 'var(--font-mono)',
+                fontWeight: 700,
+                fontSize: fontSize,
+                color: color,
+                lineHeight: 1,
+                letterSpacing: '-0.03em',
+              }}
+            >
+              {Math.round(animated)}%
+            </span>
+          </div>
+        )}
+      </div>
+
+      {label && (
+        <span
+          style={{
+            fontFamily: 'var(--font-body)',
+            fontSize: '0.6rem',
+            fontWeight: 600,
+            textTransform: 'uppercase',
+            letterSpacing: '0.08em',
+            color: 'var(--text-muted)',
+          }}
+        >
+          {label}
+        </span>
+      )}
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/components/Sparkline.jsx b/kalshi-dash/src/components/Sparkline.jsx
new file mode 100644
index 0000000..740aa16
--- /dev/null
+++ b/kalshi-dash/src/components/Sparkline.jsx
@@ -0,0 +1,91 @@
+import React, { useMemo } from 'react';
+
+/**
+ * Sparkline — tiny inline SVG line chart with gradient fill
+ *
+ * Props:
+ *   data   — array of numbers
+ *   color  — CSS color or var() (default: var(--cyan))
+ *   width  — px (default: 60)
+ *   height — px (default: 20)
+ *   id     — unique string for gradient ID (required when multiple sparklines are on the same page)
+ *
+ * Usage:
+ *   <Sparkline data={[12,18,14,22,19,25,21]} color="var(--green)" id="balance-spark" />
+ */
+export default function Sparkline({ data = [], color = 'var(--cyan)', width = 60, height = 20, id = 'spark' }) {
+  const points = useMemo(() => {
+    if (!data || data.length < 2) return null;
+
+    const min = Math.min(...data);
+    const max = Math.max(...data);
+    const range = max - min || 1;
+    const pad = 2; // px padding top/bottom
+
+    const xs = data.map((_, i) => (i / (data.length - 1)) * width);
+    const ys = data.map(v => height - pad - ((v - min) / range) * (height - pad * 2));
+
+    // Polyline points string
+    const polyline = xs.map((x, i) => `${x},${ys[i]}`).join(' ');
+
+    // Closed path for fill (go to bottom-right corner, bottom-left, back up)
+    const fillPath =
+      `M ${xs[0]},${ys[0]} ` +
+      xs.slice(1).map((x, i) => `L ${x},${ys[i + 1]}`).join(' ') +
+      ` L ${xs[xs.length - 1]},${height} L ${xs[0]},${height} Z`;
+
+    return { polyline, fillPath };
+  }, [data, width, height]);
+
+  if (!points) {
+    return <svg width={width} height={height} />;
+  }
+
+  const gradId = `spark-grad-${id}`;
+
+  return (
+    <svg
+      width={width}
+      height={height}
+      viewBox={`0 0 ${width} ${height}`}
+      style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }}
+      aria-hidden="true"
+    >
+      <defs>
+        <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
+          <stop offset="0%" stopColor={color} stopOpacity="0.25" />
+          <stop offset="100%" stopColor={color} stopOpacity="0.01" />
+        </linearGradient>
+      </defs>
+
+      {/* Gradient fill area */}
+      <path
+        d={points.fillPath}
+        fill={`url(#${gradId})`}
+      />
+
+      {/* The line */}
+      <polyline
+        points={points.polyline}
+        fill="none"
+        stroke={color}
+        strokeWidth="1.5"
+        strokeLinejoin="round"
+        strokeLinecap="round"
+      />
+
+      {/* Endpoint dot */}
+      {(() => {
+        const lastX = (data.length - 1) / (data.length - 1) * width;
+        const min = Math.min(...data);
+        const max = Math.max(...data);
+        const range = max - min || 1;
+        const pad = 2;
+        const lastY = height - pad - ((data[data.length - 1] - min) / range) * (height - pad * 2);
+        return (
+          <circle cx={lastX} cy={lastY} r="2" fill={color} />
+        );
+      })()}
+    </svg>
+  );
+}
diff --git a/kalshi-dash/src/components/TickerTape.jsx b/kalshi-dash/src/components/TickerTape.jsx
new file mode 100644
index 0000000..ae7186e
--- /dev/null
+++ b/kalshi-dash/src/components/TickerTape.jsx
@@ -0,0 +1,146 @@
+import React, { useMemo } from 'react';
+
+/**
+ * TickerTape — horizontally scrolling market ticker bar
+ *
+ * Shows 15-20 fake Kalshi-style contract tickers scrolling left infinitely.
+ * Integrated in Layout.jsx above the main content area.
+ */
+
+const TICKERS = [
+  { sym: 'POTUS-2028', price: '52c', chg: '+3.1%', up: true },
+  { sym: 'FED-RATE-CUT', price: '78c', chg: '-1.2%', up: false },
+  { sym: 'BTC-100K', price: '34c', chg: '+5.8%', up: true },
+  { sym: 'NVIDIA-BEAT', price: '91c', chg: '+0.4%', up: true },
+  { sym: 'SP-5500', price: '67c', chg: '-2.3%', up: false },
+  { sym: 'INFLATION-4PCT', price: '21c', chg: '+1.7%', up: true },
+  { sym: 'RECESSION-2025', price: '18c', chg: '-0.9%', up: false },
+  { sym: 'ETH-5K', price: '45c', chg: '+2.2%', up: true },
+  { sym: 'APPLE-3T', price: '83c', chg: '+0.6%', up: true },
+  { sym: 'FED-HOLD-JUN', price: '62c', chg: '-1.5%', up: false },
+  { sym: 'JOBS-200K', price: '55c', chg: '+0.8%', up: true },
+  { sym: 'OIL-90', price: '29c', chg: '-3.1%', up: false },
+  { sym: 'OPENAI-IPO', price: '41c', chg: '+4.4%', up: true },
+  { sym: 'GOLD-3K', price: '71c', chg: '+1.9%', up: true },
+  { sym: 'TESLA-SPLIT', price: '37c', chg: '-0.7%', up: false },
+  { sym: 'CPI-3PCT', price: '48c', chg: '+2.6%', up: true },
+  { sym: 'TRUMP-TARIFF', price: '86c', chg: '+0.3%', up: true },
+  { sym: 'VIX-SPIKE', price: '16c', chg: '-4.2%', up: false },
+  { sym: 'SPACEX-LAUNCH', price: '94c', chg: '+0.1%', up: true },
+  { sym: 'SENATE-FLIP', price: '33c', chg: '+3.7%', up: true },
+];
+
+// Dot separator between ticker items
+function Dot() {
+  return (
+    <span
+      style={{
+        display: 'inline-block',
+        width: 3,
+        height: 3,
+        borderRadius: '50%',
+        background: 'var(--cyan)',
+        opacity: 0.4,
+        margin: '0 14px',
+        verticalAlign: 'middle',
+        flexShrink: 0,
+      }}
+    />
+  );
+}
+
+function TickerItem({ sym, price, chg, up }) {
+  return (
+    <span
+      style={{
+        display: 'inline-flex',
+        alignItems: 'center',
+        gap: 6,
+        whiteSpace: 'nowrap',
+        fontFamily: 'var(--font-mono)',
+        fontSize: '0.68rem',
+      }}
+    >
+      <span style={{ color: 'var(--text-dim)', fontWeight: 600, letterSpacing: '0.04em' }}>{sym}</span>
+      <span style={{ color: 'var(--text)', fontWeight: 700 }}>{price}</span>
+      <span
+        style={{
+          color: up ? 'var(--green)' : 'var(--red)',
+          fontWeight: 700,
+          fontSize: '0.62rem',
+        }}
+      >
+        {chg}
+      </span>
+    </span>
+  );
+}
+
+export default function TickerTape() {
+  // Double the items so the seamless loop works
+  const items = useMemo(() => [...TICKERS, ...TICKERS], []);
+
+  return (
+    <div
+      style={{
+        width: '100%',
+        overflow: 'hidden',
+        background: 'var(--surface)',
+        borderBottom: '1px solid var(--border)',
+        borderTop: '1px solid var(--border)',
+        height: 32,
+        display: 'flex',
+        alignItems: 'center',
+        position: 'relative',
+      }}
+      aria-label="Live market ticker"
+      role="marquee"
+    >
+      {/* Left fade mask */}
+      <div
+        style={{
+          position: 'absolute',
+          left: 0,
+          top: 0,
+          bottom: 0,
+          width: 48,
+          background: 'linear-gradient(90deg, var(--surface), transparent)',
+          zIndex: 2,
+          pointerEvents: 'none',
+        }}
+      />
+
+      {/* Right fade mask */}
+      <div
+        style={{
+          position: 'absolute',
+          right: 0,
+          top: 0,
+          bottom: 0,
+          width: 48,
+          background: 'linear-gradient(270deg, var(--surface), transparent)',
+          zIndex: 2,
+          pointerEvents: 'none',
+        }}
+      />
+
+      {/* Scrolling strip */}
+      <div
+        style={{
+          display: 'inline-flex',
+          alignItems: 'center',
+          animation: 'ticker 60s linear infinite',
+          willChange: 'transform',
+          paddingLeft: 24,
+        }}
+      >
+        {items.map((t, i) => (
+          <React.Fragment key={i}>
+            <TickerItem {...t} />
+            <Dot />
+          </React.Fragment>
+        ))}
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/hooks/useAnimatedValue.js b/kalshi-dash/src/hooks/useAnimatedValue.js
new file mode 100644
index 0000000..a63350c
--- /dev/null
+++ b/kalshi-dash/src/hooks/useAnimatedValue.js
@@ -0,0 +1,47 @@
+import { useState, useEffect, useRef } from 'react';
+
+/**
+ * useAnimatedValue — smoothly animate between number values using rAF
+ *
+ * @param {number} target   — the target value to animate toward
+ * @param {number} duration — animation duration in ms (default: 800)
+ * @returns {number}        — the current interpolated value (use in your render)
+ *
+ * Usage:
+ *   const animatedBalance = useAnimatedValue(data?.balance ?? 0);
+ *   // renders smooth transition whenever `data.balance` changes
+ */
+export default function useAnimatedValue(target, duration = 800) {
+  const [current, setCurrent] = useState(target);
+  const fromRef = useRef(target);
+  const startRef = useRef(null);
+  const rafRef = useRef(null);
+  const targetRef = useRef(target);
+
+  useEffect(() => {
+    targetRef.current = target;
+    fromRef.current = current;
+    startRef.current = null;
+
+    const animate = (ts) => {
+      if (!startRef.current) startRef.current = ts;
+      const elapsed = ts - startRef.current;
+      const t = Math.min(1, elapsed / duration);
+
+      // ease-out exponential
+      const eased = t === 1 ? 1 : 1 - Math.pow(2, -10 * t);
+
+      const next = fromRef.current + (targetRef.current - fromRef.current) * eased;
+      setCurrent(next);
+
+      if (t < 1) {
+        rafRef.current = requestAnimationFrame(animate);
+      }
+    };
+
+    rafRef.current = requestAnimationFrame(animate);
+    return () => cancelAnimationFrame(rafRef.current);
+  }, [target]); // eslint-disable-line react-hooks/exhaustive-deps
+
+  return current;
+}
diff --git a/kalshi-dash/src/index.css b/kalshi-dash/src/index.css
new file mode 100644
index 0000000..0aea79d
--- /dev/null
+++ b/kalshi-dash/src/index.css
@@ -0,0 +1,913 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+/* ═══════════════════════════════════════════════════════════
+   KEN NEON — Design System
+   Based on CrypTrade Stock Trader UI Kit (Figma Community)
+   Colors: Neon Cyan (#00F0FF) + Bitcoin Orange (#F7931A)
+   Typography: Space Grotesk / Inter / JetBrains Mono
+   ═══════════════════════════════════════════════════════════ */
+
+:root {
+  --bg: #0E0E10;
+  --surface: #141419;
+  --surface-el: #1C1C24;
+  --border: #2A2A35;
+  --border-light: #3A3A48;
+  --cyan: #00F0FF;
+  --cyan-dim: rgba(0, 240, 255, 0.15);
+  --cyan-glow: rgba(0, 240, 255, 0.08);
+  --orange: #F7931A;
+  --orange-dim: rgba(247, 147, 26, 0.15);
+  --green: #00C389;
+  --green-dim: rgba(0, 195, 137, 0.15);
+  --red: #FF5C5C;
+  --red-dim: rgba(255, 92, 92, 0.15);
+  --blue: #3B82F6;
+  --blue-dim: rgba(59, 130, 246, 0.15);
+  --purple: #A855F7;
+  --purple-dim: rgba(168, 85, 247, 0.15);
+  --text: #E0E0E0;
+  --text-dim: #A8A8A8;
+  --text-muted: #6B7280;
+  --radius-sm: 6px;
+  --radius-md: 10px;
+  --radius-lg: 14px;
+  --radius-xl: 20px;
+  --transition: 150ms ease;
+  --font-heading: 'Space Grotesk', sans-serif;
+  --font-body: 'Inter', sans-serif;
+  --font-mono: 'JetBrains Mono', monospace;
+}
+
+/* ─── Base ─── */
+body {
+  background: var(--bg);
+  color: var(--text);
+  font-family: var(--font-body);
+  margin: 0;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+h1, h2, h3, h4, h5, h6 {
+  font-family: var(--font-heading);
+  font-weight: 700;
+  letter-spacing: -0.02em;
+}
+
+/* ─── Scrollbar ─── */
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: var(--bg); }
+::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: #4B5563; }
+
+/* ─── Cards ─── */
+.card {
+  background: var(--surface);
+  border: 1px solid var(--border);
+  border-radius: var(--radius-lg);
+  padding: 1.25rem;
+  transition: all var(--transition);
+}
+.card:hover {
+  border-color: rgba(0, 240, 255, 0.25);
+  box-shadow: 0 0 0 1px rgba(0, 240, 255, 0.08), 0 0 24px rgba(0, 240, 255, 0.12), 0 4px 16px rgba(0, 0, 0, 0.3);
+}
+.card-elevated {
+  background: var(--surface-el);
+  border: 1px solid var(--border);
+  border-radius: var(--radius-lg);
+  padding: 1.25rem;
+}
+.card-glow {
+  background: var(--surface);
+  border: 1px solid rgba(0, 240, 255, 0.2);
+  border-radius: var(--radius-lg);
+  padding: 1.25rem;
+  box-shadow: 0 0 30px var(--cyan-glow), inset 0 1px 0 rgba(0, 240, 255, 0.05);
+}
+
+/* ─── Stat Values ─── */
+.stat-v {
+  font-family: var(--font-mono);
+  font-size: 1.75rem;
+  font-weight: 600;
+  line-height: 1;
+  letter-spacing: -0.02em;
+}
+.stat-label {
+  font-size: 0.7rem;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  color: var(--text-muted);
+  font-family: var(--font-body);
+}
+
+/* ─── Badges ─── */
+.badge {
+  display: inline-flex;
+  align-items: center;
+  gap: 4px;
+  padding: 3px 10px;
+  border-radius: 9999px;
+  font-size: 0.7rem;
+  font-weight: 600;
+  font-family: var(--font-body);
+  letter-spacing: 0.02em;
+}
+.bg-cyan { background: var(--cyan-dim); color: var(--cyan); }
+.bg-green { background: var(--green-dim); color: var(--green); }
+.bg-red { background: var(--red-dim); color: var(--red); }
+.bg-yellow, .bg-orange { background: var(--orange-dim); color: var(--orange); }
+.bg-blue { background: var(--blue-dim); color: var(--blue); }
+.bg-purple { background: var(--purple-dim); color: var(--purple); }
+
+/* ─── Buttons ─── */
+.btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  padding: 0.5rem 1rem;
+  border-radius: var(--radius-md);
+  font-weight: 600;
+  font-size: 0.85rem;
+  font-family: var(--font-body);
+  cursor: pointer;
+  border: none;
+  transition: all var(--transition);
+}
+.btn-p {
+  background: var(--cyan);
+  color: #0E0E10;
+}
+.btn-p:hover { background: #00D4E0; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,240,255,0.25); }
+.btn-orange {
+  background: var(--orange);
+  color: #0E0E10;
+}
+.btn-orange:hover { background: #E5850F; }
+.btn-d { background: var(--red); color: white; }
+.btn-d:hover { background: #E04A4A; }
+.btn-s { background: var(--green); color: #0E0E10; }
+.btn-s:hover { background: #00A875; }
+.btn-o {
+  background: transparent;
+  border: 1px solid var(--border);
+  color: var(--text-dim);
+}
+.btn-o:hover { border-color: var(--cyan); color: var(--cyan); }
+.btn-ghost {
+  background: transparent;
+  color: var(--text-dim);
+  border: none;
+}
+.btn-ghost:hover { color: var(--cyan); background: var(--cyan-glow); }
+.btn-sm { padding: 0.35rem 0.75rem; font-size: 0.75rem; }
+.btn-lg { padding: 0.75rem 1.5rem; font-size: 1rem; }
+
+/* ─── Tables ─── */
+table { width: 100%; border-collapse: collapse; }
+th {
+  text-align: left;
+  padding: 0.75rem;
+  font-size: 0.65rem;
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  color: var(--text-muted);
+  font-family: var(--font-body);
+  font-weight: 600;
+  border-bottom: 1px solid var(--border);
+}
+td {
+  padding: 0.75rem;
+  border-bottom: 1px solid rgba(42, 42, 53, 0.5);
+  font-size: 0.85rem;
+}
+tr:hover td { background: var(--cyan-glow); }
+
+/* ─── Inputs ─── */
+.input {
+  width: 100%;
+  padding: 0.6rem 0.85rem;
+  background: var(--surface-el);
+  border: 1px solid var(--border);
+  border-radius: var(--radius-md);
+  color: var(--text);
+  font-family: var(--font-body);
+  font-size: 0.85rem;
+  transition: all var(--transition);
+  outline: none;
+}
+.input:focus {
+  border-color: var(--cyan);
+  box-shadow: 0 0 0 3px rgba(0, 240, 255, 0.1);
+}
+.input::placeholder { color: var(--text-muted); }
+
+/* ─── Gradients ─── */
+.kalshi-gradient {
+  background: linear-gradient(135deg, var(--cyan), var(--orange));
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+.gradient-cyan {
+  background: linear-gradient(135deg, var(--cyan), #0088FF);
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  background-clip: text;
+}
+
+/* ─── Animations ─── */
+.pulse-cyan { animation: pulse-c 2s infinite; }
+@keyframes pulse-c {
+  0%, 100% { box-shadow: 0 0 0 0 rgba(0, 240, 255, 0.4); }
+  50% { box-shadow: 0 0 0 6px rgba(0, 240, 255, 0); }
+}
+
+.pulse-g { animation: pg 2s infinite; }
+@keyframes pg {
+  0%, 100% { box-shadow: 0 0 0 0 rgba(0, 195, 137, 0.4); }
+  50% { box-shadow: 0 0 0 6px rgba(0, 195, 137, 0); }
+}
+
+.glow-cyan {
+  box-shadow: 0 0 20px rgba(0, 240, 255, 0.15), 0 0 60px rgba(0, 240, 255, 0.05);
+}
+
+.fade-in {
+  animation: fadeIn 0.3s ease-out;
+}
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(8px); }
+  to { opacity: 1; transform: translateY(0); }
+}
+
+/* ─── Section Headers ─── */
+.section-title {
+  font-family: var(--font-heading);
+  font-size: 1.25rem;
+  font-weight: 700;
+  color: var(--text);
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+.section-subtitle {
+  font-size: 0.8rem;
+  color: var(--text-muted);
+  margin-top: 2px;
+}
+
+/* ─── Toggle Switch ─── */
+.toggle {
+  position: relative;
+  width: 40px;
+  height: 22px;
+  background: var(--border);
+  border-radius: 11px;
+  cursor: pointer;
+  transition: background var(--transition);
+}
+.toggle.active { background: var(--cyan); }
+.toggle::after {
+  content: '';
+  position: absolute;
+  top: 2px;
+  left: 2px;
+  width: 18px;
+  height: 18px;
+  background: white;
+  border-radius: 50%;
+  transition: transform var(--transition);
+}
+.toggle.active::after { transform: translateX(18px); }
+
+/* ─── Progress Bar ─── */
+.progress-bar {
+  height: 4px;
+  background: var(--border);
+  border-radius: 2px;
+  overflow: hidden;
+}
+.progress-fill {
+  height: 100%;
+  border-radius: 2px;
+  background: linear-gradient(90deg, var(--cyan), #0088FF);
+  transition: width 0.5s ease;
+}
+
+/* ─── Utility ─── */
+.mono { font-family: var(--font-mono); }
+.heading { font-family: var(--font-heading); }
+.text-cyan { color: var(--cyan); }
+.text-orange { color: var(--orange); }
+.text-green { color: var(--green); }
+.text-red { color: var(--red); }
+.text-dim { color: var(--text-dim); }
+.text-muted { color: var(--text-muted); }
+.border-cyan { border-color: var(--cyan) !important; }
+
+/* ─── Glass Card ─── */
+.card-glass {
+  background: rgba(20, 20, 25, 0.8);
+  backdrop-filter: blur(12px);
+  -webkit-backdrop-filter: blur(12px);
+  border: 1px solid rgba(0, 240, 255, 0.1);
+  border-radius: var(--radius-lg);
+}
+
+/* ─── Neon Text Glow ─── */
+.text-glow {
+  text-shadow: 0 0 10px rgba(0, 240, 255, 0.5);
+}
+.text-glow-green {
+  text-shadow: 0 0 10px rgba(0, 195, 137, 0.4);
+}
+.text-glow-red {
+  text-shadow: 0 0 10px rgba(255, 92, 92, 0.4);
+}
+
+/* ─── Divider with Glow ─── */
+.divider-glow {
+  height: 1px;
+  background: linear-gradient(90deg, transparent, var(--cyan), transparent);
+  border: none;
+  margin: 0;
+}
+
+/* ─── Skeleton Shimmer ─── */
+.skeleton {
+  background: linear-gradient(
+    90deg,
+    var(--surface-el) 25%,
+    var(--border) 50%,
+    var(--surface-el) 75%
+  );
+  background-size: 200% 100%;
+  animation: shimmer 1.5s ease-in-out infinite;
+  border-radius: var(--radius-sm);
+}
+@keyframes shimmer {
+  0%   { background-position: 200% 0; }
+  100% { background-position: -200% 0; }
+}
+
+/* ─── Scrolling Ticker ─── */
+@keyframes ticker {
+  from { transform: translateX(0); }
+  to   { transform: translateX(-50%); }
+}
+
+/* ─── Button Press ─── */
+.btn:active:not(:disabled) {
+  transform: scale(0.97);
+}
+
+/* ─── Neon Focus Ring ─── */
+.neon-focus:focus-visible {
+  outline: none;
+  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--cyan), 0 0 12px rgba(0, 240, 255, 0.3);
+}
+
+/* ─── Stat card accent variants ─── */
+.card-accent-cyan  { border-top: 2px solid var(--cyan); }
+.card-accent-green { border-top: 2px solid var(--green); }
+.card-accent-red   { border-top: 2px solid var(--red); }
+.card-accent-orange { border-top: 2px solid var(--orange); }
+.card-accent-blue  { border-top: 2px solid var(--blue); }
+.card-accent-purple { border-top: 2px solid var(--purple); }
+
+/* ─── Table row: left-border accent on hover ─── */
+tr:hover td {
+  background: var(--cyan-glow);
+}
+tbody tr:hover td:first-child {
+  box-shadow: inset 3px 0 0 var(--cyan);
+}
+
+/* ─── Alternating table rows ─── */
+tbody tr:nth-child(even) td {
+  background: rgba(28, 28, 36, 0.4);
+}
+tbody tr:nth-child(even):hover td {
+  background: var(--cyan-glow);
+}
+
+/* ─── Market card hover lift ─── */
+.card-lift {
+  transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease;
+}
+.card-lift:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4), 0 0 20px rgba(0, 240, 255, 0.1);
+}
+
+/* ─── P&L glow variants ─── */
+.pnl-positive {
+  color: var(--green);
+  text-shadow: 0 0 8px rgba(0, 195, 137, 0.35);
+}
+.pnl-negative {
+  color: var(--red);
+  text-shadow: 0 0 8px rgba(255, 92, 92, 0.35);
+}
+
+/* ─── Section accent bar ─── */
+.section-accent-green {
+  border-left: 3px solid var(--green);
+  padding-left: 10px;
+}
+.section-accent-red {
+  border-left: 3px solid var(--red);
+  padding-left: 10px;
+}
+.section-accent-cyan {
+  border-left: 3px solid var(--cyan);
+  padding-left: 10px;
+}
+
+/* ─── Sidebar nav active glow ─── */
+.nav-active-glow {
+  box-shadow: inset 0 0 12px rgba(0, 240, 255, 0.08);
+}
+
+/* ─── Win/Loss bar glow ─── */
+.bar-win-glow {
+  box-shadow: 0 0 8px rgba(0, 195, 137, 0.5);
+}
+.bar-loss-glow {
+  box-shadow: 0 0 8px rgba(255, 92, 92, 0.5);
+}
+
+/* ═══════════════════════════════════════════════════════════
+   FEATURE ADDITIONS — CrypTrade UI Kit Inspired Elements
+   ═══════════════════════════════════════════════════════════ */
+
+/* ─── 3. LIVE Indicator ─── */
+.live-indicator {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  font-family: var(--font-mono);
+  font-size: 0.7rem;
+  font-weight: 700;
+  text-transform: uppercase;
+  letter-spacing: 0.1em;
+  color: var(--green);
+}
+.live-dot {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background: var(--green);
+  animation: live-blink 1.5s ease-in-out infinite;
+  flex-shrink: 0;
+}
+@keyframes live-blink {
+  0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(0, 195, 137, 0.6); }
+  50% { opacity: 0.4; box-shadow: 0 0 0 4px rgba(0, 195, 137, 0); }
+}
+
+/* ─── 5. CRT Scan Line Overlay ─── */
+@keyframes scan-line {
+  0%   { top: -5%; }
+  100% { top: 105%; }
+}
+.scan-overlay {
+  pointer-events: none;
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 9999;
+  overflow: hidden;
+}
+.scan-overlay::before {
+  content: '';
+  position: absolute;
+  width: 100%;
+  height: 2px;
+  background: linear-gradient(90deg, transparent, rgba(0, 240, 255, 0.06), transparent);
+  animation: scan-line 8s linear infinite;
+}
+.scan-overlay::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: repeating-linear-gradient(
+    0deg,
+    transparent,
+    transparent 2px,
+    rgba(0, 0, 0, 0.03) 2px,
+    rgba(0, 0, 0, 0.03) 4px
+  );
+}
+
+/* ─── 7. Sidebar Gradient Mesh ─── */
+.sidebar-mesh {
+  background:
+    radial-gradient(ellipse at 20% 80%, rgba(0, 240, 255, 0.04) 0%, transparent 50%),
+    radial-gradient(ellipse at 80% 20%, rgba(247, 147, 26, 0.03) 0%, transparent 50%),
+    var(--surface);
+}
+
+/* ─── 8. Market Status Bar ─── */
+.market-status-bar {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 6px 16px;
+  background: linear-gradient(90deg, rgba(0, 240, 255, 0.03), transparent, rgba(247, 147, 26, 0.03));
+  border-bottom: 1px solid var(--border);
+  font-size: 0.7rem;
+  color: var(--text-muted);
+  font-family: var(--font-mono);
+}
+
+/* ═══════════════════════════════════════════════════════════
+   ROUND 3 — Design Polish
+   ═══════════════════════════════════════════════════════════ */
+
+/* ─── 1. Animated Gradient Border (rotating neon conic-gradient) ─── */
+@property --neon-angle {
+  syntax: "<angle>";
+  initial-value: 0deg;
+  inherits: false;
+}
+@keyframes neon-rotate {
+  0%   { --neon-angle: 0deg; }
+  100% { --neon-angle: 360deg; }
+}
+.border-neon-animated {
+  position: relative;
+  border: none !important;
+  isolation: isolate;
+}
+.border-neon-animated::before {
+  content: '';
+  position: absolute;
+  inset: -2px;
+  border-radius: calc(var(--radius-lg) + 2px);
+  background: conic-gradient(from var(--neon-angle), var(--cyan), var(--orange), var(--green), var(--cyan));
+  animation: neon-rotate 3s linear infinite;
+  z-index: -1;
+}
+.border-neon-animated::after {
+  content: '';
+  position: absolute;
+  inset: 0;
+  border-radius: var(--radius-lg);
+  background: var(--surface);
+  z-index: -1;
+}
+
+/* ─── 3. Notification Bell ─── */
+.notification-bell {
+  position: relative;
+  cursor: pointer;
+  padding: 6px;
+  border-radius: var(--radius-sm);
+  transition: background 150ms ease;
+}
+.notification-bell:hover {
+  background: var(--cyan-glow);
+}
+.notification-bell svg {
+  width: 18px;
+  height: 18px;
+  color: var(--text-dim);
+  transition: color 150ms ease;
+}
+.notification-bell:hover svg {
+  color: var(--cyan);
+}
+.notification-count {
+  position: absolute;
+  top: 2px;
+  right: 2px;
+  min-width: 14px;
+  height: 14px;
+  border-radius: 7px;
+  background: var(--red);
+  color: white;
+  font-size: 0.55rem;
+  font-weight: 700;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0 3px;
+  border: 2px solid var(--bg);
+  animation: notification-pop 0.3s ease-out;
+}
+@keyframes notification-pop {
+  0%   { transform: scale(0); }
+  50%  { transform: scale(1.3); }
+  100% { transform: scale(1); }
+}
+
+/* ─── 4. Breathing Neon Glow ─── */
+@keyframes breathe-neon {
+  0%, 100% { box-shadow: 0 0 0 0 rgba(0, 240, 255, 0.1), 0 0 0 0 rgba(0, 240, 255, 0.05); }
+  50%       { box-shadow: 0 0 15px 2px rgba(0, 240, 255, 0.12), 0 0 40px 4px rgba(0, 240, 255, 0.04); }
+}
+@keyframes breathe-orange {
+  0%, 100% { box-shadow: 0 0 0 0 rgba(247, 147, 26, 0.1); }
+  50%       { box-shadow: 0 0 15px 2px rgba(247, 147, 26, 0.12); }
+}
+.breathe-neon {
+  animation: breathe-neon 4s ease-in-out infinite;
+}
+.breathe-orange {
+  animation: breathe-orange 3.5s ease-in-out infinite;
+}
+
+/* ─── 5. Animated Logo Glow ─── */
+@keyframes logo-pulse {
+  0%, 100% { filter: drop-shadow(0 0 4px rgba(247, 147, 26, 0.3)); }
+  50%       { filter: drop-shadow(0 0 12px rgba(247, 147, 26, 0.6)); }
+}
+.logo-animated {
+  animation: logo-pulse 3s ease-in-out infinite;
+}
+
+/* ─── 6. Position Status Indicators ─── */
+.position-winning {
+  border-left: 3px solid var(--green);
+  position: relative;
+}
+.position-winning::before {
+  content: '';
+  position: absolute;
+  left: -7px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: var(--green);
+  box-shadow: 0 0 8px rgba(0, 195, 137, 0.5);
+}
+.position-losing {
+  border-left: 3px solid var(--red);
+  position: relative;
+}
+.position-losing::before {
+  content: '';
+  position: absolute;
+  left: -7px;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: var(--red);
+  box-shadow: 0 0 8px rgba(255, 92, 92, 0.5);
+}
+
+/* ─── Chart Theme ─── */
+/* Recharts custom tooltip wrapper */
+.chart-tooltip {
+  background: var(--surface-el);
+  border: 1px solid var(--border);
+  border-radius: 8px;
+  padding: 10px 14px;
+  font-family: var(--font-body);
+  font-size: 12px;
+  color: var(--text);
+  min-width: 140px;
+}
+.chart-tooltip-label {
+  font-family: var(--font-body);
+  font-size: 11px;
+  font-weight: 600;
+  color: var(--text-muted);
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+  margin-bottom: 6px;
+}
+.chart-tooltip-row {
+  display: flex;
+  justify-content: space-between;
+  gap: 12px;
+  margin-bottom: 3px;
+}
+.chart-tooltip-key {
+  color: var(--text-muted);
+}
+.chart-tooltip-val {
+  font-family: var(--font-mono);
+  font-weight: 700;
+}
+
+/* Stat pill for chart cards */
+.chart-stat-pill {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 4px 12px;
+  border-radius: 20px;
+  font-size: 11px;
+  font-family: var(--font-body);
+}
+.chart-stat-pill .pill-label {
+  font-size: 10px;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  color: var(--text-muted);
+}
+.chart-stat-pill .pill-value {
+  font-family: var(--font-mono);
+  font-weight: 700;
+  font-size: 12px;
+}
+.chart-stat-pill-positive {
+  background: rgba(0, 195, 137, 0.10);
+  border: 1px solid rgba(0, 195, 137, 0.25);
+}
+.chart-stat-pill-negative {
+  background: rgba(255, 92, 92, 0.10);
+  border: 1px solid rgba(255, 92, 92, 0.25);
+}
+.chart-stat-pill-neutral {
+  background: rgba(107, 114, 128, 0.10);
+  border: 1px solid rgba(107, 114, 128, 0.25);
+}
+
+/* ═══════════════════════════════════════════════════════════
+   STRATEGY DEEP DIVE — Section Styles
+   ═══════════════════════════════════════════════════════════ */
+
+.strategy-deep-dive {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+/* ─── Section Header with Lines ─── */
+.sdd-header {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  padding: 8px 0;
+}
+.sdd-header-line {
+  flex: 1;
+  height: 1px;
+  background: linear-gradient(90deg, transparent, var(--cyan), transparent);
+  opacity: 0.4;
+}
+.sdd-header-content {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  flex-shrink: 0;
+}
+
+/* ─── Section Cards ─── */
+.sdd-section-card {
+  animation: sdd-fadeUp 0.4s ease-out both;
+}
+.sdd-section-card:nth-child(2) { animation-delay: 0.05s; }
+.sdd-section-card:nth-child(3) { animation-delay: 0.1s; }
+.sdd-section-card:nth-child(4) { animation-delay: 0.15s; }
+
+@keyframes sdd-fadeUp {
+  from { opacity: 0; transform: translateY(12px); }
+  to   { opacity: 1; transform: translateY(0); }
+}
+
+/* ─── Stat Glow on Key Numbers ─── */
+.sdd-stat-glow {
+  position: relative;
+}
+.sdd-stat-glow::after {
+  content: '';
+  position: absolute;
+  inset: -1px;
+  border-radius: 11px;
+  background: conic-gradient(from 0deg, rgba(0,240,255,0.08), rgba(0,195,137,0.08), rgba(0,240,255,0.08));
+  z-index: -1;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+.sdd-stat-glow:hover::after {
+  opacity: 1;
+}
+
+/* ─── Flow Row ─── */
+.sdd-flow-row {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+  justify-content: center;
+}
+.sdd-flow-step {
+  flex: 1;
+  max-width: 180px;
+  padding: 12px 10px;
+  border-radius: 10px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: transform 150ms ease, box-shadow 150ms ease;
+}
+.sdd-flow-step:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+}
+.sdd-flow-arrow {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  animation: sdd-arrow-pulse 2s ease-in-out infinite;
+}
+@keyframes sdd-arrow-pulse {
+  0%, 100% { opacity: 0.4; transform: translateX(0); }
+  50%       { opacity: 0.8; transform: translateX(2px); }
+}
+
+/* ─── Example Trades Table ─── */
+.sdd-trades-table {
+  width: 100%;
+  border-collapse: collapse;
+}
+.sdd-trades-table th {
+  text-align: center;
+  padding: 8px 10px;
+  font-size: 0.62rem;
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+  color: var(--text-muted);
+  font-family: var(--font-body);
+  font-weight: 600;
+  border-bottom: 1px solid var(--border);
+  white-space: nowrap;
+}
+.sdd-trades-table td {
+  padding: 10px 10px;
+  border-bottom: 1px solid rgba(42, 42, 53, 0.5);
+  font-size: 0.85rem;
+  white-space: nowrap;
+}
+.sdd-trades-table tbody tr {
+  transition: background 150ms ease;
+}
+.sdd-trades-table tbody tr:hover td {
+  background: rgba(0, 240, 255, 0.04);
+}
+.sdd-trades-table tbody tr:nth-child(even) td {
+  background: rgba(28, 28, 36, 0.4);
+}
+.sdd-trades-table tbody tr:nth-child(even):hover td {
+  background: rgba(0, 240, 255, 0.04);
+}
+
+/* ─── Insights Grid ─── */
+.sdd-insights-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 14px;
+}
+.sdd-insights-grid > :nth-child(4),
+.sdd-insights-grid > :nth-child(5) {
+  /* Last two cards span wider in 2-col layout for the bottom row */
+}
+
+.sdd-insight-card {
+  border-radius: 12px;
+  padding: 18px 16px;
+  transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease;
+}
+.sdd-insight-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35), 0 0 16px rgba(0, 240, 255, 0.06);
+}
+
+/* ─── Responsive adjustments ─── */
+@media (max-width: 1200px) {
+  .sdd-insights-grid {
+    grid-template-columns: repeat(2, 1fr);
+  }
+  .sdd-flow-row {
+    flex-wrap: wrap;
+  }
+  .sdd-flow-step {
+    max-width: none;
+  }
+}
+@media (max-width: 768px) {
+  .sdd-insights-grid {
+    grid-template-columns: 1fr;
+  }
+}
diff --git a/kalshi-dash/src/main.jsx b/kalshi-dash/src/main.jsx
new file mode 100644
index 0000000..bedd2ed
--- /dev/null
+++ b/kalshi-dash/src/main.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import './index.css';
+import Login from './Login';
+import Layout from './Layout';
+import SetupWizard from './setup/SetupWizard';
+import Dashboard from './pages/Dashboard';
+import Markets from './pages/Markets';
+import Positions from './pages/Positions';
+import Orders from './pages/Orders';
+import Weather from './pages/Weather';
+import Risk from './pages/Risk';
+import Settings from './pages/Settings';
+import Signals from './pages/Signals';
+import Intelligence from './pages/Intelligence';
+import Trading from './pages/Trading';
+import Heatmap from './pages/Heatmap';
+import Orbit from './pages/Orbit';
+import Pulse from './pages/Pulse';
+
+function App() {
+  return (
+    <BrowserRouter>
+      <Routes>
+        <Route path="/login" element={<Login />} />
+        <Route path="/setup" element={<SetupWizard />} />
+        <Route path="/" element={<Layout />}>
+          <Route index element={<Dashboard />} />
+          <Route path="markets" element={<Markets />} />
+          <Route path="positions" element={<Positions />} />
+          <Route path="orders" element={<Orders />} />
+          <Route path="signals" element={<Signals />} />
+          <Route path="intelligence" element={<Intelligence />} />
+          <Route path="trading" element={<Trading />} />
+          <Route path="heatmap" element={<Heatmap />} />
+          <Route path="orbit" element={<Orbit />} />
+          <Route path="pulse" element={<Pulse />} />
+          <Route path="weather" element={<Weather />} />
+          <Route path="risk" element={<Risk />} />
+          <Route path="settings" element={<Settings />} />
+        </Route>
+      </Routes>
+    </BrowserRouter>
+  );
+}
+
+ReactDOM.createRoot(document.getElementById('root')).render(<App />);
diff --git a/kalshi-dash/src/pages/Dashboard.jsx b/kalshi-dash/src/pages/Dashboard.jsx
new file mode 100644
index 0000000..d4a9f97
--- /dev/null
+++ b/kalshi-dash/src/pages/Dashboard.jsx
@@ -0,0 +1,1900 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+import { Link } from 'react-router-dom';
+import {
+  AreaChart, Area, BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid,
+  Tooltip, ResponsiveContainer, PieChart, Pie, RadialBarChart, RadialBar,
+  Legend,
+} from 'recharts';
+import Sparkline from '../components/Sparkline';
+import useAnimatedValue from '../hooks/useAnimatedValue';
+import { CHART_COLORS, TOOLTIP_STYLE, AXIS_TICK, GRID_STYLE, fmtDollars, fmtDateShort } from '../utils/chartTheme';
+
+/* ── Seeded random sparkline generator (stable per label) ── */
+function genSparkData(seed, count = 8) {
+  let s = seed;
+  return Array.from({ length: count }, () => {
+    s = (s * 1664525 + 1013904223) & 0xffffffff;
+    return (Math.abs(s) % 100) + 10;
+  });
+}
+
+/* Pre-generate stable sparkline datasets for each stat card */
+const SPARK_DATA = {
+  Balance:        genSparkData(42, 9),
+  Positions:      genSparkData(17, 9),
+  'Open Markets': genSparkData(93, 9),
+  'Today P&L':    genSparkData(55, 9),
+  'Markets Tracked': genSparkData(71, 9),
+};
+
+/* ── Helper: P&L badge ── */
+function PnlBadge({ cents }) {
+  const val = Number(cents || 0);
+  const dollars = (val / 100).toFixed(2);
+  if (val > 0) return <span className="mono" style={{ color: 'var(--green)', fontWeight: 700 }}>+${dollars}</span>;
+  if (val < 0) return <span className="mono" style={{ color: 'var(--red)', fontWeight: 700 }}>-${Math.abs(val / 100).toFixed(2)}</span>;
+  return <span className="mono" style={{ color: 'var(--text-muted)' }}>$0.00</span>;
+}
+
+/* ── Helper: Win/loss ratio ── */
+function WinRate({ wins, losses }) {
+  const total = wins + losses;
+  if (!total) return <span style={{ color: 'var(--text-muted)' }}>—</span>;
+  const pct = (wins / total * 100).toFixed(0);
+  const color = pct >= 80 ? 'var(--green)' : pct >= 50 ? 'var(--orange)' : 'var(--red)';
+  return <span style={{ color }}>{wins}/{total} ({pct}%)</span>;
+}
+
+/* ── Helper: Hold indicator badge ── */
+function HoldsIndicator({ wins, losses, pnl }) {
+  const total = wins + losses;
+  const wr = total > 0 ? wins / total : 0;
+  const base = { padding: '2px 8px', borderRadius: 4, fontSize: 10, fontWeight: 700, fontFamily: 'var(--font-body)' };
+  if (wr >= 0.90 && pnl > 0) return <span style={{ ...base, background: 'var(--green-dim)', color: 'var(--green)', border: '1px solid rgba(0,195,137,0.25)' }}>ADD MORE</span>;
+  if (wr >= 0.65 && pnl > 0) return <span style={{ ...base, background: 'var(--orange-dim)', color: 'var(--orange)', border: '1px solid rgba(247,147,26,0.25)' }}>HOLD</span>;
+  if (pnl < 0) return <span style={{ ...base, background: 'var(--red-dim)', color: 'var(--red)', border: '1px solid rgba(255,92,92,0.25)' }}>REDUCE</span>;
+  return <span style={{ ...base, background: 'rgba(107,114,128,0.15)', color: 'var(--text-muted)', border: '1px solid var(--border)' }}>WATCH</span>;
+}
+
+/* ── Top 3 Best Opportunities ── */
+function TopOpportunities({ opportunities }) {
+  const ops = opportunities || [];
+
+  if (!ops.length) {
+    return (
+      <div className="card">
+        <div className="section-title" style={{ marginBottom: 8 }}>
+          <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
+          </svg>
+          Best Opportunities Right Now
+        </div>
+        <p style={{ color: 'var(--text-muted)', fontSize: 13, textAlign: 'center', padding: '24px 0' }}>
+          No fresh signals — waiting for next pipeline cycle...
+        </p>
+      </div>
+    );
+  }
+
+  const rankStyles = [
+    { borderColor: 'rgba(247,147,26,0.35)', background: 'rgba(247,147,26,0.06)', rankColor: 'var(--orange)', rankLabel: '#1' },
+    { borderColor: 'rgba(160,160,160,0.25)', background: 'rgba(160,160,160,0.04)', rankColor: 'var(--text-dim)', rankLabel: '#2' },
+    { borderColor: 'rgba(180,100,50,0.25)', background: 'rgba(180,100,50,0.04)', rankColor: '#CD7F32', rankLabel: '#3' },
+  ];
+
+  return (
+    <div className="card" style={{ border: '1px solid rgba(0,240,255,0.15)', background: 'linear-gradient(135deg, rgba(0,240,255,0.03), var(--surface), rgba(0,195,137,0.03))' }}>
+      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
+        <div className="section-title">
+          <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
+          </svg>
+          Best Opportunities Right Now
+        </div>
+        <span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--text-muted)', textTransform: 'uppercase', fontWeight: 600, letterSpacing: '0.08em' }}>Live Signals</span>
+      </div>
+
+      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
+        {ops.map((o, i) => {
+          const rs = rankStyles[i] || rankStyles[2];
+          const hasHistory = o.hist_trades > 0;
+          const histColor = parseFloat(o.hist_wr) >= 80 ? 'var(--green)' : parseFloat(o.hist_wr) >= 50 ? 'var(--orange)' : 'var(--red)';
+
+          return (
+            <div
+              key={o.ticker}
+              className={i === 0 ? 'breathe-neon' : ''}
+              style={{
+                borderRadius: 10,
+                padding: '14px 16px',
+                background: rs.background,
+                border: `1px solid ${rs.borderColor}`,
+                display: 'flex',
+                gap: 14,
+                alignItems: 'flex-start',
+              }}
+            >
+              {/* Rank */}
+              <div
+                style={{
+                  width: 28,
+                  height: 28,
+                  borderRadius: 7,
+                  background: `${rs.rankColor}22`,
+                  border: `1px solid ${rs.borderColor}`,
+                  display: 'flex',
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  flexShrink: 0,
+                  marginTop: 2,
+                }}
+              >
+                <span style={{ fontSize: 11, fontWeight: 800, color: rs.rankColor, fontFamily: 'var(--font-mono)' }}>{rs.rankLabel}</span>
+              </div>
+
+              <div style={{ flex: 1, minWidth: 0 }}>
+                <a
+                  href={o.url}
+                  target="_blank"
+                  rel="noopener noreferrer"
+                  style={{
+                    fontSize: 13,
+                    fontWeight: 600,
+                    color: 'var(--text)',
+                    textDecoration: 'none',
+                    display: 'block',
+                    marginBottom: 8,
+                    overflow: 'hidden',
+                    textOverflow: 'ellipsis',
+                    whiteSpace: 'nowrap',
+                  }}
+                  onMouseEnter={e => { e.currentTarget.style.color = 'var(--cyan)'; }}
+                  onMouseLeave={e => { e.currentTarget.style.color = 'var(--text)'; }}
+                >
+                  {o.title}
+                </a>
+
+                <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 8 }}>
+                  <span
+                    className="badge"
+                    style={o.direction === 'NO'
+                      ? { background: 'var(--red-dim)', color: 'var(--red)', border: '1px solid rgba(255,92,92,0.25)' }
+                      : { background: 'var(--green-dim)', color: 'var(--green)', border: '1px solid rgba(0,195,137,0.25)' }
+                    }
+                  >
+                    BUY {o.direction}
+                  </span>
+
+                  <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
+                    Edge: <span className="mono" style={{ color: 'var(--cyan)', fontWeight: 700 }}>{o.edge_pct}%</span>
+                  </span>
+                  <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
+                    Conf: <span style={{ color: 'var(--blue)', fontWeight: 700 }}>{o.confidence}%</span>
+                  </span>
+                  {o.price_cents > 0 && (
+                    <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
+                      Price: <span className="mono" style={{ color: 'var(--text)' }}>{o.price_cents}c</span>
+                    </span>
+                  )}
+                  {hasHistory && (
+                    <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
+                      History: <span style={{ fontWeight: 700, color: histColor }}>{o.hist_wr}% WR</span>
+                      <span style={{ color: 'var(--text-muted)', marginLeft: 4 }}>({o.hist_trades} trades, {(o.hist_pnl / 100) >= 0 ? '+' : ''}${(o.hist_pnl / 100).toFixed(0)})</span>
+                    </span>
+                  )}
+                  {!hasHistory && <span style={{ fontSize: 11, color: 'var(--text-muted)', fontStyle: 'italic' }}>New market</span>}
+                </div>
+
+                <p style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>{o.why}</p>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
+
+/* ── Strategy Flow Diagram ── */
+function StrategyFlowDiagram({ topMarkets, seedMoney, balance }) {
+  const best = (topMarkets || []).slice(0, 3);
+  if (!best.length) return null;
+
+  const avgWinRate = best.reduce((s, m) => s + m.wins / (m.wins + m.losses), 0) / best.length * 100;
+  const avgEntry = best.length > 0 ? Math.round(best.reduce((s, m) => {
+    const wr = m.wins / (m.wins + m.losses || 1);
+    return s + (wr >= 0.9 ? 95 : 80);
+  }, 0) / best.length) : 95;
+  const seedDollars = seedMoney ? (seedMoney / 100) : 50000;
+  const balDollars = balance ? (balance / 100) : seedDollars;
+  const pnlPct = seedDollars > 0 ? ((balDollars - seedDollars) / seedDollars * 100) : 0;
+
+  const steps = [
+    {
+      icon: (
+        <svg style={{ width: 20, height: 20 }} fill="none" stroke="var(--cyan)" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
+        </svg>
+      ),
+      label: `Start: $${seedDollars.toLocaleString()}`,
+      sub: `$${balDollars.toLocaleString()} (${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%)`,
+      borderColor: 'rgba(0,240,255,0.25)',
+      subColor: pnlPct >= 0 ? 'var(--green)' : 'var(--red)',
+    },
+    {
+      icon: (
+        <svg style={{ width: 20, height: 20 }} fill="none" stroke="var(--blue)" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0" />
+        </svg>
+      ),
+      label: 'Signal Found',
+      sub: 'AI scans 135 markets',
+      borderColor: 'rgba(59,130,246,0.25)',
+      subColor: 'var(--text-muted)',
+    },
+    {
+      icon: (
+        <svg style={{ width: 20, height: 20 }} fill="none" stroke="var(--orange)" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
+        </svg>
+      ),
+      label: `Buy NO at ${avgEntry}c`,
+      sub: 'High-prob event',
+      borderColor: 'rgba(247,147,26,0.25)',
+      subColor: 'var(--text-muted)',
+    },
+    {
+      icon: (
+        <svg style={{ width: 20, height: 20 }} fill="none" stroke="var(--purple)" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
+        </svg>
+      ),
+      label: 'Hold 5min-2hr',
+      sub: 'TP +3c / SL -5c',
+      borderColor: 'rgba(168,85,247,0.25)',
+      subColor: 'var(--text-muted)',
+    },
+    {
+      icon: (
+        <svg style={{ width: 20, height: 20 }} fill="none" stroke="var(--green)" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+        </svg>
+      ),
+      label: `Collect ${100 - avgEntry}c`,
+      sub: `${avgWinRate.toFixed(0)}% of the time`,
+      borderColor: 'rgba(0,195,137,0.25)',
+      subColor: 'var(--text-muted)',
+    },
+  ];
+
+  return (
+    <div className="card" style={{ border: '1px solid rgba(0,195,137,0.12)', background: 'linear-gradient(135deg, rgba(0,195,137,0.04), var(--surface), rgba(247,147,26,0.04))' }}>
+      <div className="section-title" style={{ marginBottom: 16 }}>
+        <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M13 10V3L4 14h7v7l9-11h-7z" />
+        </svg>
+        Winning Strategy — Live Flow
+      </div>
+
+      <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 18 }}>
+        {steps.map((step, i) => (
+          <React.Fragment key={i}>
+            <div
+              style={{
+                flex: 1,
+                borderRadius: 10,
+                padding: '12px 10px',
+                background: 'rgba(255,255,255,0.02)',
+                border: `1px solid ${step.borderColor}`,
+                textAlign: 'center',
+              }}
+            >
+              <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 6 }}>{step.icon}</div>
+              <p style={{ fontSize: 11, fontWeight: 600, color: 'var(--text)', marginBottom: 2 }}>{step.label}</p>
+              <p style={{ fontSize: 10, color: step.subColor }}>{step.sub}</p>
+            </div>
+            {i < steps.length - 1 && (
+              <svg style={{ width: 14, height: 14, color: 'var(--border-light)', flexShrink: 0 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
+              </svg>
+            )}
+          </React.Fragment>
+        ))}
+      </div>
+
+      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,1fr)', gap: 10 }}>
+        {[
+          { label: 'Cost Per Trade', value: `${avgEntry}c`, sub: 'buying NO side', color: 'var(--orange)' },
+          { label: 'Profit on Win', value: `${100 - avgEntry}c`, sub: 'per contract', color: 'var(--green)' },
+          { label: 'Return Multiple', value: `${((100 - avgEntry) / avgEntry * 100).toFixed(0)}%`, sub: 'per winning trade', color: 'var(--green)' },
+        ].map(stat => (
+          <div
+            key={stat.label}
+            style={{
+              borderRadius: 8,
+              padding: '12px',
+              background: 'var(--surface-el)',
+              border: '1px solid var(--border)',
+              textAlign: 'center',
+            }}
+          >
+            <p className="stat-label" style={{ marginBottom: 4 }}>{stat.label}</p>
+            <p className="mono" style={{ fontSize: 20, fontWeight: 700, color: stat.color }}>{stat.value}</p>
+            <p style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>{stat.sub}</p>
+          </div>
+        ))}
+      </div>
+    </div>
+  );
+}
+
+/* ── Next Actions Panel ── */
+function NextActions({ topMarkets, worstMarkets, strategies }) {
+  const addMore = (topMarkets || []).filter(m => m.wins / (m.wins + m.losses || 1) >= 0.90 && m.pnl > 0);
+  const cutLosses = (worstMarkets || []).filter(m => m.pnl < -5000);
+  const disableStrats = (strategies || []).filter(s => s.pnl < -10000 && s.win_rate < 20).slice(0, 3);
+
+  const actions = [];
+  if (addMore.length > 0) {
+    actions.push({ priority: 'HIGH', color: 'green', iconPath: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6', title: `Double down on ${addMore.length} near-certain markets`, detail: addMore.slice(0, 2).map(m => m.title?.split('?')[0] || m.ticker).join(', '), reason: `${addMore.length} markets have 90%+ win rate — the edge is proven` });
+  }
+  if (cutLosses.length > 0) {
+    actions.push({ priority: 'HIGH', color: 'red', iconPath: 'M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636', title: `Exit ${cutLosses.length} bleeding market(s)`, detail: cutLosses.slice(0, 2).map(m => m.title?.split('?')[0] || m.ticker).join(', '), reason: `0% win rate, losing $${Math.abs(cutLosses.reduce((s, m) => s + m.pnl, 0) / 100).toFixed(0)} total` });
+  }
+  if (disableStrats.length > 0) {
+    actions.push({ priority: 'MED', color: 'orange', iconPath: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z', title: `Consider disabling ${disableStrats.length} strategy(ies)`, detail: disableStrats.map(s => s.name).join(', '), reason: 'Sub-20% win rate — these are coin flips minus fees' });
+  }
+  actions.push({ priority: 'INFO', color: 'cyan', iconPath: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', title: 'Keep hyper-trading high-prob NO markets', detail: 'The 95c+ NO strategy is the #1 edge — scale it up', reason: 'Compounding small spreads at 99%+ win rate = consistent returns' });
+
+  const colorMap = {
+    green: { bg: 'var(--green-dim)', border: 'rgba(0,195,137,0.2)', text: 'var(--green)', badge: 'rgba(0,195,137,0.2)' },
+    red: { bg: 'var(--red-dim)', border: 'rgba(255,92,92,0.2)', text: 'var(--red)', badge: 'rgba(255,92,92,0.2)' },
+    orange: { bg: 'var(--orange-dim)', border: 'rgba(247,147,26,0.2)', text: 'var(--orange)', badge: 'rgba(247,147,26,0.2)' },
+    cyan: { bg: 'var(--cyan-dim)', border: 'rgba(0,240,255,0.2)', text: 'var(--cyan)', badge: 'rgba(0,240,255,0.2)' },
+  };
+
+  return (
+    <div className="card" style={{ border: '1px solid rgba(247,147,26,0.12)' }}>
+      <div className="section-title" style={{ marginBottom: 14 }}>
+        <svg style={{ width: 18, height: 18, color: 'var(--orange)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
+        </svg>
+        What To Do Next
+      </div>
+      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
+        {actions.map((a, i) => {
+          const c = colorMap[a.color];
+          return (
+            <div
+              key={i}
+              style={{
+                borderRadius: 10,
+                padding: '12px 14px',
+                background: c.bg,
+                border: `1px solid ${c.border}`,
+                display: 'flex',
+                gap: 12,
+                alignItems: 'flex-start',
+              }}
+            >
+              <div
+                style={{
+                  width: 30,
+                  height: 30,
+                  borderRadius: 8,
+                  background: c.badge,
+                  display: 'flex',
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  flexShrink: 0,
+                  marginTop: 1,
+                }}
+              >
+                <svg style={{ width: 15, height: 15, color: c.text }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d={a.iconPath} />
+                </svg>
+              </div>
+              <div style={{ flex: 1 }}>
+                <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
+                  <span
+                    className="badge"
+                    style={{ background: c.badge, color: c.text, fontSize: 9, padding: '2px 6px' }}
+                  >
+                    {a.priority}
+                  </span>
+                  <span style={{ fontSize: 13, fontWeight: 600, color: c.text }}>{a.title}</span>
+                </div>
+                <p style={{ fontSize: 12, color: 'var(--text-dim)', marginBottom: 2 }}>{a.detail}</p>
+                <p style={{ fontSize: 11, color: 'var(--text-muted)' }}>{a.reason}</p>
+              </div>
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
+
+/* ── Strategy P&L Bar Chart ── */
+function StrategyBars({ strategies }) {
+  const sorted = [...(strategies || [])].sort((a, b) => b.pnl - a.pnl).slice(0, 10);
+  if (!sorted.length) return null;
+  const barData = sorted.map(s => ({
+    name: s.name,
+    pnl: s.pnl / 100,
+    fill: s.pnl >= 0 ? '#00C389' : '#FF5C5C',
+  }));
+
+  return (
+    <div className="card">
+      <div className="section-title" style={{ marginBottom: 14 }}>
+        <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
+        </svg>
+        Strategy P&L — Top 10
+      </div>
+      <ResponsiveContainer width="100%" height={250}>
+        <BarChart data={barData} layout="vertical" margin={{ left: 70, right: 20 }}>
+          <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" horizontal={false} />
+          <XAxis type="number" stroke="var(--text-muted)" tick={{ fill: 'var(--text-muted)', fontSize: 11 }} tickFormatter={v => `$${v}`} />
+          <YAxis type="category" dataKey="name" stroke="var(--text-muted)" tick={{ fill: 'var(--text-muted)', fontSize: 11 }} width={65} />
+          <Tooltip
+            contentStyle={{ background: 'var(--surface-el)', border: '1px solid var(--border)', borderRadius: 8, color: 'var(--text)' }}
+            formatter={v => [`$${v.toFixed(2)}`, 'P&L']}
+          />
+          <Bar dataKey="pnl" radius={[0, 4, 4, 0]}>
+            {barData.map((entry, i) => <Cell key={i} fill={entry.fill} />)}
+          </Bar>
+        </BarChart>
+      </ResponsiveContainer>
+    </div>
+  );
+}
+
+/* ── Win Rate Donut ── */
+function WinRateDonut({ todayWins, todayLosses }) {
+  const total = (todayWins || 0) + (todayLosses || 0);
+  if (!total) return null;
+  const wr = ((todayWins / total) * 100).toFixed(1);
+  const data = [
+    { name: 'Wins', value: todayWins, fill: '#00C389' },
+    { name: 'Losses', value: todayLosses, fill: '#FF5C5C' },
+  ];
+
+  return (
+    <div className="card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
+      <p className="stat-label" style={{ marginBottom: 12 }}>Today's Win Rate</p>
+      <div style={{ position: 'relative', width: 140, height: 140 }}>
+        <ResponsiveContainer width="100%" height="100%">
+          <PieChart>
+            <Pie data={data} innerRadius={45} outerRadius={65} paddingAngle={2} dataKey="value" startAngle={90} endAngle={-270}>
+              {data.map((entry, i) => <Cell key={i} fill={entry.fill} />)}
+            </Pie>
+          </PieChart>
+        </ResponsiveContainer>
+        <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
+          <span className="mono" style={{ fontSize: 22, fontWeight: 700, color: Number(wr) >= 50 ? 'var(--green)' : 'var(--red)' }}>{wr}%</span>
+          <span style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 2 }}>{total.toLocaleString()} trades</span>
+        </div>
+      </div>
+      <div style={{ display: 'flex', gap: 16, marginTop: 10 }}>
+        <span style={{ fontSize: 12, color: 'var(--green)', fontWeight: 600 }}>{todayWins?.toLocaleString()} W</span>
+        <span style={{ fontSize: 12, color: 'var(--red)', fontWeight: 600 }}>{todayLosses?.toLocaleString()} L</span>
+      </div>
+    </div>
+  );
+}
+
+/* ── Custom Tooltip: Daily P&L ── */
+function DailyPnlTooltip({ active, payload, label }) {
+  if (!active || !payload || !payload.length) return null;
+  const d = payload[0]?.payload || {};
+  return (
+    <div style={{
+      background: '#1C1C24',
+      border: '1px solid #2A2A35',
+      borderRadius: 8,
+      padding: '10px 14px',
+      fontFamily: "'Inter', sans-serif",
+      fontSize: 12,
+      minWidth: 160,
+    }}>
+      <p style={{ color: '#6B7280', fontWeight: 600, marginBottom: 6, fontSize: 11 }}>{label}</p>
+      <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+        <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
+          <span style={{ color: '#6B7280' }}>Daily P&L</span>
+          <span className="mono" style={{ color: (d.pnl || 0) >= 0 ? '#00C389' : '#FF5C5C', fontWeight: 700 }}>
+            {(d.pnl || 0) >= 0 ? '+' : ''}${((d.pnl || 0) / 100).toFixed(2)}
+          </span>
+        </div>
+        <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
+          <span style={{ color: '#6B7280' }}>Cumulative</span>
+          <span className="mono" style={{ color: '#00F0FF', fontWeight: 700 }}>
+            {(d.cumulative_pnl || 0) >= 0 ? '+' : ''}${((d.cumulative_pnl || 0) / 100).toFixed(2)}
+          </span>
+        </div>
+        {d.trades != null && (
+          <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
+            <span style={{ color: '#6B7280' }}>Trades</span>
+            <span className="mono" style={{ color: '#E0E0E0' }}>
+              {d.wins || 0}W / {(d.trades || 0) - (d.wins || 0)}L
+            </span>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+/* ── Chart 1: Daily P&L Area Chart ── */
+function DailyPnlChart({ dailyPnl }) {
+  const data = dailyPnl || [];
+
+  if (!data.length) {
+    return (
+      <div className="card" style={{ borderLeft: '3px solid #00F0FF', paddingLeft: 16 }}>
+        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 12 }}>
+          <h2 className="section-title section-accent-cyan" style={{ borderLeft: 'none', paddingLeft: 0 }}>Portfolio Performance</h2>
+        </div>
+        <p style={{ color: '#6B7280', fontSize: 13, textAlign: 'center', padding: '48px 0' }}>
+          No daily P&L data yet — will appear after trading activity is recorded
+        </p>
+      </div>
+    );
+  }
+
+  /* Compute stat pills */
+  const bestDay = data.reduce((best, d) => d.pnl > (best?.pnl || -Infinity) ? d : best, data[0]);
+  const worstDay = data.reduce((worst, d) => d.pnl < (worst?.pnl || Infinity) ? d : worst, data[0]);
+  const latestCumulative = data[data.length - 1]?.cumulative_pnl || 0;
+  const firstCumulative = data[0]?.cumulative_pnl || 0;
+  const totalReturn = latestCumulative - firstCumulative;
+  const isPositive = totalReturn >= 0;
+
+  /* Determine if overall trend is up or down for gradient color */
+  const gradientColor = isPositive ? '#00C389' : '#FF5C5C';
+
+  /* Format data for chart */
+  const chartData = data.map(d => ({
+    ...d,
+    label: fmtDateShort(d.date),
+    cumDisplay: d.cumulative_pnl / 100,
+  }));
+
+  return (
+    <div className="card" style={{ borderLeft: '3px solid #00F0FF', paddingLeft: 20 }}>
+      {/* Header */}
+      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
+        <h2 className="section-title" style={{ fontSize: '1.1rem' }}>Portfolio Performance</h2>
+        {/* Stat pills */}
+        <div style={{ display: 'flex', gap: 10 }}>
+          <div style={{
+            padding: '4px 12px', borderRadius: 20,
+            background: isPositive ? 'rgba(0,195,137,0.12)' : 'rgba(255,92,92,0.12)',
+            border: `1px solid ${isPositive ? 'rgba(0,195,137,0.3)' : 'rgba(255,92,92,0.3)'}`,
+          }}>
+            <span style={{ fontSize: 10, color: '#6B7280', marginRight: 6, fontWeight: 600 }}>30d RETURN</span>
+            <span className="mono" style={{ fontSize: 12, fontWeight: 700, color: isPositive ? '#00C389' : '#FF5C5C' }}>
+              {isPositive ? '+' : ''}${(totalReturn / 100).toFixed(2)}
+            </span>
+          </div>
+          <div style={{
+            padding: '4px 12px', borderRadius: 20,
+            background: 'rgba(0,195,137,0.08)',
+            border: '1px solid rgba(0,195,137,0.2)',
+          }}>
+            <span style={{ fontSize: 10, color: '#6B7280', marginRight: 6, fontWeight: 600 }}>BEST DAY</span>
+            <span className="mono" style={{ fontSize: 12, fontWeight: 700, color: '#00C389' }}>
+              +${((bestDay?.pnl || 0) / 100).toFixed(2)}
+            </span>
+          </div>
+          <div style={{
+            padding: '4px 12px', borderRadius: 20,
+            background: 'rgba(255,92,92,0.08)',
+            border: '1px solid rgba(255,92,92,0.2)',
+          }}>
+            <span style={{ fontSize: 10, color: '#6B7280', marginRight: 6, fontWeight: 600 }}>WORST DAY</span>
+            <span className="mono" style={{ fontSize: 12, fontWeight: 700, color: '#FF5C5C' }}>
+              ${((worstDay?.pnl || 0) / 100).toFixed(2)}
+            </span>
+          </div>
+        </div>
+      </div>
+
+      <ResponsiveContainer width="100%" height={280}>
+        <AreaChart data={chartData} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
+          <defs>
+            <linearGradient id="dailyPnlGradPos" x1="0" y1="0" x2="0" y2="1">
+              <stop offset="5%"  stopColor="#00C389" stopOpacity={0.25} />
+              <stop offset="95%" stopColor="#00C389" stopOpacity={0.01} />
+            </linearGradient>
+            <linearGradient id="dailyPnlGradNeg" x1="0" y1="0" x2="0" y2="1">
+              <stop offset="5%"  stopColor="#FF5C5C" stopOpacity={0.22} />
+              <stop offset="95%" stopColor="#FF5C5C" stopOpacity={0.01} />
+            </linearGradient>
+          </defs>
+          <CartesianGrid
+            strokeDasharray="3 3"
+            stroke="#2A2A35"
+            strokeOpacity={0.6}
+            horizontal={true}
+            vertical={false}
+          />
+          <XAxis
+            dataKey="label"
+            stroke="#2A2A35"
+            tick={{ fill: '#6B7280', fontSize: 11, fontFamily: "'Inter', sans-serif" }}
+            tickLine={false}
+            axisLine={{ stroke: '#2A2A35' }}
+          />
+          <YAxis
+            stroke="#2A2A35"
+            tick={{ fill: '#6B7280', fontSize: 11, fontFamily: "'Inter', sans-serif" }}
+            tickLine={false}
+            axisLine={false}
+            tickFormatter={v => `$${v >= 0 ? '' : ''}${v.toFixed(0)}`}
+            width={55}
+          />
+          <Tooltip content={<DailyPnlTooltip />} />
+          <Area
+            type="monotone"
+            dataKey="cumDisplay"
+            name="Cumulative P&L"
+            stroke={gradientColor}
+            strokeWidth={2}
+            fill={`url(#${isPositive ? 'dailyPnlGradPos' : 'dailyPnlGradNeg'})`}
+            dot={false}
+            activeDot={{
+              r: 5,
+              fill: '#00F0FF',
+              stroke: '#1C1C24',
+              strokeWidth: 2,
+              filter: 'drop-shadow(0 0 6px rgba(0,240,255,0.8))',
+            }}
+          />
+        </AreaChart>
+      </ResponsiveContainer>
+    </div>
+  );
+}
+
+/* ── Custom Tooltip: Strategy Breakdown ── */
+function StrategyTooltip({ active, payload, label }) {
+  if (!active || !payload || !payload.length) return null;
+  const d = payload[0]?.payload || {};
+  return (
+    <div style={{
+      background: '#1C1C24',
+      border: '1px solid #2A2A35',
+      borderRadius: 8,
+      padding: '10px 14px',
+      fontFamily: "'Inter', sans-serif",
+      fontSize: 12,
+      minWidth: 180,
+    }}>
+      <p style={{ color: '#E0E0E0', fontWeight: 600, marginBottom: 6, fontSize: 12 }}>{d.name}</p>
+      <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+        <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
+          <span style={{ color: '#6B7280' }}>P&L</span>
+          <span className="mono" style={{ color: d.pnlRaw >= 0 ? '#00C389' : '#FF5C5C', fontWeight: 700 }}>
+            {d.pnlRaw >= 0 ? '+' : ''}${Math.abs(d.pnlRaw).toFixed(2)}
+          </span>
+        </div>
+        {d.trades != null && (
+          <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
+            <span style={{ color: '#6B7280' }}>Trades</span>
+            <span className="mono" style={{ color: '#E0E0E0' }}>{d.trades}</span>
+          </div>
+        )}
+        {d.win_rate != null && (
+          <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
+            <span style={{ color: '#6B7280' }}>Win Rate</span>
+            <span className="mono" style={{ color: d.win_rate >= 50 ? '#00C389' : '#FF5C5C' }}>{d.win_rate}%</span>
+          </div>
+        )}
+        {d.avg_edge != null && (
+          <div style={{ display: 'flex', justifyContent: 'space-between', gap: 16 }}>
+            <span style={{ color: '#6B7280' }}>Avg Edge</span>
+            <span className="mono" style={{ color: '#00F0FF' }}>{d.avg_edge}%</span>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+/* ── Chart 2: Strategy Performance Horizontal Bar Chart ── */
+function StrategyBreakdownChart({ strategies }) {
+  const sorted = [...(strategies || [])].sort((a, b) => b.pnl - a.pnl).slice(0, 10);
+
+  if (!sorted.length) {
+    return (
+      <div className="card">
+        <h2 className="section-title" style={{ marginBottom: 12, fontSize: '1.1rem' }}>Strategy Breakdown</h2>
+        <p style={{ color: '#6B7280', fontSize: 13, textAlign: 'center', padding: '32px 0' }}>
+          No strategy data yet
+        </p>
+      </div>
+    );
+  }
+
+  const barData = sorted.map(s => ({
+    name: (s.name || s.strategy || 'Unknown').slice(0, 22),
+    pnl: Math.abs(s.pnl / 100),
+    pnlRaw: s.pnl / 100,
+    fill: s.pnl >= 0 ? '#00C389' : '#FF5C5C',
+    trades: s.trades,
+    win_rate: s.win_rate,
+    avg_edge: s.avg_edge,
+  }));
+
+  const dynamicHeight = Math.max(220, barData.length * 36);
+
+  return (
+    <div className="card">
+      <h2 className="section-title" style={{ marginBottom: 16, fontSize: '1.1rem' }}>Strategy Breakdown</h2>
+      <ResponsiveContainer width="100%" height={dynamicHeight}>
+        <BarChart
+          data={barData}
+          layout="vertical"
+          margin={{ left: 10, right: 24, top: 4, bottom: 4 }}
+        >
+          <CartesianGrid
+            strokeDasharray="3 3"
+            stroke="#2A2A35"
+            strokeOpacity={0.6}
+            horizontal={false}
+          />
+          <XAxis
+            type="number"
+            stroke="#2A2A35"
+            tick={{ fill: '#6B7280', fontSize: 11, fontFamily: "'Inter', sans-serif" }}
+            tickLine={false}
+            axisLine={{ stroke: '#2A2A35' }}
+            tickFormatter={v => `$${v.toFixed(0)}`}
+          />
+          <YAxis
+            type="category"
+            dataKey="name"
+            stroke="#2A2A35"
+            tick={{ fill: '#A8A8A8', fontSize: 11, fontFamily: "'Inter', sans-serif" }}
+            tickLine={false}
+            axisLine={false}
+            width={130}
+          />
+          <Tooltip content={<StrategyTooltip />} cursor={{ fill: 'rgba(0,240,255,0.04)' }} />
+          <Bar dataKey="pnl" radius={[0, 4, 4, 0]} maxBarSize={22}>
+            {barData.map((entry, i) => (
+              <Cell key={i} fill={entry.fill} fillOpacity={0.85} />
+            ))}
+          </Bar>
+        </BarChart>
+      </ResponsiveContainer>
+    </div>
+  );
+}
+
+/* ── Chart 3: Win Rate Radial Widget ── */
+function WinRateRadial({ winRate }) {
+  const pct = Math.round((winRate || 0) * 100);
+  const color = pct >= 80 ? '#00C389' : pct >= 50 ? '#F7931A' : '#FF5C5C';
+
+  const radialData = [
+    { name: 'Win Rate', value: pct, fill: color },
+  ];
+
+  return (
+    <div className="card" style={{
+      display: 'flex',
+      flexDirection: 'column',
+      alignItems: 'center',
+      justifyContent: 'center',
+      minHeight: 200,
+    }}>
+      <p className="stat-label" style={{ marginBottom: 8 }}>Overall Win Rate</p>
+      <div style={{ position: 'relative', width: 160, height: 160 }}>
+        <ResponsiveContainer width="100%" height="100%">
+          <RadialBarChart
+            cx="50%"
+            cy="50%"
+            innerRadius="60%"
+            outerRadius="85%"
+            startAngle={90}
+            endAngle={-270}
+            data={radialData}
+          >
+            {/* Background track */}
+            <RadialBar
+              background={{ fill: '#2A2A35' }}
+              dataKey="value"
+              cornerRadius={6}
+              fill={color}
+            />
+          </RadialBarChart>
+        </ResponsiveContainer>
+        {/* Center text overlay */}
+        <div style={{
+          position: 'absolute',
+          inset: 0,
+          display: 'flex',
+          flexDirection: 'column',
+          alignItems: 'center',
+          justifyContent: 'center',
+          pointerEvents: 'none',
+        }}>
+          <span
+            className="mono"
+            style={{
+              fontSize: 28,
+              fontWeight: 700,
+              color,
+              textShadow: `0 0 12px ${color}60`,
+              lineHeight: 1,
+            }}
+          >
+            {pct}%
+          </span>
+          <span style={{ fontSize: 10, color: '#6B7280', marginTop: 4, fontFamily: "'Inter', sans-serif" }}>
+            win rate
+          </span>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/* ══ Strategy Deep Dive Section ══ */
+function StrategyDeepDive() {
+  const exampleTrades = [
+    { id: 1, market: 'NVDA closes above $50 on Mar 2', direction: 'NO @ 95c', entry: '95c', exit: '$1.00', contracts: 200, cost: 190.00, profit: 10.00, roi: '5.26%', holdTime: '47min' },
+    { id: 2, market: 'S&P 500 above 4000 on Mar 2', direction: 'NO @ 96c', entry: '96c', exit: '$1.00', contracts: 150, cost: 144.00, profit: 6.00, roi: '4.17%', holdTime: '1h 12min' },
+    { id: 3, market: 'BTC above $10K on Mar 2', direction: 'NO @ 97c', entry: '97c', exit: '$1.00', contracts: 300, cost: 291.00, profit: 9.00, roi: '3.09%', holdTime: '23min' },
+    { id: 4, market: 'Fed rate cut Mar 2', direction: 'NO @ 94c', entry: '94c', exit: '$1.00', contracts: 250, cost: 235.00, profit: 15.00, roi: '6.38%', holdTime: '1h 45min' },
+    { id: 5, market: 'Tesla below $50 on Mar 2', direction: 'NO @ 93c', entry: '93c', exit: '$1.00', contracts: 100, cost: 93.00, profit: 7.00, roi: '7.53%', holdTime: '55min' },
+  ];
+
+  const insights = [
+    {
+      title: 'Mathematical Edge',
+      icon: 'M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z',
+      color: 'var(--cyan)',
+      borderColor: 'rgba(0,240,255,0.2)',
+      bgColor: 'rgba(0,240,255,0.05)',
+      text: 'Even with occasional losses, a 95% win rate with 5% gains per trade generates massive compound returns. One loss costs 5c, but 19 consecutive wins cover that loss and then some.',
+    },
+    {
+      title: 'Market Inefficiency',
+      icon: 'M13 10V3L4 14h7v7l9-11h-7z',
+      color: 'var(--orange)',
+      borderColor: 'rgba(247,147,26,0.2)',
+      bgColor: 'rgba(247,147,26,0.05)',
+      text: 'Prediction markets still underprice certainty. Events priced at 95% probability should often be 99%+. This gap between market price and true probability is our edge.',
+    },
+    {
+      title: 'AI Advantage',
+      icon: 'M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
+      color: 'var(--green)',
+      borderColor: 'rgba(0,195,137,0.2)',
+      bgColor: 'rgba(0,195,137,0.05)',
+      text: 'Our AI scans 135+ markets in real-time, finding opportunities humans miss. It evaluates probability, liquidity, and time-to-expiry simultaneously across every active contract.',
+    },
+    {
+      title: 'Time Decay Works For Us',
+      icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
+      color: 'var(--purple)',
+      borderColor: 'rgba(168,85,247,0.2)',
+      bgColor: 'rgba(168,85,247,0.05)',
+      text: 'As events approach expiry, NO contracts on events that will not happen converge toward $1.00. Time is our ally. The closer to expiry, the more certain the outcome, the safer the trade.',
+    },
+    {
+      title: 'Scalability',
+      icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6',
+      color: 'var(--blue)',
+      borderColor: 'rgba(59,130,246,0.2)',
+      bgColor: 'rgba(59,130,246,0.05)',
+      text: 'Strategy works whether trading $100 or $100,000. Kalshi liquidity is sufficient for large positions on high-volume markets. Scale capital, not complexity.',
+    },
+  ];
+
+  const flowSteps = [
+    { label: 'AI Scans 135 Markets', color: 'var(--cyan)', bgColor: 'rgba(0,240,255,0.08)' },
+    { label: 'Finds High-Prob NO', color: 'var(--orange)', bgColor: 'rgba(247,147,26,0.08)' },
+    { label: 'Buy at 95c', color: 'var(--green)', bgColor: 'rgba(0,195,137,0.08)' },
+    { label: 'Hold 5min-2hr', color: 'var(--purple)', bgColor: 'rgba(168,85,247,0.08)' },
+    { label: 'Collect 5c Profit', color: 'var(--green)', bgColor: 'rgba(0,195,137,0.12)' },
+  ];
+
+  return (
+    <div className="strategy-deep-dive">
+      {/* Section Header */}
+      <div className="sdd-header">
+        <div className="sdd-header-line" />
+        <div className="sdd-header-content">
+          <svg style={{ width: 22, height: 22, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
+          </svg>
+          <h2 className="heading" style={{ fontSize: 22, fontWeight: 800, color: 'var(--text)' }}>Strategy Deep Dive</h2>
+          <svg style={{ width: 22, height: 22, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
+          </svg>
+        </div>
+        <div className="sdd-header-line" />
+      </div>
+
+      {/* Section 1: How It Works */}
+      <div className="card sdd-section-card" style={{ border: '1px solid rgba(0,240,255,0.15)', background: 'linear-gradient(135deg, rgba(0,240,255,0.03), var(--surface), rgba(0,195,137,0.03))' }}>
+        <div className="section-title" style={{ marginBottom: 20 }}>
+          <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M13 10V3L4 14h7v7l9-11h-7z" />
+          </svg>
+          Winning Strategy — How It Works
+        </div>
+
+        {/* Key Points Grid */}
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 12, marginBottom: 24 }}>
+          {[
+            { label: 'The Edge', value: 'Buy NO at 95c', sub: 'on near-certain events', color: 'var(--cyan)', glow: true },
+            { label: 'The Math', value: '95c in, $1 out', sub: '= 5c profit per contract', color: 'var(--green)', glow: false },
+            { label: 'Per-Trade Return', value: '5.26%', sub: 'per winning trade', color: 'var(--green)', glow: true },
+            { label: 'Risk Mgmt', value: 'TP +3c / SL -5c', sub: 'early exit if favorable', color: 'var(--orange)', glow: false },
+            { label: 'Win Rate', value: '95%+', sub: 'on high-prob events', color: 'var(--cyan)', glow: true },
+          ].map(stat => (
+            <div
+              key={stat.label}
+              className={stat.glow ? 'sdd-stat-glow' : ''}
+              style={{
+                borderRadius: 10,
+                padding: '16px 12px',
+                background: 'var(--surface-el)',
+                border: `1px solid ${stat.color}33`,
+                textAlign: 'center',
+              }}
+            >
+              <p className="stat-label" style={{ marginBottom: 6 }}>{stat.label}</p>
+              <p className="mono" style={{ fontSize: 16, fontWeight: 700, color: stat.color, lineHeight: 1.2 }}>{stat.value}</p>
+              <p style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 4 }}>{stat.sub}</p>
+            </div>
+          ))}
+        </div>
+
+        {/* Flow Diagram */}
+        <div style={{ marginBottom: 16 }}>
+          <p className="stat-label" style={{ marginBottom: 12, textAlign: 'center' }}>Trade Flow</p>
+          <div className="sdd-flow-row">
+            {flowSteps.map((step, i) => (
+              <React.Fragment key={i}>
+                <div
+                  className="sdd-flow-step"
+                  style={{
+                    background: step.bgColor,
+                    border: `1px solid ${step.color}33`,
+                  }}
+                >
+                  <span className="mono" style={{ fontSize: 12, fontWeight: 700, color: step.color, textAlign: 'center' }}>
+                    {step.label}
+                  </span>
+                </div>
+                {i < flowSteps.length - 1 && (
+                  <div className="sdd-flow-arrow">
+                    <svg style={{ width: 16, height: 16, color: 'var(--border-light)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2.5" d="M9 5l7 7-7 7" />
+                    </svg>
+                  </div>
+                )}
+              </React.Fragment>
+            ))}
+          </div>
+        </div>
+
+        {/* Compound Effect Callout */}
+        <div
+          style={{
+            borderRadius: 10,
+            padding: '14px 18px',
+            background: 'linear-gradient(90deg, rgba(0,195,137,0.08), rgba(0,240,255,0.06))',
+            border: '1px solid rgba(0,195,137,0.18)',
+            display: 'flex',
+            alignItems: 'center',
+            gap: 14,
+          }}
+        >
+          <div style={{
+            width: 36, height: 36, borderRadius: 10,
+            background: 'rgba(0,195,137,0.15)',
+            display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
+          }}>
+            <svg style={{ width: 18, height: 18, color: 'var(--green)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
+            </svg>
+          </div>
+          <div>
+            <p style={{ fontSize: 13, fontWeight: 600, color: 'var(--green)', marginBottom: 2 }}>Compound Effect</p>
+            <p style={{ fontSize: 12, color: 'var(--text-dim)', lineHeight: 1.5 }}>
+              5% per trade x multiple trades per day = exponential growth. At just 3 trades/day with 95% win rate,
+              capital compounds at roughly 14% per week. This is how $60K becomes $221K.
+            </p>
+          </div>
+        </div>
+      </div>
+
+      {/* Section 2: Example Trades */}
+      <div className="card sdd-section-card" style={{ border: '1px solid rgba(247,147,26,0.15)', background: 'linear-gradient(135deg, rgba(247,147,26,0.03), var(--surface))' }}>
+        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
+          <div className="section-title">
+            <svg style={{ width: 18, height: 18, color: 'var(--orange)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
+            </svg>
+            Example Trades
+          </div>
+          <span className="badge" style={{ background: 'var(--orange-dim)', color: 'var(--orange)', fontSize: 10 }}>ILLUSTRATIVE</span>
+        </div>
+
+        <div style={{ overflowX: 'auto' }}>
+          <table className="sdd-trades-table">
+            <thead>
+              <tr>
+                <th style={{ width: 32 }}>#</th>
+                <th style={{ textAlign: 'left' }}>Market</th>
+                <th>Direction</th>
+                <th style={{ textAlign: 'right' }}>Entry</th>
+                <th style={{ textAlign: 'right' }}>Exit</th>
+                <th style={{ textAlign: 'right' }}>Contracts</th>
+                <th style={{ textAlign: 'right' }}>Cost</th>
+                <th style={{ textAlign: 'right' }}>Profit</th>
+                <th style={{ textAlign: 'right' }}>ROI</th>
+                <th style={{ textAlign: 'right' }}>Hold Time</th>
+              </tr>
+            </thead>
+            <tbody>
+              {exampleTrades.map(t => (
+                <tr key={t.id}>
+                  <td style={{ textAlign: 'center' }}>
+                    <span className="mono" style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t.id}</span>
+                  </td>
+                  <td style={{ textAlign: 'left', maxWidth: 240 }}>
+                    <span style={{ fontSize: 13, color: 'var(--text)', fontWeight: 500 }}>{t.market}</span>
+                  </td>
+                  <td style={{ textAlign: 'center' }}>
+                    <span className="badge" style={{ background: 'var(--red-dim)', color: 'var(--red)', fontSize: 10, border: '1px solid rgba(255,92,92,0.25)' }}>
+                      {t.direction}
+                    </span>
+                  </td>
+                  <td style={{ textAlign: 'right' }}>
+                    <span className="mono" style={{ fontSize: 13, color: 'var(--text-dim)' }}>{t.entry}</span>
+                  </td>
+                  <td style={{ textAlign: 'right' }}>
+                    <span className="mono" style={{ fontSize: 13, color: 'var(--green)' }}>{t.exit}</span>
+                  </td>
+                  <td style={{ textAlign: 'right' }}>
+                    <span className="mono" style={{ fontSize: 13, color: 'var(--text)' }}>{t.contracts}</span>
+                  </td>
+                  <td style={{ textAlign: 'right' }}>
+                    <span className="mono" style={{ fontSize: 13, color: 'var(--orange)' }}>${t.cost.toFixed(2)}</span>
+                  </td>
+                  <td style={{ textAlign: 'right' }}>
+                    <span className="mono" style={{ fontSize: 13, fontWeight: 700, color: 'var(--green)' }}>+${t.profit.toFixed(2)}</span>
+                  </td>
+                  <td style={{ textAlign: 'right' }}>
+                    <span className="mono" style={{ fontSize: 12, color: 'var(--cyan)', fontWeight: 600 }}>{t.roi}</span>
+                  </td>
+                  <td style={{ textAlign: 'right' }}>
+                    <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t.holdTime}</span>
+                  </td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+
+        {/* Summary Row */}
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 12, marginTop: 16, paddingTop: 14, borderTop: '1px solid var(--border)' }}>
+          {[
+            { label: 'Total Cost', value: '$953.00', color: 'var(--orange)' },
+            { label: 'Total Profit', value: '+$47.00', color: 'var(--green)' },
+            { label: 'Avg ROI', value: '5.29%', color: 'var(--cyan)' },
+            { label: 'Avg Hold', value: '52min', color: 'var(--purple)' },
+          ].map(s => (
+            <div key={s.label} style={{ textAlign: 'center' }}>
+              <p className="stat-label" style={{ marginBottom: 4 }}>{s.label}</p>
+              <p className="mono" style={{ fontSize: 18, fontWeight: 700, color: s.color }}>{s.value}</p>
+            </div>
+          ))}
+        </div>
+      </div>
+
+      {/* Section 3: Why This Works Long-Term */}
+      <div className="card sdd-section-card" style={{ border: '1px solid rgba(0,195,137,0.15)', background: 'linear-gradient(135deg, rgba(0,195,137,0.03), var(--surface), rgba(168,85,247,0.03))' }}>
+        <div className="section-title" style={{ marginBottom: 20 }}>
+          <svg style={{ width: 18, height: 18, color: 'var(--green)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
+          </svg>
+          Why This Works Long-Term
+        </div>
+
+        <div className="sdd-insights-grid">
+          {insights.map((ins, i) => (
+            <div
+              key={i}
+              className="sdd-insight-card"
+              style={{
+                border: `1px solid ${ins.borderColor}`,
+                background: ins.bgColor,
+              }}
+            >
+              <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
+                <div style={{
+                  width: 32, height: 32, borderRadius: 8,
+                  background: `${ins.color}1A`,
+                  display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
+                }}>
+                  <svg style={{ width: 16, height: 16, color: ins.color }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d={ins.icon} />
+                  </svg>
+                </div>
+                <h4 style={{ fontSize: 14, fontWeight: 700, color: ins.color, fontFamily: 'var(--font-heading)' }}>{ins.title}</h4>
+              </div>
+              <p style={{ fontSize: 12, color: 'var(--text-dim)', lineHeight: 1.6 }}>{ins.text}</p>
+            </div>
+          ))}
+        </div>
+
+        {/* Bottom CTA */}
+        <div
+          style={{
+            marginTop: 20,
+            borderRadius: 10,
+            padding: '16px 20px',
+            background: 'linear-gradient(90deg, rgba(0,240,255,0.06), rgba(247,147,26,0.06))',
+            border: '1px solid rgba(0,240,255,0.12)',
+            textAlign: 'center',
+          }}
+        >
+          <p style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)', marginBottom: 4 }}>
+            The Bottom Line
+          </p>
+          <p style={{ fontSize: 13, color: 'var(--text-dim)', lineHeight: 1.6, maxWidth: 700, margin: '0 auto' }}>
+            This is not gambling. It is systematic, AI-driven arbitrage on prediction market inefficiencies.
+            Small, consistent profits compounded over hundreds of trades per week create exponential portfolio growth.
+            The math is on our side, and the AI never sleeps.
+          </p>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+/* ══ Animated stat card inner component (uses hooks at top level) ══ */
+function AnimatedStatCard({ label, rawValue, displayValue, color, accent, icon, sparkColor, hero }) {
+  const animated = useAnimatedValue(typeof rawValue === 'number' ? rawValue : 0);
+  const sparkData = SPARK_DATA[label] || SPARK_DATA['Balance'];
+
+  // For numeric stat cards, show animated value; for formatted strings fall back to displayValue
+  const shownValue = typeof rawValue === 'number' && rawValue !== 0
+    ? (label === 'Balance' || label === 'Today P&L'
+        ? `$${(animated / 100).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
+        : Math.round(animated).toLocaleString())
+    : displayValue;
+
+  return (
+    <div className={`card ${accent}${hero ? ' border-neon-animated' : ''}`} style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
+      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
+        <span className="stat-label">{label}</span>
+        <div
+          style={{
+            width: 28,
+            height: 28,
+            borderRadius: 7,
+            background: `${color}1A`,
+            display: 'flex',
+            alignItems: 'center',
+            justifyContent: 'center',
+          }}
+        >
+          <svg style={{ width: 14, height: 14, color }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d={icon} />
+          </svg>
+        </div>
+      </div>
+      <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 8 }}>
+        <p className="stat-v mono" style={{ color }}>{shownValue}</p>
+        <Sparkline
+          data={sparkData}
+          color={sparkColor || color}
+          width={60}
+          height={22}
+          id={label.replace(/\s+/g, '-').toLowerCase()}
+        />
+      </div>
+    </div>
+  );
+}
+
+/* ══ Main Dashboard ══ */
+/* ── Collapsible Section Wrapper ── */
+function CollapsibleSection({ title, defaultOpen = true, children }) {
+  const key = `ken-sec-${title.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
+  const [open, setOpen] = useState(() => {
+    try { return localStorage.getItem(key) !== 'false'; } catch { return defaultOpen; }
+  });
+  const toggle = () => setOpen(v => {
+    const next = !v;
+    try { localStorage.setItem(key, String(next)); } catch {}
+    return next;
+  });
+  return (
+    <div>
+      <button
+        onClick={toggle}
+        style={{
+          display: 'flex', alignItems: 'center', gap: 8,
+          padding: '2px 0 8px', width: '100%',
+          background: 'transparent', border: 'none', cursor: 'pointer',
+        }}
+        onMouseEnter={e => { e.currentTarget.style.opacity = '0.7'; }}
+        onMouseLeave={e => { e.currentTarget.style.opacity = '1'; }}
+      >
+        <svg
+          style={{
+            width: 12, height: 12, color: 'var(--text-muted)', flexShrink: 0,
+            transition: 'transform 220ms ease',
+            transform: open ? 'rotate(0deg)' : 'rotate(-90deg)',
+          }}
+          fill="none" stroke="currentColor" viewBox="0 0 24 24"
+        >
+          <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
+        </svg>
+        <span style={{
+          fontSize: 10, fontWeight: 700, fontFamily: 'var(--font-body)',
+          color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.1em',
+        }}>
+          {title}
+        </span>
+      </button>
+      <div style={{
+        maxHeight: open ? '3500px' : 0,
+        overflow: 'hidden',
+        opacity: open ? 1 : 0,
+        transition: 'max-height 0.4s cubic-bezier(0.4,0,0.2,1), opacity 0.3s ease',
+      }}>
+        {children}
+      </div>
+    </div>
+  );
+}
+
+export default function Dashboard() {
+  const [data, setData] = useState(null);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    loadDashboard();
+    const i = setInterval(loadDashboard, 30000);
+    return () => clearInterval(i);
+  }, []);
+
+  async function loadDashboard() {
+    try {
+      const r = await api('/dashboard');
+      setData(r);
+    } catch {}
+    setLoading(false);
+  }
+
+  if (loading) {
+    return (
+      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: 256 }}>
+        <div
+          className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
+          style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+        />
+      </div>
+    );
+  }
+
+  const d = data || {};
+  const pnlData = (d.pnl_history || []).map((p, i) => ({ day: i + 1, pnl: p }));
+  const todayWinRate = d.today_trades > 0 ? (d.today_wins / d.today_trades * 100).toFixed(1) : '0';
+  const topStrats = (d.strategies || []).filter(s => s.pnl > 0).slice(0, 5);
+  const worstStrats = (d.strategies || []).filter(s => s.pnl < 0).slice(-5).reverse();
+
+  const statCards = [
+    {
+      label: 'Balance',
+      rawValue: d.balance || 0,
+      displayValue: d.balance ? `$${(d.balance / 100).toLocaleString(undefined, { minimumFractionDigits: 2 })}` : '—',
+      color: 'var(--green)',
+      sparkColor: 'var(--green)',
+      accent: 'card-accent-green',
+      icon: 'M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z',
+    },
+    {
+      label: 'Positions',
+      rawValue: d.position_count || 0,
+      displayValue: d.position_count ?? '—',
+      color: 'var(--orange)',
+      sparkColor: 'var(--orange)',
+      accent: 'card-accent-orange',
+      icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
+    },
+    {
+      label: 'Open Markets',
+      rawValue: d.open_orders || 0,
+      displayValue: d.open_orders ?? '—',
+      color: 'var(--blue)',
+      sparkColor: 'var(--cyan)',
+      accent: 'card-accent-blue',
+      icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6',
+    },
+    {
+      label: 'Today P&L',
+      rawValue: d.daily_pnl || 0,
+      displayValue: d.daily_pnl ? `$${(d.daily_pnl / 100).toLocaleString(undefined, { minimumFractionDigits: 2 })}` : '$0.00',
+      color: (d.daily_pnl || 0) >= 0 ? 'var(--green)' : 'var(--red)',
+      sparkColor: (d.daily_pnl || 0) >= 0 ? 'var(--green)' : 'var(--red)',
+      accent: (d.daily_pnl || 0) >= 0 ? 'card-accent-green' : 'card-accent-red',
+      icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
+    },
+    {
+      label: 'Markets Tracked',
+      rawValue: d.markets_tracked || 0,
+      displayValue: d.markets_tracked ?? '—',
+      color: 'var(--purple)',
+      sparkColor: 'var(--purple)',
+      accent: 'card-accent-purple',
+      icon: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9',
+    },
+  ];
+
+  return (
+    <div className="fade-in" style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
+
+      {/* ── Page Header ── */}
+      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
+        <div>
+          <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }}>
+            <h1 className="heading" style={{ fontSize: 26, fontWeight: 800, color: 'var(--text)', letterSpacing: '-0.025em' }}>
+              Dashboard
+            </h1>
+            <span className="live-indicator">
+              <span className="live-dot" />
+              LIVE
+            </span>
+          </div>
+          <p className="section-subtitle">Ken event contracts overview</p>
+        </div>
+        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          <span
+            className="badge"
+            style={d.env === 'prod'
+              ? { background: 'var(--green-dim)', color: 'var(--green)' }
+              : { background: 'var(--orange-dim)', color: 'var(--orange)' }
+            }
+          >
+            {(d.env || 'DEMO').toUpperCase()}
+          </span>
+          {d.safe_mode && (
+            <span className="badge" style={{ background: 'var(--orange-dim)', color: 'var(--orange)' }}>
+              SAFE MODE
+            </span>
+          )}
+        </div>
+      </div>
+
+      {/* ── 5 Stat Cards with Sparklines + Animated Values ── */}
+      <CollapsibleSection title="Overview">
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(5,1fr)', gap: 14 }}>
+          {statCards.map((s, i) => (
+            <AnimatedStatCard key={s.label} {...s} hero={i === 0} />
+          ))}
+        </div>
+      </CollapsibleSection>
+
+      {/* ── Chart 1: Daily P&L Area Chart (Portfolio Performance) ── */}
+      <CollapsibleSection title="Portfolio Performance">
+        <DailyPnlChart dailyPnl={d.daily_pnl_history || d.dailyPnl || []} />
+      </CollapsibleSection>
+
+      {/* ── Charts 2+3: Strategy Breakdown + Win Rate Radial ── */}
+      <CollapsibleSection title="Strategy Breakdown">
+        <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 16 }}>
+          <StrategyBreakdownChart strategies={d.strategies} />
+          <WinRateRadial winRate={d.win_rate || (d.today_trades > 0 ? d.today_wins / d.today_trades : 0)} />
+        </div>
+      </CollapsibleSection>
+
+      {/* ── Capital & Economics ── */}
+      {d.seed_money > 0 && (
+        <CollapsibleSection title="Capital & Economics">
+        <div
+          className="card"
+          style={{
+            border: '1px solid rgba(59,130,246,0.18)',
+            background: 'linear-gradient(135deg, rgba(59,130,246,0.04), var(--surface), rgba(0,195,137,0.04))',
+          }}
+        >
+          <div className="section-title" style={{ marginBottom: 16 }}>
+            <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
+            </svg>
+            Capital & Economics
+          </div>
+
+          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6,1fr)', gap: 12, textAlign: 'center' }}>
+            {[
+              { label: 'Seed Money', value: `$${(d.seed_money / 100).toLocaleString(undefined, { minimumFractionDigits: 0 })}`, sub: `${d.num_portfolios} portfolios`, color: 'var(--blue)' },
+              { label: 'Current Balance', value: `$${(d.balance / 100).toLocaleString(undefined, { minimumFractionDigits: 0 })}`, sub: 'all portfolios', color: 'var(--green)' },
+              { label: 'Net P&L', value: `${d.net_pnl_from_seed >= 0 ? '+' : ''}$${(d.net_pnl_from_seed / 100).toLocaleString(undefined, { minimumFractionDigits: 0 })}`, sub: `${d.roi_pct >= 0 ? '+' : ''}${d.roi_pct}% ROI`, color: d.net_pnl_from_seed >= 0 ? 'var(--green)' : 'var(--red)', subColor: d.roi_pct >= 0 ? 'var(--green)' : 'var(--red)' },
+              { label: 'Capital Deployed', value: `$${(d.total_invested / 100).toLocaleString(undefined, { minimumFractionDigits: 0 })}`, sub: 'total traded', color: 'var(--orange)' },
+              { label: 'Capital at Risk', value: `$${(d.capital_at_risk / 100).toLocaleString(undefined, { minimumFractionDigits: 0 })}`, sub: 'open positions', color: 'var(--red)' },
+              { label: 'Daily Budget', value: `$${(d.daily_budget / 100).toLocaleString(undefined, { minimumFractionDigits: 0 })}`, sub: 'across strategies', color: 'var(--purple)' },
+            ].map(stat => (
+              <div key={stat.label}>
+                <p className="stat-label" style={{ marginBottom: 6 }}>{stat.label}</p>
+                <p className="mono" style={{ fontSize: 18, fontWeight: 700, color: stat.color }}>{stat.value}</p>
+                <p style={{ fontSize: 10, color: stat.subColor || 'var(--text-muted)', marginTop: 2 }}>{stat.sub}</p>
+              </div>
+            ))}
+          </div>
+
+          {/* Daily Breakdown — 7-day grid */}
+          {d.daily_economics?.length > 0 && (
+            <div style={{ marginTop: 18, paddingTop: 14, borderTop: '1px solid var(--border)' }}>
+              <p className="stat-label" style={{ marginBottom: 10 }}>Daily Breakdown (Last 7 Days)</p>
+              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 8 }}>
+                {(d.daily_economics || []).slice(0, 7).reverse().map((e, i) => {
+                  const dayLabel = new Date(e.day).toLocaleDateString('en-US', { weekday: 'short', month: 'numeric', day: 'numeric', timeZone: 'America/Los_Angeles' });
+                  const isPositive = e.net_pnl >= 0;
+                  return (
+                    <div
+                      key={i}
+                      style={{
+                        borderRadius: 8,
+                        padding: '10px 8px',
+                        background: isPositive ? 'rgba(0,195,137,0.06)' : 'rgba(255,92,92,0.06)',
+                        border: `1px solid ${isPositive ? 'rgba(0,195,137,0.18)' : 'rgba(255,92,92,0.18)'}`,
+                        textAlign: 'center',
+                      }}
+                    >
+                      <p style={{ fontSize: 10, color: 'var(--text-muted)', fontWeight: 600, marginBottom: 4 }}>{dayLabel}</p>
+                      <p className="mono" style={{ fontSize: 13, fontWeight: 700, color: isPositive ? 'var(--green)' : 'var(--red)', marginBottom: 3 }}>
+                        {isPositive ? '+' : ''}${(e.net_pnl / 100).toFixed(0)}
+                      </p>
+                      <p style={{ fontSize: 10, color: 'var(--text-muted)', marginBottom: 2 }}>{e.trades} trades</p>
+                      <p style={{ fontSize: 10, color: 'var(--text-muted)' }}>
+                        <span style={{ color: 'var(--green)' }}>{e.wins}W</span>
+                        {' / '}
+                        <span style={{ color: 'var(--red)' }}>{e.losses}L</span>
+                      </p>
+                    </div>
+                  );
+                })}
+              </div>
+            </div>
+          )}
+        </div>
+        </CollapsibleSection>
+      )}
+
+      {/* ── Best Opportunities ── */}
+      <CollapsibleSection title="Best Opportunities">
+        <TopOpportunities opportunities={d.top_opportunities} />
+      </CollapsibleSection>
+
+      {/* ── Today's Lessons ── */}
+      <CollapsibleSection title="Today's Performance">
+      <div
+        className="card"
+        style={{
+          border: '1px solid rgba(247,147,26,0.18)',
+          background: 'linear-gradient(135deg, rgba(247,147,26,0.04), var(--surface))',
+        }}
+      >
+        <div style={{ display: 'flex', alignItems: 'center', marginBottom: 14 }}>
+          <div className="section-title">
+            <svg style={{ width: 18, height: 18, color: 'var(--orange)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
+            </svg>
+            Today's Lessons
+          </div>
+          <span style={{ marginLeft: 'auto', fontSize: 12, color: 'var(--text-muted)' }}>
+            {new Date().toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })}
+          </span>
+        </div>
+
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', gap: 16, textAlign: 'center' }}>
+          {[
+            { label: 'Trades Resolved', value: (d.today_trades || 0).toLocaleString(), color: 'var(--orange)' },
+            {
+              label: 'Wins / Losses',
+              value: (
+                <span>
+                  <span style={{ color: 'var(--green)' }}>{d.today_wins || 0}</span>
+                  <span style={{ color: 'var(--border-light)', margin: '0 4px' }}>/</span>
+                  <span style={{ color: 'var(--red)' }}>{d.today_losses || 0}</span>
+                </span>
+              ),
+              color: null,
+            },
+            { label: 'Win Rate', value: `${todayWinRate}%`, color: Number(todayWinRate) >= 50 ? 'var(--green)' : 'var(--red)' },
+            {
+              label: 'Day P&L',
+              value: `${(d.daily_pnl || 0) >= 0 ? '+' : ''}$${((d.daily_pnl || 0) / 100).toLocaleString(undefined, { minimumFractionDigits: 2 })}`,
+              color: (d.daily_pnl || 0) >= 0 ? 'var(--green)' : 'var(--red)',
+            },
+          ].map((s, i) => (
+            <div key={i}>
+              <p className="stat-label" style={{ marginBottom: 6 }}>{s.label}</p>
+              {typeof s.value === 'string' ? (
+                <p className="mono" style={{ fontSize: 22, fontWeight: 700, color: s.color }}>{s.value}</p>
+              ) : (
+                <p className="mono" style={{ fontSize: 22, fontWeight: 700 }}>{s.value}</p>
+              )}
+            </div>
+          ))}
+        </div>
+      </div>
+      </CollapsibleSection>
+
+      {/* ── Strategy Flow + Win Rate Donut ── */}
+      <CollapsibleSection title="Strategy Flow">
+        <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: 16 }}>
+          <StrategyFlowDiagram topMarkets={d.top_markets} seedMoney={d.seed_money} balance={d.balance} />
+          <WinRateDonut todayWins={d.today_wins} todayLosses={d.today_losses} />
+        </div>
+      </CollapsibleSection>
+
+      {/* ── What To Do Next ── */}
+      <CollapsibleSection title="Action Plan">
+        <NextActions topMarkets={d.top_markets} worstMarkets={d.worst_markets} strategies={d.strategies} />
+      </CollapsibleSection>
+
+      {/* ── Top Markets Table ── */}
+      {(d.top_markets || []).length > 0 && (
+        <CollapsibleSection title="Top Markets">
+        <div className="card">
+          <div className="section-title" style={{ marginBottom: 14 }}>
+            <svg style={{ width: 18, height: 18, color: 'var(--green)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M5 3l14 9-14 9V3z" />
+            </svg>
+            Top Markets — Money Makers
+          </div>
+          <div style={{ overflowX: 'auto' }}>
+            <table>
+              <thead>
+                <tr>
+                  <th style={{ textAlign: 'left' }}>Market</th>
+                  <th style={{ textAlign: 'right' }}>Trades</th>
+                  <th style={{ textAlign: 'right' }}>Wins</th>
+                  <th style={{ textAlign: 'right' }}>P&L</th>
+                  <th>Edge</th>
+                  <th>Signal</th>
+                </tr>
+              </thead>
+              <tbody>
+                {(d.top_markets || []).map((m, i) => (
+                  <tr key={i}>
+                    <td style={{ maxWidth: 300 }}>
+                      <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
+                        <Link
+                          to={`/positions?market=${encodeURIComponent(m.ticker)}`}
+                          style={{
+                            fontSize: 13,
+                            color: 'var(--text)',
+                            textDecoration: 'none',
+                            display: 'block',
+                            overflow: 'hidden',
+                            textOverflow: 'ellipsis',
+                            whiteSpace: 'nowrap',
+                            transition: 'color 150ms',
+                          }}
+                          onMouseEnter={e => { e.currentTarget.style.color = 'var(--cyan)'; }}
+                          onMouseLeave={e => { e.currentTarget.style.color = 'var(--text)'; }}
+                        >
+                          {m.title}
+                        </Link>
+                        <a
+                          href={`https://kalshi.com/search?query=${encodeURIComponent(m.title?.split('?')[0] || m.ticker)}`}
+                          target="_blank"
+                          rel="noopener noreferrer"
+                          style={{ color: 'var(--text-muted)', flexShrink: 0, textDecoration: 'none', fontSize: 13 }}
+                          onMouseEnter={e => { e.currentTarget.style.color = 'var(--cyan)'; }}
+                          onMouseLeave={e => { e.currentTarget.style.color = 'var(--text-muted)'; }}
+                          title="View on Kalshi"
+                        >
+                          ↗
+                        </a>
+                      </div>
+                      <span className="mono" style={{ fontSize: 10, color: 'var(--text-muted)' }}>{m.ticker}</span>
+                    </td>
+                    <td style={{ textAlign: 'right' }} className="mono"><span style={{ fontSize: 13, color: 'var(--text-dim)' }}>{m.trades}</span></td>
+                    <td style={{ textAlign: 'right', fontSize: 13 }}><WinRate wins={m.wins} losses={m.losses} /></td>
+                    <td style={{ textAlign: 'right', fontSize: 13 }}><PnlBadge cents={m.pnl} /></td>
+                    <td style={{ fontSize: 12, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>{m.edge}</td>
+                    <td style={{ textAlign: 'center' }}><HoldsIndicator wins={m.wins} losses={m.losses} pnl={m.pnl} /></td>
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+          </div>
+        </div>
+        </CollapsibleSection>
+      )}
+
+      {/* ── Worst Markets Table ── */}
+      {(d.worst_markets || []).length > 0 && (
+        <CollapsibleSection title="Bleeding Markets">
+        <div className="card" style={{ border: '1px solid rgba(255,92,92,0.12)' }}>
+          <div className="section-title" style={{ marginBottom: 14, color: 'var(--red)' }}>
+            <svg style={{ width: 18, height: 18 }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
+            </svg>
+            Bleeding Markets — Consider Exiting
+          </div>
+          <div style={{ overflowX: 'auto' }}>
+            <table>
+              <thead>
+                <tr>
+                  <th style={{ textAlign: 'left' }}>Market</th>
+                  <th style={{ textAlign: 'right' }}>Trades</th>
+                  <th style={{ textAlign: 'right' }}>Wins</th>
+                  <th style={{ textAlign: 'right' }}>P&L</th>
+                  <th>Edge</th>
+                  <th>Signal</th>
+                </tr>
+              </thead>
+              <tbody>
+                {(d.worst_markets || []).map((m, i) => (
+                  <tr key={i}>
+                    <td style={{ maxWidth: 300 }}>
+                      <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
+                        <Link
+                          to={`/positions?market=${encodeURIComponent(m.ticker)}`}
+                          style={{ fontSize: 13, color: 'var(--text)', textDecoration: 'none', display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', transition: 'color 150ms' }}
+                          onMouseEnter={e => { e.currentTarget.style.color = 'var(--red)'; }}
+                          onMouseLeave={e => { e.currentTarget.style.color = 'var(--text)'; }}
+                        >
+                          {m.title}
+                        </Link>
+                        <a
+                          href={`https://kalshi.com/search?query=${encodeURIComponent(m.title?.split('?')[0] || m.ticker)}`}
+                          target="_blank"
+                          rel="noopener noreferrer"
+                          style={{ color: 'var(--text-muted)', flexShrink: 0, textDecoration: 'none', fontSize: 13 }}
+                          onMouseEnter={e => { e.currentTarget.style.color = 'var(--red)'; }}
+                          onMouseLeave={e => { e.currentTarget.style.color = 'var(--text-muted)'; }}
+                          title="View on Kalshi"
+                        >
+                          ↗
+                        </a>
+                      </div>
+                      <span className="mono" style={{ fontSize: 10, color: 'var(--text-muted)' }}>{m.ticker}</span>
+                    </td>
+                    <td style={{ textAlign: 'right' }} className="mono"><span style={{ fontSize: 13, color: 'var(--text-dim)' }}>{m.trades}</span></td>
+                    <td style={{ textAlign: 'right', fontSize: 13 }}><WinRate wins={m.wins} losses={m.losses} /></td>
+                    <td style={{ textAlign: 'right', fontSize: 13 }}><PnlBadge cents={m.pnl} /></td>
+                    <td style={{ fontSize: 12, color: 'var(--text-muted)', whiteSpace: 'nowrap' }}>{m.edge}</td>
+                    <td style={{ textAlign: 'center' }}><HoldsIndicator wins={m.wins} losses={m.losses} pnl={m.pnl} /></td>
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+          </div>
+        </div>
+        </CollapsibleSection>
+      )}
+
+      {/* ── P&L History Chart ── */}
+      {pnlData.length > 0 && (
+        <CollapsibleSection title="P&L History">
+        <div className="card">
+          <div className="section-title" style={{ marginBottom: 14 }}>
+            <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M7 12l3-3 3 3 4-4M8 21l4-4 4 4M3 4h18M4 4h16v12a1 1 0 01-1 1H5a1 1 0 01-1-1V4z" />
+            </svg>
+            P&L History (Daily)
+          </div>
+          <ResponsiveContainer width="100%" height={200}>
+            <AreaChart data={pnlData}>
+              <defs>
+                <linearGradient id="pnlGrad" x1="0" y1="0" x2="0" y2="1">
+                  <stop offset="5%" stopColor="#00F0FF" stopOpacity={0.2} />
+                  <stop offset="95%" stopColor="#00F0FF" stopOpacity={0} />
+                </linearGradient>
+              </defs>
+              <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
+              <XAxis dataKey="day" stroke="var(--text-muted)" tick={{ fill: 'var(--text-muted)', fontSize: 11 }} />
+              <YAxis stroke="var(--text-muted)" tick={{ fill: 'var(--text-muted)', fontSize: 11 }} tickFormatter={v => `$${v}`} />
+              <Tooltip
+                contentStyle={{ background: 'var(--surface-el)', border: '1px solid var(--border)', borderRadius: 8, color: 'var(--text)' }}
+              />
+              <Area type="monotone" dataKey="pnl" stroke="var(--cyan)" strokeWidth={2} fill="url(#pnlGrad)" />
+            </AreaChart>
+          </ResponsiveContainer>
+        </div>
+        </CollapsibleSection>
+      )}
+
+      {/* ── Strategy P&L Bar Chart ── */}
+      <CollapsibleSection title="Strategy P&L">
+        <StrategyBars strategies={d.strategies} />
+      </CollapsibleSection>
+
+      {/* ── Strategy Leaderboard ── */}
+      <CollapsibleSection title="Strategy Leaderboard">
+      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
+        {/* Top Strategies */}
+        <div className="card">
+          <div className="section-title" style={{ marginBottom: 12 }}>
+            <svg style={{ width: 18, height: 18, color: 'var(--green)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M5 3l14 9-14 9V3z" />
+            </svg>
+            Top Strategies
+          </div>
+          <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+            {topStrats.map((s, i) => (
+              <div
+                key={i}
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  justifyContent: 'space-between',
+                  padding: '8px 0',
+                  borderBottom: i < topStrats.length - 1 ? '1px solid var(--border)' : 'none',
+                }}
+              >
+                <div>
+                  <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text)' }}>{s.name}</span>
+                  <span style={{ marginLeft: 8, fontSize: 10, color: 'var(--text-muted)' }}>{s.strategy}</span>
+                </div>
+                <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
+                  <span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{s.win_rate}% WR</span>
+                  <PnlBadge cents={s.pnl} />
+                </div>
+              </div>
+            ))}
+            {topStrats.length === 0 && (
+              <p style={{ color: 'var(--text-muted)', fontSize: 13, textAlign: 'center', padding: '16px 0' }}>No profitable strategies yet</p>
+            )}
+          </div>
+        </div>
+
+        {/* Worst Strategies */}
+        <div className="card">
+          <div className="section-title" style={{ marginBottom: 12 }}>
+            <svg style={{ width: 18, height: 18, color: 'var(--red)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M13 17h8m0 0V9m0 8l-8-8-4 4-6-6" />
+            </svg>
+            Losing Strategies
+          </div>
+          <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+            {worstStrats.map((s, i) => (
+              <div
+                key={i}
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  justifyContent: 'space-between',
+                  padding: '8px 0',
+                  borderBottom: i < worstStrats.length - 1 ? '1px solid var(--border)' : 'none',
+                }}
+              >
+                <div>
+                  <span style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-dim)' }}>{s.name}</span>
+                  <span style={{ marginLeft: 8, fontSize: 10, color: 'var(--text-muted)' }}>{s.strategy}</span>
+                </div>
+                <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
+                  <span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{s.win_rate}% WR</span>
+                  <PnlBadge cents={s.pnl} />
+                </div>
+              </div>
+            ))}
+            {worstStrats.length === 0 && (
+              <p style={{ color: 'var(--text-muted)', fontSize: 13, textAlign: 'center', padding: '16px 0' }}>No losing strategies</p>
+            )}
+          </div>
+        </div>
+      </div>
+      </CollapsibleSection>
+
+      {/* ── Strategy Deep Dive ── */}
+      <CollapsibleSection title="Strategy Deep Dive" defaultOpen={false}>
+        <StrategyDeepDive />
+      </CollapsibleSection>
+
+      {/* ── Recent Fills + Quick Actions ── */}
+      <CollapsibleSection title="Recent Activity">
+      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
+        {/* Recent Fills */}
+        <div className="card">
+          <div className="section-title" style={{ marginBottom: 12 }}>
+            <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
+            </svg>
+            Recent Fills
+          </div>
+          {(d.recent_fills || []).length > 0 ? (
+            <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
+              {(d.recent_fills || []).slice(0, 5).map((f, i) => (
+                <div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: 13 }}>
+                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+                    <span className="mono" style={{ fontSize: 11, color: 'var(--text-muted)' }}>{f.ticker}</span>
+                    <span
+                      className="badge"
+                      style={f.side === 'yes'
+                        ? { background: 'var(--green-dim)', color: 'var(--green)' }
+                        : { background: 'var(--red-dim)', color: 'var(--red)' }
+                      }
+                    >
+                      {f.side?.toUpperCase()}
+                    </span>
+                  </div>
+                  <span className="mono" style={{ color: 'var(--text-dim)', fontSize: 12 }}>{f.count}@{f.yes_price}c</span>
+                </div>
+              ))}
+            </div>
+          ) : (
+            <p style={{ color: 'var(--text-muted)', fontSize: 13, textAlign: 'center', padding: '24px 0' }}>No recent fills</p>
+          )}
+        </div>
+
+        {/* Quick Actions */}
+        <div className="card">
+          <div className="section-title" style={{ marginBottom: 12 }}>
+            <svg style={{ width: 18, height: 18, color: 'var(--cyan)' }} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.8" d="M13 10V3L4 14h7v7l9-11h-7z" />
+            </svg>
+            Quick Actions
+          </div>
+          <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
+            {[
+              { to: '/markets', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6', iconColor: 'var(--orange)', label: 'Browse Markets', sub: 'Find weather contracts' },
+              { to: '/positions', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z', iconColor: 'var(--green)', label: 'View Positions', sub: 'Current portfolio' },
+              { to: '/settings', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z', iconColor: 'var(--blue)', label: 'Scan Config', sub: 'Intervals & risk params' },
+            ].map(action => (
+              <Link
+                key={action.to}
+                to={action.to}
+                style={{
+                  display: 'flex',
+                  alignItems: 'center',
+                  gap: 12,
+                  padding: '10px 12px',
+                  borderRadius: 8,
+                  background: 'var(--surface-el)',
+                  border: '1px solid var(--border)',
+                  textDecoration: 'none',
+                  transition: 'all 150ms ease',
+                }}
+                onMouseEnter={e => { e.currentTarget.style.borderColor = 'var(--cyan)'; e.currentTarget.style.background = 'rgba(0,240,255,0.04)'; }}
+                onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.background = 'var(--surface-el)'; }}
+              >
+                <div
+                  style={{
+                    width: 32,
+                    height: 32,
+                    borderRadius: 8,
+                    background: `${action.iconColor}1A`,
+                    display: 'flex',
+                    alignItems: 'center',
+                    justifyContent: 'center',
+                    flexShrink: 0,
+                  }}
+                >
+                  <svg style={{ width: 15, height: 15, color: action.iconColor }} fill="none" stroke="currentColor" strokeWidth="1.8" viewBox="0 0 24 24">
+                    <path strokeLinecap="round" strokeLinejoin="round" d={action.icon} />
+                  </svg>
+                </div>
+                <div>
+                  <p style={{ fontSize: 13, fontWeight: 500, color: 'var(--text)', marginBottom: 1 }}>{action.label}</p>
+                  <p style={{ fontSize: 11, color: 'var(--text-muted)' }}>{action.sub}</p>
+                </div>
+              </Link>
+            ))}
+          </div>
+        </div>
+      </div>
+      </CollapsibleSection>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Heatmap.jsx b/kalshi-dash/src/pages/Heatmap.jsx
new file mode 100644
index 0000000..40ab247
--- /dev/null
+++ b/kalshi-dash/src/pages/Heatmap.jsx
@@ -0,0 +1,986 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { api } from '../api';
+import RingGauge from '../components/RingGauge';
+import {
+  PieChart, Pie, Cell, Tooltip as RechartsTooltip, ResponsiveContainer,
+  BarChart, Bar, XAxis, YAxis, CartesianGrid,
+} from 'recharts';
+import { COLOR_CYCLE } from '../utils/chartTheme';
+
+/* ── Chart 4: Category P&L Donut ── */
+function CategoryDonutChart({ categories }) {
+  const entries = Object.entries(categories || {});
+  if (!entries.length) return null;
+
+  const pieData = entries
+    .map(([cat, markets], i) => {
+      const arr = Array.isArray(markets) ? markets : [];
+      const rawPnl = arr.reduce((s, m) => s + (m.pnl || 0), 0);
+      return {
+        name: cat,
+        value: Math.max(Math.abs(rawPnl), 1),
+        rawPnl,
+        color: COLOR_CYCLE[i % COLOR_CYCLE.length],
+      };
+    })
+    .filter(d => Math.abs(d.rawPnl) > 0)
+    .sort((a, b) => b.value - a.value);
+
+  const totalPnl = entries.reduce((s, [, markets]) => {
+    const arr = Array.isArray(markets) ? markets : [];
+    return s + arr.reduce((ss, m) => ss + (m.pnl || 0), 0);
+  }, 0);
+
+  const CustomTooltip = ({ active, payload }) => {
+    if (!active || !payload?.length) return null;
+    const d = payload[0]?.payload || {};
+    return (
+      <div style={{
+        background: '#1C1C24', border: '1px solid #2A2A35', borderRadius: 8,
+        padding: '8px 12px', fontSize: 12, fontFamily: "'Inter', sans-serif",
+      }}>
+        <p style={{ color: d.color, fontWeight: 700, marginBottom: 4 }}>{d.name}</p>
+        <p className="mono" style={{ color: d.rawPnl >= 0 ? '#00C389' : '#FF5C5C', fontWeight: 700 }}>
+          {d.rawPnl >= 0 ? '+' : ''}${(d.rawPnl / 100).toFixed(2)}
+        </p>
+      </div>
+    );
+  };
+
+  const CustomLegend = () => (
+    <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px 14px', justifyContent: 'center', marginTop: 8 }}>
+      {pieData.map(d => (
+        <div key={d.name} style={{ display: 'flex', alignItems: 'center', gap: 5 }}>
+          <div style={{ width: 8, height: 8, borderRadius: '50%', background: d.color, flexShrink: 0 }} />
+          <span style={{ fontSize: 11, color: '#A8A8A8', fontFamily: "'Inter', sans-serif" }}>{d.name}</span>
+        </div>
+      ))}
+    </div>
+  );
+
+  return (
+    <div className="card" style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+      <h3 className="section-title" style={{ fontSize: '1rem', marginBottom: 4, alignSelf: 'flex-start' }}>
+        P&L by Category
+      </h3>
+      <div style={{ position: 'relative', width: '100%', height: 220 }}>
+        <ResponsiveContainer width="100%" height="100%">
+          <PieChart>
+            <defs>
+              {pieData.map(d => (
+                <radialGradient key={d.name} id={`donut-grad-${d.name}`} cx="50%" cy="50%" r="50%">
+                  <stop offset="0%" stopColor={d.color} stopOpacity={0.9} />
+                  <stop offset="100%" stopColor={d.color} stopOpacity={0.6} />
+                </radialGradient>
+              ))}
+            </defs>
+            <Pie
+              data={pieData}
+              cx="50%"
+              cy="50%"
+              innerRadius={65}
+              outerRadius={95}
+              paddingAngle={2}
+              dataKey="value"
+              startAngle={90}
+              endAngle={-270}
+            >
+              {pieData.map((entry, i) => (
+                <Cell key={entry.name} fill={entry.color} fillOpacity={0.85} stroke="transparent" />
+              ))}
+            </Pie>
+            <RechartsTooltip content={<CustomTooltip />} />
+          </PieChart>
+        </ResponsiveContainer>
+        {/* Center total P&L */}
+        <div style={{
+          position: 'absolute', inset: 0,
+          display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
+          pointerEvents: 'none',
+        }}>
+          <span style={{ fontSize: 10, color: '#6B7280', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.06em' }}>
+            Total P&L
+          </span>
+          <span className="mono" style={{
+            fontSize: 20, fontWeight: 700, lineHeight: 1.2,
+            color: totalPnl >= 0 ? '#00C389' : '#FF5C5C',
+            textShadow: `0 0 10px ${totalPnl >= 0 ? 'rgba(0,195,137,0.4)' : 'rgba(255,92,92,0.4)'}`,
+          }}>
+            {totalPnl >= 0 ? '+' : ''}${Math.abs(totalPnl / 100).toFixed(0)}
+          </span>
+        </div>
+      </div>
+      <CustomLegend />
+    </div>
+  );
+}
+
+/* ── Chart 5: Category Win/Loss Stacked Bar ── */
+function CategoryWinLossChart({ categories }) {
+  const entries = Object.entries(categories || {});
+  if (!entries.length) return null;
+
+  const barData = entries
+    .map(([cat, markets]) => {
+      const arr = Array.isArray(markets) ? markets : [];
+      const wins = arr.reduce((s, m) => s + (m.wins || 0), 0);
+      const losses = arr.reduce((s, m) => s + (m.losses || 0), 0);
+      const total = wins + losses;
+      const wr = total > 0 ? ((wins / total) * 100).toFixed(1) : 0;
+      return { name: cat, wins, losses, total, wr: Number(wr) };
+    })
+    .filter(d => d.total > 0)
+    .sort((a, b) => b.total - a.total);
+
+  const CustomTooltip = ({ active, payload, label }) => {
+    if (!active || !payload?.length) return null;
+    const d = payload[0]?.payload || {};
+    return (
+      <div style={{
+        background: '#1C1C24', border: '1px solid #2A2A35', borderRadius: 8,
+        padding: '10px 14px', fontSize: 12, fontFamily: "'Inter', sans-serif", minWidth: 150,
+      }}>
+        <p style={{ color: '#E0E0E0', fontWeight: 700, marginBottom: 6 }}>{label}</p>
+        <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
+          <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
+            <span style={{ color: '#6B7280' }}>Wins</span>
+            <span className="mono" style={{ color: '#00C389', fontWeight: 700 }}>{d.wins}</span>
+          </div>
+          <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
+            <span style={{ color: '#6B7280' }}>Losses</span>
+            <span className="mono" style={{ color: '#FF5C5C', fontWeight: 700 }}>{d.losses}</span>
+          </div>
+          <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
+            <span style={{ color: '#6B7280' }}>Win Rate</span>
+            <span className="mono" style={{ color: d.wr >= 50 ? '#00C389' : '#FF5C5C', fontWeight: 700 }}>{d.wr}%</span>
+          </div>
+        </div>
+      </div>
+    );
+  };
+
+  return (
+    <div className="card">
+      <h3 className="section-title" style={{ fontSize: '1rem', marginBottom: 16 }}>
+        Category Win / Loss
+      </h3>
+      <ResponsiveContainer width="100%" height={220}>
+        <BarChart data={barData} margin={{ top: 4, right: 16, left: 0, bottom: 4 }}>
+          <CartesianGrid
+            strokeDasharray="3 3"
+            stroke="#2A2A35"
+            strokeOpacity={0.6}
+            vertical={false}
+          />
+          <XAxis
+            dataKey="name"
+            stroke="#2A2A35"
+            tick={{ fill: '#6B7280', fontSize: 10, fontFamily: "'Inter', sans-serif" }}
+            tickLine={false}
+            axisLine={{ stroke: '#2A2A35' }}
+          />
+          <YAxis
+            stroke="#2A2A35"
+            tick={{ fill: '#6B7280', fontSize: 11, fontFamily: "'Inter', sans-serif" }}
+            tickLine={false}
+            axisLine={false}
+            allowDecimals={false}
+          />
+          <RechartsTooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(0,240,255,0.04)' }} />
+          <Bar dataKey="wins" stackId="a" fill="#00C389" fillOpacity={0.85} radius={[0, 0, 0, 0]} maxBarSize={40} name="Wins" />
+          <Bar dataKey="losses" stackId="a" fill="#FF5C5C" fillOpacity={0.85} radius={[4, 4, 0, 0]} maxBarSize={40} name="Losses" />
+        </BarChart>
+      </ResponsiveContainer>
+      {/* Legend */}
+      <div style={{ display: 'flex', justifyContent: 'center', gap: 20, marginTop: 8 }}>
+        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+          <div style={{ width: 10, height: 10, borderRadius: 2, background: '#00C389' }} />
+          <span style={{ fontSize: 11, color: '#A8A8A8' }}>Wins</span>
+        </div>
+        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
+          <div style={{ width: 10, height: 10, borderRadius: 2, background: '#FF5C5C' }} />
+          <span style={{ fontSize: 11, color: '#A8A8A8' }}>Losses</span>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+// ── Market Category Classification ──
+const CATEGORY_MAP = {
+  // Politics
+  'trump': 'Politics', 'president': 'Politics', 'impeach': 'Politics', 'congress': 'Politics',
+  'senate': 'Politics', 'house': 'Politics', 'governor': 'Politics', 'election': 'Politics',
+  'democrat': 'Politics', 'republican': 'Politics', 'vote': 'Politics', 'biden': 'Politics',
+  'vance': 'Politics', 'cabinet': 'Politics', 'scotus': 'Politics', 'supreme': 'Politics',
+  'pardon': 'Politics', 'approval': 'Politics', 'poll': 'Politics',
+  'secretary': 'Politics', 'defense': 'Politics', 'attorney': 'Politics',
+  // Economics
+  'gdp': 'Economy', 'inflation': 'Economy', 'cpi': 'Economy', 'fed': 'Economy',
+  'interest rate': 'Economy', 'unemployment': 'Economy', 'jobs': 'Economy', 'payroll': 'Economy',
+  'recession': 'Economy', 'stock': 'Economy', 'sp500': 'Economy', 's&p': 'Economy',
+  'nasdaq': 'Economy', 'dow': 'Economy', 'treasury': 'Economy', 'bond': 'Economy',
+  'debt': 'Economy', 'trade': 'Economy', 'tariff': 'Economy', 'deficit': 'Economy',
+  // Weather
+  'temperature': 'Weather', 'rain': 'Weather', 'snow': 'Weather', 'hurricane': 'Weather',
+  'weather': 'Weather', 'tornado': 'Weather', 'flood': 'Weather', 'heat': 'Weather',
+  'cold': 'Weather', 'storm': 'Weather', 'wind': 'Weather', 'precipitation': 'Weather',
+  'climate': 'Weather',
+  // Crypto
+  'bitcoin': 'Crypto', 'btc': 'Crypto', 'ethereum': 'Crypto', 'eth': 'Crypto',
+  'crypto': 'Crypto', 'token': 'Crypto', 'blockchain': 'Crypto', 'defi': 'Crypto',
+  // Sports
+  'nfl': 'Sports', 'nba': 'Sports', 'mlb': 'Sports', 'nhl': 'Sports',
+  'soccer': 'Sports', 'tennis': 'Sports', 'super bowl': 'Sports', 'championship': 'Sports',
+  'game': 'Sports', 'match': 'Sports', 'playoff': 'Sports', 'world series': 'Sports',
+  // Tech
+  'ai': 'Tech', 'spacex': 'Tech', 'tesla': 'Tech', 'apple': 'Tech',
+  'google': 'Tech', 'meta': 'Tech', 'openai': 'Tech', 'chatgpt': 'Tech',
+  'tech': 'Tech', 'rocket': 'Tech', 'launch': 'Tech',
+  // Culture
+  'oscar': 'Culture', 'movie': 'Culture', 'tv': 'Culture', 'celebrity': 'Culture',
+  'grammy': 'Culture', 'award': 'Culture', 'music': 'Culture', 'entertainment': 'Culture',
+  // Energy
+  'oil': 'Energy', 'gas': 'Energy', 'energy': 'Energy', 'solar': 'Energy',
+  'opec': 'Energy', 'crude': 'Energy', 'coal': 'Energy', 'renewable': 'Energy',
+};
+
+const CATEGORY_COLORS = {
+  'Politics':  { bg: 'var(--blue)',   glow: 'rgba(59,130,246,0.3)',   hex: '#3B82F6' },
+  'Economy':   { bg: 'var(--green)',  glow: 'rgba(0,195,137,0.3)',    hex: '#00C389' },
+  'Weather':   { bg: 'var(--cyan)',   glow: 'rgba(0,240,255,0.3)',    hex: '#00F0FF' },
+  'Crypto':    { bg: 'var(--orange)', glow: 'rgba(247,147,26,0.3)',   hex: '#F7931A' },
+  'Sports':    { bg: 'var(--purple)', glow: 'rgba(168,85,247,0.3)',   hex: '#A855F7' },
+  'Tech':      { bg: 'var(--cyan)',   glow: 'rgba(0,240,255,0.3)',    hex: '#00F0FF' },
+  'Culture':   { bg: 'var(--orange)', glow: 'rgba(247,147,26,0.3)',  hex: '#F7931A' },
+  'Energy':    { bg: 'var(--orange)', glow: 'rgba(247,147,26,0.3)',  hex: '#F7931A' },
+  'Other':     { bg: 'var(--text-muted)', glow: 'rgba(107,114,128,0.3)', hex: '#6B7280' },
+};
+
+function classifyMarket(title) {
+  const lower = (title || '').toLowerCase();
+  for (const [keyword, category] of Object.entries(CATEGORY_MAP)) {
+    if (lower.includes(keyword)) return category;
+  }
+  return 'Other';
+}
+
+function getPnlColor(pnl) {
+  if (pnl > 50000) return 'var(--green)';
+  if (pnl > 10000) return '#4ade80';
+  if (pnl > 1000)  return '#86efac';
+  if (pnl > 0)     return '#bbf7d0';
+  if (pnl === 0)   return 'var(--text-muted)';
+  if (pnl > -1000) return '#fecaca';
+  if (pnl > -10000) return '#f87171';
+  return 'var(--red)';
+}
+
+function getPnlBg(pnl) {
+  if (pnl > 50000)  return 'rgba(0,195,137,0.35)';
+  if (pnl > 10000)  return 'rgba(0,195,137,0.25)';
+  if (pnl > 1000)   return 'rgba(0,195,137,0.15)';
+  if (pnl > 0)      return 'rgba(0,195,137,0.08)';
+  if (pnl === 0)    return 'rgba(107,114,128,0.15)';
+  if (pnl > -1000)  return 'rgba(255,92,92,0.08)';
+  if (pnl > -10000) return 'rgba(255,92,92,0.15)';
+  return 'rgba(255,92,92,0.30)';
+}
+
+// ── Treemap Layout Algorithm ──
+function squarify(items, x, y, w, h) {
+  if (!items.length) return [];
+  if (items.length === 1) {
+    return [{ ...items[0], x, y, w, h }];
+  }
+
+  const total = items.reduce((s, i) => s + i.size, 0);
+  if (total === 0) return items.map(i => ({ ...i, x, y, w: 0, h: 0 }));
+
+  // Split into two groups using golden ratio
+  let sum = 0;
+  let splitIdx = 0;
+  const target = total * 0.5;
+  for (let i = 0; i < items.length; i++) {
+    sum += items[i].size;
+    if (sum >= target) { splitIdx = i + 1; break; }
+  }
+  if (splitIdx === 0) splitIdx = 1;
+  if (splitIdx >= items.length) splitIdx = items.length - 1;
+
+  const left = items.slice(0, splitIdx);
+  const right = items.slice(splitIdx);
+  const leftSize = left.reduce((s, i) => s + i.size, 0);
+  const ratio = leftSize / total;
+
+  if (w >= h) {
+    const leftW = w * ratio;
+    return [
+      ...squarify(left, x, y, leftW, h),
+      ...squarify(right, x + leftW, y, w - leftW, h),
+    ];
+  } else {
+    const leftH = h * ratio;
+    return [
+      ...squarify(left, x, y, w, leftH),
+      ...squarify(right, x, y + leftH, w, h - leftH),
+    ];
+  }
+}
+
+// ── Individual Market Tile ──
+function MarketTile({ item, onClick, isSelected }) {
+  const pnlDollars = (item.pnl / 100).toFixed(2);
+  const winRate = item.trades > 0 ? ((item.wins / item.trades) * 100).toFixed(0) : '0';
+  const isSmall = item.w < 120 || item.h < 80;
+  const isTiny = item.w < 80 || item.h < 50;
+
+  return (
+    <div
+      onClick={() => onClick(item)}
+      className="absolute cursor-pointer overflow-hidden group"
+      style={{
+        left: item.x,
+        top: item.y,
+        width: item.w,
+        height: item.h,
+        backgroundColor: getPnlBg(item.pnl),
+        border: isSelected
+          ? '2px solid var(--cyan)'
+          : '1px solid var(--border)',
+        borderRadius: 6,
+        zIndex: isSelected ? 10 : 1,
+        transition: 'all 150ms ease',
+        boxShadow: isSelected ? '0 0 16px rgba(0,240,255,0.4)' : undefined,
+      }}
+    >
+      {/* Hover glow */}
+      <div
+        className="absolute inset-0 opacity-0 group-hover:opacity-100 pointer-events-none"
+        style={{
+          background: `radial-gradient(circle at center, ${getPnlBg(item.pnl).replace(')', ', transparent)').replace('rgba', 'radial-gradient').includes('radial') ? getPnlBg(item.pnl) : getPnlBg(item.pnl)}, transparent)`,
+          boxShadow: `inset 0 0 12px ${item.pnl >= 0 ? 'rgba(0,195,137,0.3)' : 'rgba(255,92,92,0.3)'}`,
+          transition: 'opacity 200ms ease',
+        }}
+      />
+
+      <div className="relative h-full p-1.5 flex flex-col justify-between z-10">
+        {!isTiny && (
+          <>
+            <div className="truncate text-[10px] font-medium leading-tight" style={{ color: 'var(--text-dim)' }}>
+              {item.shortTitle}
+            </div>
+            <div className="flex items-end justify-between mt-auto">
+              <span className="mono font-bold text-xs" style={{ color: getPnlColor(item.pnl) }}>
+                {item.pnl >= 0 ? '+' : ''}{pnlDollars > 999 ? `$${(item.pnl / 100 / 1000).toFixed(1)}k` : `$${pnlDollars}`}
+              </span>
+              {!isSmall && (
+                <span className="text-[9px] font-bold mono" style={{
+                  color: Number(winRate) >= 80 ? 'var(--green)' : Number(winRate) >= 50 ? 'var(--orange)' : 'var(--red)',
+                }}>
+                  {winRate}%
+                </span>
+              )}
+            </div>
+          </>
+        )}
+        {isTiny && (
+          <div className="flex items-center justify-center h-full">
+            <span className="mono font-bold text-[10px]" style={{ color: getPnlColor(item.pnl) }}>
+              {item.pnl >= 0 ? '+' : ''}{(item.pnl / 100).toFixed(0)}
+            </span>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+// ── Category Header Tile ──
+function CategoryBlock({ category, items, x, y, w, h, onTileClick, selectedTicker }) {
+  const catColor = CATEGORY_COLORS[category] || CATEGORY_COLORS.Other;
+  const totalPnl = items.reduce((s, i) => s + i.pnl, 0);
+  const totalTrades = items.reduce((s, i) => s + i.trades, 0);
+  const headerH = Math.min(28, h * 0.15);
+  const innerItems = squarify(items, 0, 0, w, h - headerH);
+
+  return (
+    <div
+      className="absolute overflow-hidden"
+      style={{
+        left: x,
+        top: y,
+        width: w,
+        height: h,
+        borderRadius: 8,
+        border: `1px solid ${catColor.hex}40`,
+      }}
+    >
+      {/* Category Header */}
+      <div
+        className="flex items-center justify-between px-2 relative z-20"
+        style={{
+          height: headerH,
+          background: `linear-gradient(135deg, ${catColor.hex}25, ${catColor.hex}0D)`,
+          borderBottom: `1px solid ${catColor.hex}40`,
+        }}
+      >
+        <span className="text-xs font-bold tracking-wide heading" style={{ color: catColor.hex }}>
+          {category}
+        </span>
+        <span className="text-[10px] mono" style={{ color: totalPnl >= 0 ? 'var(--green)' : 'var(--red)' }}>
+          {totalPnl >= 0 ? '+' : ''}${(totalPnl / 100).toFixed(0)} | {totalTrades} trades
+        </span>
+      </div>
+
+      {/* Market Tiles */}
+      <div className="relative" style={{ height: h - headerH }}>
+        {innerItems.map(tile => (
+          <MarketTile
+            key={tile.ticker}
+            item={{ ...tile, y: tile.y }}
+            onClick={onTileClick}
+            isSelected={selectedTicker === tile.ticker}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+// ── Market Detail Panel ──
+function DetailPanel({ market, onClose }) {
+  if (!market) return null;
+  const pnlDollars = (market.pnl / 100).toFixed(2);
+  const winRate = market.trades > 0 ? ((market.wins / market.trades) * 100).toFixed(1) : '0';
+  const catColor = CATEGORY_COLORS[market.category] || CATEGORY_COLORS.Other;
+
+  return (
+    <div className="card-glow fade-in">
+      <div className="flex items-start justify-between mb-4">
+        <div className="flex-1 min-w-0 mr-4">
+          <h3 className="heading text-sm font-bold truncate" style={{ color: 'var(--text)' }}>{market.title}</h3>
+          <p className="mono text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{market.ticker}</p>
+        </div>
+        <button
+          onClick={onClose}
+          className="btn btn-ghost text-lg leading-none"
+          style={{ padding: '0 6px' }}
+        >
+          &times;
+        </button>
+      </div>
+
+      <div className="grid grid-cols-4 gap-3 text-center">
+        {[
+          {
+            label: 'P&L',
+            value: `${market.pnl >= 0 ? '+' : ''}$${pnlDollars}`,
+            color: market.pnl >= 0 ? 'var(--green)' : 'var(--red)',
+          },
+          {
+            label: 'Win Rate',
+            value: `${winRate}%`,
+            color: Number(winRate) >= 80 ? 'var(--green)' : Number(winRate) >= 50 ? 'var(--orange)' : 'var(--red)',
+          },
+          {
+            label: 'Trades',
+            value: market.trades.toLocaleString(),
+            color: 'var(--orange)',
+          },
+          {
+            label: 'Record',
+            valueEl: (
+              <p className="text-lg font-bold">
+                <span style={{ color: 'var(--green)' }}>{market.wins}</span>
+                <span style={{ color: 'var(--border-light)' }} className="mx-0.5">-</span>
+                <span style={{ color: 'var(--red)' }}>{market.losses}</span>
+              </p>
+            ),
+          },
+        ].map(s => (
+          <div
+            key={s.label}
+            className="rounded-lg p-2.5 text-center"
+            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+          >
+            <p className="stat-label mb-1">{s.label}</p>
+            {s.valueEl || (
+              <p className="stat-v text-lg" style={{ color: s.color }}>{s.value}</p>
+            )}
+          </div>
+        ))}
+      </div>
+
+      <div className="mt-3 flex items-center justify-between text-xs">
+        <span style={{ color: 'var(--text-muted)' }}>
+          Category:{' '}
+          <span style={{ color: catColor.hex }} className="font-semibold">{market.category}</span>
+        </span>
+        {market.url && (
+          <a
+            href={market.url}
+            target="_blank"
+            rel="noopener noreferrer"
+            className="text-cyan transition-colors"
+            style={{ color: 'var(--cyan)' }}
+          >
+            View on Kalshi &rarr;
+          </a>
+        )}
+      </div>
+
+      {market.edge && (
+        <div
+          className="mt-2 text-[11px] rounded-lg px-3 py-2"
+          style={{
+            color: 'var(--text-dim)',
+            background: 'var(--surface-el)',
+            border: '1px solid var(--border)',
+          }}
+        >
+          {market.edge}
+        </div>
+      )}
+    </div>
+  );
+}
+
+// ── Legend ──
+function Legend() {
+  const pnlRanges = [
+    { label: '> $500',   bg: 'rgba(0,195,137,0.35)' },
+    { label: '$100-500', bg: 'rgba(0,195,137,0.25)' },
+    { label: '$10-100',  bg: 'rgba(0,195,137,0.15)' },
+    { label: '$0-10',    bg: 'rgba(0,195,137,0.08)' },
+    { label: '$0',       bg: 'rgba(107,114,128,0.15)' },
+    { label: '-$10',     bg: 'rgba(255,92,92,0.08)' },
+    { label: '-$100',    bg: 'rgba(255,92,92,0.15)' },
+    { label: '< -$100',  bg: 'rgba(255,92,92,0.30)' },
+  ];
+
+  return (
+    <div className="flex items-center gap-2 flex-wrap">
+      <span className="stat-label mr-1">P&L Scale:</span>
+      {pnlRanges.map(r => (
+        <div key={r.label} className="flex items-center gap-1">
+          <div
+            className="w-3 h-3 rounded-sm"
+            style={{ backgroundColor: r.bg, border: '1px solid var(--border)' }}
+          />
+          <span className="text-[9px]" style={{ color: 'var(--text-muted)' }}>{r.label}</span>
+        </div>
+      ))}
+    </div>
+  );
+}
+
+// ── Main Heatmap Page ──
+export default function Heatmap() {
+  const [data, setData] = useState(null);
+  const [signals, setSignals] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [selectedTicker, setSelectedTicker] = useState(null);
+  const [viewMode, setViewMode] = useState('category'); // 'category' | 'flat'
+  const [sortBy, setSortBy] = useState('pnl'); // 'pnl' | 'trades' | 'winrate'
+
+  useEffect(() => {
+    load();
+    const i = setInterval(load, 30000);
+    return () => clearInterval(i);
+  }, []);
+
+  async function load() {
+    try {
+      const [d, s] = await Promise.all([api('/dashboard'), api('/signals')]);
+      setData(d);
+      setSignals(s);
+    } catch {}
+    setLoading(false);
+  }
+
+  // Combine top_markets and worst_markets, strategies into categories
+  const { categories, allMarkets, summary } = useMemo(() => {
+    if (!data) return { categories: {}, allMarkets: [], summary: {} };
+
+    const markets = [];
+    const seen = new Set();
+
+    // Add top_markets
+    for (const m of (data.top_markets || [])) {
+      if (!seen.has(m.ticker)) {
+        seen.add(m.ticker);
+        const cat = classifyMarket(m.title);
+        markets.push({
+          ...m,
+          category: cat,
+          trades: (m.wins || 0) + (m.losses || 0),
+          shortTitle: (m.title || m.ticker).split('?')[0].substring(0, 40),
+          size: Math.max(Math.abs(m.pnl || 1), 100),
+        });
+      }
+    }
+
+    // Add worst_markets
+    for (const m of (data.worst_markets || [])) {
+      if (!seen.has(m.ticker)) {
+        seen.add(m.ticker);
+        const cat = classifyMarket(m.title);
+        markets.push({
+          ...m,
+          category: cat,
+          trades: (m.wins || 0) + (m.losses || 0),
+          shortTitle: (m.title || m.ticker).split('?')[0].substring(0, 40),
+          size: Math.max(Math.abs(m.pnl || 1), 100),
+        });
+      }
+    }
+
+    // Add strategies as synthetic market entries
+    for (const s of (data.strategies || [])) {
+      const key = `strategy-${s.strategy || s.name}`;
+      if (!seen.has(key)) {
+        seen.add(key);
+        markets.push({
+          ticker: key,
+          title: `Strategy: ${s.name}`,
+          shortTitle: s.name,
+          category: 'Other',
+          wins: s.wins || 0,
+          losses: s.losses || 0,
+          trades: (s.wins || 0) + (s.losses || 0),
+          pnl: s.pnl || 0,
+          edge: `Win Rate: ${s.win_rate || 0}%`,
+          size: Math.max(Math.abs(s.pnl || 1), 100),
+        });
+      }
+    }
+
+    // Sort
+    if (sortBy === 'pnl') markets.sort((a, b) => b.pnl - a.pnl);
+    else if (sortBy === 'trades') markets.sort((a, b) => b.trades - a.trades);
+    else if (sortBy === 'winrate') {
+      markets.sort((a, b) => {
+        const wrA = a.trades > 0 ? a.wins / a.trades : 0;
+        const wrB = b.trades > 0 ? b.wins / b.trades : 0;
+        return wrB - wrA;
+      });
+    }
+
+    // Group by category
+    const cats = {};
+    for (const m of markets) {
+      if (!cats[m.category]) cats[m.category] = [];
+      cats[m.category].push(m);
+    }
+
+    // Summary stats
+    const totalPnl = markets.reduce((s, m) => s + m.pnl, 0);
+    const totalTrades = markets.reduce((s, m) => s + m.trades, 0);
+    const totalWins = markets.reduce((s, m) => s + m.wins, 0);
+    const totalLosses = markets.reduce((s, m) => s + m.losses, 0);
+    const profitableCount = markets.filter(m => m.pnl > 0).length;
+
+    return {
+      categories: cats,
+      allMarkets: markets,
+      summary: { totalPnl, totalTrades, totalWins, totalLosses, profitableCount, totalMarkets: markets.length },
+    };
+  }, [data, sortBy]);
+
+  const selectedMarket = allMarkets.find(m => m.ticker === selectedTicker);
+
+  if (loading) return (
+    <div className="flex items-center justify-center h-64">
+      <div
+        className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
+        style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+      />
+    </div>
+  );
+
+  // Calculate treemap layout
+  const WIDTH = 1200;
+  const HEIGHT = 700;
+
+  const categoryEntries = Object.entries(categories).sort((a, b) => {
+    const pnlA = a[1].reduce((s, m) => s + Math.abs(m.pnl), 0);
+    const pnlB = b[1].reduce((s, m) => s + Math.abs(m.pnl), 0);
+    return pnlB - pnlA;
+  });
+
+  const categoryBlocks = squarify(
+    categoryEntries.map(([cat, items]) => ({
+      name: cat,
+      size: items.reduce((s, i) => s + Math.abs(i.pnl || 1), 0) + items.length * 100,
+      items,
+    })),
+    0, 0, WIDTH, HEIGHT
+  );
+
+  // Flat mode layout
+  const flatBlocks = viewMode === 'flat' ? squarify(
+    allMarkets.map(m => ({ ...m, size: Math.max(Math.abs(m.pnl || 1), 200) })),
+    0, 0, WIDTH, HEIGHT
+  ) : [];
+
+  const summaryStats = [
+    {
+      label: 'Total P&L',
+      value: `${summary.totalPnl >= 0 ? '+' : ''}$${(summary.totalPnl / 100).toLocaleString(undefined, { minimumFractionDigits: 0 })}`,
+      color: summary.totalPnl >= 0 ? 'var(--green)' : 'var(--red)',
+    },
+    { label: 'Total Trades', value: summary.totalTrades?.toLocaleString() || '0', color: 'var(--orange)' },
+    {
+      label: 'Win Rate',
+      value: summary.totalTrades > 0 ? `${((summary.totalWins / summary.totalTrades) * 100).toFixed(1)}%` : '0%',
+      color: 'var(--blue)',
+    },
+    { label: 'Markets', value: summary.totalMarkets || 0, color: 'var(--purple)' },
+    { label: 'Profitable', value: summary.profitableCount || 0, color: 'var(--green)' },
+    { label: 'Categories', value: Object.keys(categories).length, color: 'var(--cyan)' },
+  ];
+
+  return (
+    <div className="space-y-6">
+      {/* Page Header */}
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="heading text-2xl font-bold kalshi-gradient">Market Heatmap</h1>
+          <p className="section-subtitle mt-1">Treemap visualization of trading activity and P&L by market category</p>
+        </div>
+        <div className="flex items-center gap-3">
+          {/* View mode toggle */}
+          <div
+            className="flex items-center gap-0.5 rounded-lg p-0.5"
+            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+          >
+            {['category', 'flat'].map(mode => (
+              <button
+                key={mode}
+                onClick={() => setViewMode(mode)}
+                className="btn btn-sm"
+                style={viewMode === mode
+                  ? { background: 'var(--cyan)', color: 'var(--bg)', fontWeight: 700 }
+                  : { background: 'transparent', color: 'var(--text-muted)' }
+                }
+              >
+                {mode === 'category' ? 'By Category' : 'All Markets'}
+              </button>
+            ))}
+          </div>
+
+          {/* Sort select */}
+          <select
+            value={sortBy}
+            onChange={e => setSortBy(e.target.value)}
+            className="input"
+            style={{ width: 'auto', paddingRight: '1.5rem' }}
+          >
+            <option value="pnl">Sort by P&L</option>
+            <option value="trades">Sort by Trades</option>
+            <option value="winrate">Sort by Win Rate</option>
+          </select>
+
+          <button onClick={load} className="btn btn-o btn-sm">Refresh</button>
+        </div>
+      </div>
+
+      {/* Summary Stats — Win Rate and Profitable use RingGauge */}
+      <div className="grid grid-cols-6 gap-3">
+        {summaryStats.map((s, i) => {
+          const accentMap = ['card-accent-green', 'card-accent-orange', 'card-accent-blue', 'card-accent-purple', 'card-accent-green', 'card-accent-cyan'];
+
+          // Win Rate (index 2) gets a green ring gauge
+          if (s.label === 'Win Rate') {
+            const numericWR = parseFloat(s.value) || 0;
+            return (
+              <div key={s.label} className={`card ${accentMap[i] || 'card-accent-cyan'}`} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
+                <RingGauge
+                  value={numericWR}
+                  size={52}
+                  strokeWidth={4}
+                  color={numericWR >= 80 ? 'var(--green)' : numericWR >= 50 ? 'var(--orange)' : 'var(--red)'}
+                />
+                <div>
+                  <p className="stat-label mb-0.5">{s.label}</p>
+                  <p className="stat-v mono text-lg" style={{ color: s.color }}>{s.value}</p>
+                </div>
+              </div>
+            );
+          }
+
+          // Profitable (index 4) gets a cyan ring gauge (profitable% out of total markets)
+          if (s.label === 'Profitable') {
+            const totalMkts = summaryStats.find(x => x.label === 'Markets')?.value || 1;
+            const profitPct = totalMkts > 0 ? Math.round((Number(s.value) / Number(totalMkts)) * 100) : 0;
+            return (
+              <div key={s.label} className={`card ${accentMap[i] || 'card-accent-cyan'}`} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
+                <RingGauge
+                  value={profitPct}
+                  size={52}
+                  strokeWidth={4}
+                  color="var(--green)"
+                />
+                <div>
+                  <p className="stat-label mb-0.5">{s.label}</p>
+                  <p className="stat-v mono text-lg" style={{ color: s.color }}>{s.value}</p>
+                </div>
+              </div>
+            );
+          }
+
+          return (
+            <div key={s.label} className={`card ${accentMap[i] || 'card-accent-cyan'}`}>
+              <p className="stat-label mb-1">{s.label}</p>
+              <p className="stat-v mono text-xl" style={{ color: s.color }}>{s.value}</p>
+            </div>
+          );
+        })}
+      </div>
+
+      {/* ── Charts 4 & 5: Category Donut + Win/Loss Bar ── */}
+      {Object.keys(categories).length > 0 && (
+        <div style={{ display: 'grid', gridTemplateColumns: '1fr 2fr', gap: 16 }}>
+          <CategoryDonutChart categories={categories} />
+          <CategoryWinLossChart categories={categories} />
+        </div>
+      )}
+
+      {/* Category Pills */}
+      <div className="flex items-center gap-2 flex-wrap">
+        {Object.entries(categories).map(([cat, items]) => {
+          const catColor = CATEGORY_COLORS[cat] || CATEGORY_COLORS.Other;
+          const catPnl = items.reduce((s, m) => s + m.pnl, 0);
+          return (
+            <div
+              key={cat}
+              className="flex items-center gap-1.5 px-3 py-1.5 rounded-full"
+              style={{
+                backgroundColor: `${catColor.hex}18`,
+                border: `1px solid ${catColor.hex}40`,
+              }}
+            >
+              <div className="w-2 h-2 rounded-full" style={{ backgroundColor: catColor.hex }} />
+              <span className="text-xs font-semibold heading" style={{ color: catColor.hex }}>{cat}</span>
+              <span className="text-[10px]" style={{ color: 'var(--text-muted)' }}>{items.length}</span>
+              <span
+                className="text-[10px] mono font-bold"
+                style={{ color: catPnl >= 0 ? 'var(--green)' : 'var(--red)' }}
+              >
+                {catPnl >= 0 ? '+' : ''}${(catPnl / 100).toFixed(0)}
+              </span>
+            </div>
+          );
+        })}
+      </div>
+
+      {/* Treemap */}
+      <div
+        className="card p-2 relative overflow-hidden"
+        style={{ minHeight: HEIGHT + 20 }}
+      >
+        <div className="relative" style={{ width: WIDTH, height: HEIGHT, margin: '0 auto', maxWidth: '100%' }}>
+          {viewMode === 'category' ? (
+            categoryBlocks.map(block => (
+              <CategoryBlock
+                key={block.name}
+                category={block.name}
+                items={block.items}
+                x={block.x}
+                y={block.y}
+                w={block.w}
+                h={block.h}
+                onTileClick={m => setSelectedTicker(m.ticker === selectedTicker ? null : m.ticker)}
+                selectedTicker={selectedTicker}
+              />
+            ))
+          ) : (
+            flatBlocks.map(tile => (
+              <MarketTile
+                key={tile.ticker}
+                item={tile}
+                onClick={m => setSelectedTicker(m.ticker === selectedTicker ? null : m.ticker)}
+                isSelected={selectedTicker === tile.ticker}
+              />
+            ))
+          )}
+        </div>
+
+        {/* Legend overlay */}
+        <div className="mt-3 px-2">
+          <Legend />
+        </div>
+      </div>
+
+      {/* Selected Market Detail */}
+      {selectedMarket && (
+        <DetailPanel market={selectedMarket} onClose={() => setSelectedTicker(null)} />
+      )}
+
+      {/* Category Breakdown Table */}
+      <div className="card">
+        <div className="flex items-center gap-2 mb-4">
+          <h2 className="section-title">Category Breakdown</h2>
+        </div>
+        <table>
+          <thead>
+            <tr>
+              <th>Category</th>
+              <th>Markets</th>
+              <th>Total Trades</th>
+              <th>Win Rate</th>
+              <th>P&L</th>
+              <th>Best Market</th>
+            </tr>
+          </thead>
+          <tbody>
+            {categoryEntries.map(([cat, items]) => {
+              const catColor = CATEGORY_COLORS[cat] || CATEGORY_COLORS.Other;
+              const catPnl = items.reduce((s, m) => s + m.pnl, 0);
+              const catTrades = items.reduce((s, m) => s + m.trades, 0);
+              const catWins = items.reduce((s, m) => s + m.wins, 0);
+              const catWr = catTrades > 0 ? ((catWins / catTrades) * 100).toFixed(1) : '0';
+              const best = items.reduce((b, m) => m.pnl > (b?.pnl || -Infinity) ? m : b, items[0]);
+
+              return (
+                <tr key={cat}>
+                  <td>
+                    <div className="flex items-center gap-2">
+                      <div className="w-3 h-3 rounded-full" style={{ backgroundColor: catColor.hex }} />
+                      <span className="font-semibold heading" style={{ color: catColor.hex }}>{cat}</span>
+                    </div>
+                  </td>
+                  <td style={{ color: 'var(--text-dim)' }}>{items.length}</td>
+                  <td className="mono" style={{ color: 'var(--text-dim)' }}>{catTrades.toLocaleString()}</td>
+                  <td
+                    className="mono font-bold"
+                    style={{
+                      color: Number(catWr) >= 80 ? 'var(--green)'
+                           : Number(catWr) >= 50 ? 'var(--orange)'
+                           : 'var(--red)',
+                    }}
+                  >
+                    {catWr}%
+                  </td>
+                  <td
+                    className="mono font-bold"
+                    style={{ color: catPnl >= 0 ? 'var(--green)' : 'var(--red)' }}
+                  >
+                    {catPnl >= 0 ? '+' : ''}${(catPnl / 100).toFixed(2)}
+                  </td>
+                  <td
+                    className="text-xs truncate max-w-[200px]"
+                    style={{ color: 'var(--text-muted)' }}
+                  >
+                    {best?.shortTitle || '--'}
+                  </td>
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Intelligence.jsx b/kalshi-dash/src/pages/Intelligence.jsx
new file mode 100644
index 0000000..0d88cf6
--- /dev/null
+++ b/kalshi-dash/src/pages/Intelligence.jsx
@@ -0,0 +1,340 @@
+import React, { useState, useEffect } from 'react';
+import { api } from '../api';
+
+/* ─── Source icon map (text-based, no emoji dependency issues) ─── */
+const SOURCE_ICONS = {
+  reddit_hot: '🔴', reddit: '📋', google_news: '📰', hackernews: '🟠',
+  metaculus: '🔮', manifold: '📊', gdelt_tv: '📺', youtube: '▶️',
+  lemmy: '🟢', lobsters: '🦞', tildes: '~', bluesky: '🦋',
+  mastodon: '🐘', predictit: '💰', fred: '📈', nws: '⛈️',
+  govtrack: '🏛️', discord: '💬',
+};
+
+/* Colour-code source badges by type */
+function sourceBadgeClass(source) {
+  if (['reddit_hot', 'reddit', 'lemmy'].includes(source)) return 'bg-red';
+  if (['google_news', 'govtrack', 'nws'].includes(source)) return 'bg-blue';
+  if (['metaculus', 'manifold', 'predictit'].includes(source)) return 'bg-purple';
+  if (['hackernews', 'lobsters', 'tildes'].includes(source)) return 'bg-orange';
+  if (['fred'].includes(source)) return 'bg-green';
+  if (['bluesky', 'mastodon', 'discord'].includes(source)) return 'bg-cyan';
+  return 'bg-blue';
+}
+
+/* Relevance / sentiment badge */
+function SentimentBadge({ value }) {
+  const v = parseFloat(value) || 0;
+  if (v > 0.1)  return <span className="badge bg-cyan"  >+{(v * 100).toFixed(0)}%</span>;
+  if (v < -0.1) return <span className="badge bg-red"   >{(v * 100).toFixed(0)}%</span>;
+  return             <span className="badge bg-orange" >{(v * 100).toFixed(0)}%</span>;
+}
+
+/* Sortable column header */
+function SortHead({ col, label, sortCol, sortDir, onSort }) {
+  const active = sortCol === col;
+  return (
+    <th
+      onClick={() => onSort(col)}
+      className="cursor-pointer select-none transition"
+      style={{ color: active ? 'var(--cyan)' : undefined }}
+    >
+      <span className="flex items-center gap-1">
+        {label}
+        {active
+          ? <span style={{ color: 'var(--cyan)' }}>{sortDir === 'asc' ? '▲' : '▼'}</span>
+          : <span style={{ color: 'var(--border-light)' }}>⇅</span>
+        }
+      </span>
+    </th>
+  );
+}
+
+/* ─── Stat card ─── */
+function StatCard({ label, value, color }) {
+  return (
+    <div className="card">
+      <p className="stat-label mb-2">{label}</p>
+      <p className="stat-v" style={{ color }}>{value}</p>
+    </div>
+  );
+}
+
+/* ─── Main Intelligence page ─── */
+export default function Intelligence() {
+  const [data, setData] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [filter, setFilter] = useState('');
+  const [sortCol, setSortCol] = useState('captured_at');
+  const [sortDir, setSortDir] = useState('desc');
+
+  useEffect(() => {
+    load();
+    const i = setInterval(load, 60000);
+    return () => clearInterval(i);
+  }, []);
+
+  async function load() {
+    try {
+      const r = await api('/intelligence');
+      setData(r);
+    } catch {}
+    setLoading(false);
+  }
+
+  function toggleSort(col) {
+    if (sortCol === col) setSortDir(d => d === 'asc' ? 'desc' : 'asc');
+    else { setSortCol(col); setSortDir(col === 'captured_at' ? 'desc' : 'asc'); }
+  }
+
+  if (loading) {
+    return (
+      <div className="flex items-center justify-center h-64">
+        <div
+          className="animate-spin w-8 h-8 border-2 border-t-transparent rounded-full pulse-cyan"
+          style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+        />
+      </div>
+    );
+  }
+
+  const totals = data?.totals || {};
+  const sources = data?.sourceCounts || [];
+
+  const filtered = (data?.recent || []).filter(item =>
+    !filter ||
+    item.source.toLowerCase().includes(filter.toLowerCase()) ||
+    (item.post_title || '').toLowerCase().includes(filter.toLowerCase()) ||
+    (item.keyword || '').toLowerCase().includes(filter.toLowerCase())
+  );
+
+  function sortFn(a, b) {
+    let va, vb;
+    switch (sortCol) {
+      case 'captured_at': va = new Date(a.captured_at); vb = new Date(b.captured_at); break;
+      case 'source':      va = a.source || '';          vb = b.source || '';           break;
+      case 'keyword':     va = a.keyword || a.subreddit || ''; vb = b.keyword || b.subreddit || ''; break;
+      case 'post_title':  va = (a.post_title || '').toLowerCase(); vb = (b.post_title || '').toLowerCase(); break;
+      case 'score':       va = Number(a.score || 0);    vb = Number(b.score || 0);     break;
+      case 'sentiment':   va = parseFloat(a.sentiment_score || 0); vb = parseFloat(b.sentiment_score || 0); break;
+      default: return 0;
+    }
+    if (va < vb) return sortDir === 'asc' ? -1 : 1;
+    if (va > vb) return sortDir === 'asc' ?  1 : -1;
+    return 0;
+  }
+
+  const items = [...filtered].sort(sortFn);
+
+  const avgSent = parseFloat(totals.avg_sentiment_1h) || 0;
+  const avgSentColor = avgSent > 0 ? 'var(--green)' : avgSent < 0 ? 'var(--red)' : 'var(--text-muted)';
+
+  const sortProps = { sortCol, sortDir, onSort: toggleSort };
+
+  return (
+    <div className="space-y-6 fade-in">
+
+      {/* ── Page Header ── */}
+      <div className="flex items-center justify-between">
+        <div>
+          <div className="flex items-center gap-3">
+            {/* Flask icon */}
+            <span className="text-2xl" style={{ color: 'var(--cyan)' }}>⚗</span>
+            <h1 className="heading text-3xl font-bold" style={{ color: 'var(--text)' }}>
+              Intel Feed
+            </h1>
+            <span className="flex items-center gap-1.5">
+              <span
+                className="inline-block w-2 h-2 rounded-full pulse-cyan"
+                style={{ background: 'var(--cyan)' }}
+              />
+              <span className="text-xs" style={{ color: 'var(--cyan)' }}>LIVE</span>
+            </span>
+          </div>
+          <p className="section-subtitle mt-1">Real-time data from 20+ sources</p>
+        </div>
+        <button onClick={load} className="btn btn-o btn-sm">↻ Refresh</button>
+      </div>
+
+      {/* ── Stats ── */}
+      <div className="grid grid-cols-4 gap-4">
+        <StatCard label="Total Items"      value={Number(totals.total_items || 0).toLocaleString()} color="var(--orange)" />
+        <StatCard label="Last Hour"        value={totals.last_hour || 0}                            color="var(--green)" />
+        <StatCard label="Last 4 Hours"     value={totals.last_4h || 0}                              color="var(--blue)" />
+        <StatCard
+          label="Avg Sentiment 1h"
+          value={totals.avg_sentiment_1h ? `${(avgSent * 100).toFixed(1)}%` : '—'}
+          color={avgSentColor}
+        />
+      </div>
+
+      {/* ── Source filter pills ── */}
+      <div className="card" style={{ padding: '1rem' }}>
+        <p className="section-title mb-3" style={{ fontSize: '0.9rem' }}>
+          Active Sources
+        </p>
+        <div className="flex flex-wrap gap-2">
+          {/* "All" pill */}
+          <button
+            onClick={() => setFilter('')}
+            className="badge transition cursor-pointer"
+            style={{
+              background: !filter ? 'var(--cyan-dim)' : 'rgba(42,42,53,0.6)',
+              color: !filter ? 'var(--cyan)' : 'var(--text-muted)',
+              border: !filter ? '1px solid rgba(0,240,255,0.3)' : '1px solid var(--border)',
+            }}
+          >
+            All
+          </button>
+
+          {sources.map((s, i) => {
+            const active = filter === s.source;
+            const sent = parseFloat(s.avg_sent) || 0;
+            return (
+              <button
+                key={i}
+                onClick={() => setFilter(active ? '' : s.source)}
+                className="badge transition cursor-pointer"
+                style={{
+                  background: active ? 'var(--cyan-dim)' : 'rgba(42,42,53,0.6)',
+                  color: active ? 'var(--cyan)' : 'var(--text-muted)',
+                  border: active ? '1px solid rgba(0,240,255,0.3)' : '1px solid var(--border)',
+                }}
+              >
+                <span>{SOURCE_ICONS[s.source] || '📡'}</span>
+                <span className="font-medium">{s.source}</span>
+                <span style={{ color: 'var(--text-muted)', opacity: 0.7 }}>{s.cnt}</span>
+                <span
+                  className="mono"
+                  style={{ color: sent > 0.05 ? 'var(--green)' : sent < -0.05 ? 'var(--red)' : 'var(--text-muted)' }}
+                >
+                  {sent > 0 ? '+' : ''}{(sent * 100).toFixed(0)}%
+                </span>
+              </button>
+            );
+          })}
+        </div>
+      </div>
+
+      {/* ── Search Bar ── */}
+      <div className="relative">
+        <span
+          className="absolute left-3 top-1/2 -translate-y-1/2 text-sm pointer-events-none"
+          style={{ color: 'var(--text-muted)' }}
+        >
+          ⌕
+        </span>
+        <input
+          type="text"
+          value={filter}
+          onChange={e => setFilter(e.target.value)}
+          placeholder="Filter by source, keyword, or title..."
+          className="input pl-8"
+        />
+      </div>
+
+      {/* ── Intel Feed Table ── */}
+      <div className="card overflow-x-auto" style={{ padding: 0 }}>
+        <table>
+          <thead>
+            <tr>
+              <SortHead col="captured_at" label="Time"      {...sortProps} />
+              <SortHead col="source"      label="Source"    {...sortProps} />
+              <SortHead col="keyword"     label="Topic"     {...sortProps} />
+              <SortHead col="post_title"  label="Headline"  {...sortProps} />
+              <SortHead col="score"       label="Score"     {...sortProps} />
+              <SortHead col="sentiment"   label="Sentiment" {...sortProps} />
+            </tr>
+          </thead>
+          <tbody>
+            {items.length === 0 ? (
+              <tr>
+                <td colSpan={6} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
+                  <div className="flex flex-col items-center gap-2">
+                    <span className="text-2xl">⚗</span>
+                    <span className="text-sm">No items match your filter</span>
+                  </div>
+                </td>
+              </tr>
+            ) : (
+              items.slice(0, 50).map((item, i) => {
+                const sent = parseFloat(item.sentiment_score) || 0;
+                return (
+                  <tr key={i}>
+                    {/* Time */}
+                    <td className="whitespace-nowrap">
+                      <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
+                        {new Date(item.captured_at).toLocaleTimeString()}
+                      </span>
+                    </td>
+
+                    {/* Source badge */}
+                    <td>
+                      <span className={`badge ${sourceBadgeClass(item.source)}`}>
+                        {SOURCE_ICONS[item.source] || '📡'} {item.source}
+                      </span>
+                    </td>
+
+                    {/* Topic / keyword */}
+                    <td>
+                      <span className="text-xs" style={{ color: 'var(--text-dim)' }}>
+                        {item.keyword || item.subreddit || '—'}
+                      </span>
+                    </td>
+
+                    {/* Headline */}
+                    <td className="max-w-[400px]">
+                      {item.post_url ? (
+                        <a
+                          href={item.post_url}
+                          target="_blank"
+                          rel="noopener noreferrer"
+                          className="text-sm truncate block hover:underline transition heading"
+                          style={{ color: 'var(--text)' }}
+                        >
+                          {item.post_title}
+                        </a>
+                      ) : (
+                        <span className="text-sm truncate block heading" style={{ color: 'var(--text)' }}>
+                          {item.post_title}
+                        </span>
+                      )}
+                    </td>
+
+                    {/* Score */}
+                    <td>
+                      <span className="mono text-xs" style={{ color: 'var(--text-dim)' }}>
+                        {item.score || 0}
+                        {item.num_comments ? (
+                          <span style={{ color: 'var(--text-muted)' }}> / {item.num_comments}c</span>
+                        ) : null}
+                      </span>
+                    </td>
+
+                    {/* Sentiment badge */}
+                    <td>
+                      <SentimentBadge value={item.sentiment_score} />
+                    </td>
+                  </tr>
+                );
+              })
+            )}
+          </tbody>
+        </table>
+
+        {/* Row count footer */}
+        {items.length > 0 && (
+          <div
+            className="px-4 py-3 text-xs"
+            style={{
+              color: 'var(--text-muted)',
+              borderTop: '1px solid var(--border)',
+            }}
+          >
+            Showing {Math.min(50, items.length)} of {items.length} items
+            {filter && ` matching "${filter}"`}
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Markets.jsx b/kalshi-dash/src/pages/Markets.jsx
new file mode 100644
index 0000000..f134896
--- /dev/null
+++ b/kalshi-dash/src/pages/Markets.jsx
@@ -0,0 +1,427 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../api';
+
+/* ─── Orderbook sub-row inside a ContractRow ─── */
+function ContractRow({ m }) {
+  const [ob, setOb] = useState(null);
+  const [obOpen, setObOpen] = useState(false);
+  const [obLoading, setObLoading] = useState(false);
+
+  async function loadOb() {
+    if (obOpen) { setObOpen(false); return; }
+    setObOpen(true);
+    setObLoading(true);
+    try {
+      const r = await post('/kalshi', { action: 'get_orderbook', ticker: m.ticker });
+      if (r.success) setOb(r.orderbook);
+    } catch {}
+    setObLoading(false);
+  }
+
+  return (
+    <>
+      <tr style={{ background: 'rgba(0,240,255,0.02)' }}>
+        {/* Contract title */}
+        <td className="pl-8 max-w-[250px]">
+          <span className="truncate block text-sm" style={{ color: 'var(--text-dim)' }}>
+            {m.title || m.subtitle || m.ticker}
+          </span>
+        </td>
+
+        {/* Ticker link */}
+        <td>
+          <a
+            href={`https://kalshi.com/search?query=${encodeURIComponent((m.ticker || '').split('-')[0])}`}
+            target="_blank"
+            rel="noopener noreferrer"
+            className="mono text-xs hover:underline transition"
+            style={{ color: 'var(--cyan)' }}
+          >
+            {m.ticker}
+          </a>
+        </td>
+
+        {/* Yes Bid */}
+        <td>
+          <span className="mono text-sm" style={{ color: 'var(--green)' }}>
+            {m.yes_bid ?? '—'}¢
+          </span>
+        </td>
+
+        {/* Yes Ask */}
+        <td>
+          <span className="mono text-sm" style={{ color: 'var(--red)' }}>
+            {m.yes_ask ?? '—'}¢
+          </span>
+        </td>
+
+        {/* Volume */}
+        <td>
+          <span className="mono text-xs" style={{ color: 'var(--text-dim)' }}>
+            {(m.volume || 0).toLocaleString()}
+          </span>
+        </td>
+
+        {/* Open Interest */}
+        <td>
+          <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
+            {m.open_interest ? m.open_interest.toLocaleString() : '—'}
+          </span>
+        </td>
+
+        {/* Orderbook toggle */}
+        <td>
+          <button
+            onClick={loadOb}
+            className="btn btn-ghost btn-sm"
+            style={{ color: obOpen ? 'var(--cyan)' : 'var(--text-muted)' }}
+          >
+            {obOpen ? '▾ Book' : '▸ Book'}
+          </button>
+        </td>
+      </tr>
+
+      {/* Orderbook detail row */}
+      {obOpen && (
+        <tr style={{ background: 'rgba(0,240,255,0.01)' }}>
+          <td colSpan={7} className="p-4">
+            {obLoading ? (
+              <div className="flex items-center justify-center h-16">
+                <div
+                  className="animate-spin w-5 h-5 border-2 border-t-transparent rounded-full"
+                  style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+                />
+              </div>
+            ) : ob ? (
+              <div className="grid grid-cols-2 gap-6 max-w-lg">
+                {/* YES bids */}
+                <div>
+                  <p className="stat-label mb-2" style={{ color: 'var(--green)' }}>YES Bids</p>
+                  <div className="space-y-1">
+                    {(ob.yes || []).slice(0, 8).map((b, i) => (
+                      <div key={i} className="relative flex justify-between text-xs px-2 py-1 rounded overflow-hidden" style={{ background: 'var(--surface-el)' }}>
+                        <div
+                          className="absolute inset-0 rounded"
+                          style={{ width: `${Math.min((b[1] || 0) * 2, 100)}%`, background: 'rgba(0,195,137,0.08)' }}
+                        />
+                        <span className="mono relative z-10" style={{ color: 'var(--green)' }}>{b[0]}¢</span>
+                        <span className="mono relative z-10" style={{ color: 'var(--text-muted)' }}>{b[1]}</span>
+                      </div>
+                    ))}
+                    {(!ob.yes || ob.yes.length === 0) && (
+                      <p className="text-xs text-center py-2" style={{ color: 'var(--text-muted)' }}>Empty</p>
+                    )}
+                  </div>
+                </div>
+
+                {/* NO bids */}
+                <div>
+                  <p className="stat-label mb-2" style={{ color: 'var(--red)' }}>NO Bids</p>
+                  <div className="space-y-1">
+                    {(ob.no || []).slice(0, 8).map((a, i) => (
+                      <div key={i} className="relative flex justify-between text-xs px-2 py-1 rounded overflow-hidden" style={{ background: 'var(--surface-el)' }}>
+                        <div
+                          className="absolute inset-0 rounded"
+                          style={{ width: `${Math.min((a[1] || 0) * 2, 100)}%`, background: 'rgba(255,92,92,0.08)' }}
+                        />
+                        <span className="mono relative z-10" style={{ color: 'var(--red)' }}>{a[0]}¢</span>
+                        <span className="mono relative z-10" style={{ color: 'var(--text-muted)' }}>{a[1]}</span>
+                      </div>
+                    ))}
+                    {(!ob.no || ob.no.length === 0) && (
+                      <p className="text-xs text-center py-2" style={{ color: 'var(--text-muted)' }}>Empty</p>
+                    )}
+                  </div>
+                </div>
+              </div>
+            ) : (
+              <p className="text-xs text-center py-2" style={{ color: 'var(--text-muted)' }}>No orderbook data</p>
+            )}
+          </td>
+        </tr>
+      )}
+    </>
+  );
+}
+
+/* ─── Expandable event row ─── */
+function EventRow({ e }) {
+  const [expanded, setExpanded] = useState(false);
+  const [contracts, setContracts] = useState([]);
+  const [loading, setLoading] = useState(false);
+
+  async function toggle() {
+    if (expanded) { setExpanded(false); return; }
+    setExpanded(true);
+    if (contracts.length > 0) return;
+    setLoading(true);
+    try {
+      const r = await post('/kalshi', { action: 'get_markets', params: { event_ticker: e.event_ticker } });
+      setContracts(r.markets || []);
+    } catch {}
+    setLoading(false);
+  }
+
+  return (
+    <>
+      {/* Event header row */}
+      <tr
+        className="cursor-pointer transition"
+        onClick={toggle}
+        style={{ background: expanded ? 'rgba(0,240,255,0.04)' : undefined }}
+        onMouseEnter={ev => {
+          if (!expanded) {
+            ev.currentTarget.style.background = 'var(--cyan-glow)';
+          }
+        }}
+        onMouseLeave={ev => {
+          if (!expanded) ev.currentTarget.style.background = '';
+        }}
+      >
+        <td className="max-w-[300px]">
+          <div className="flex items-center gap-2">
+            <span
+              className="text-xs transition-transform inline-block"
+              style={{
+                color: 'var(--text-muted)',
+                transform: expanded ? 'rotate(90deg)' : 'rotate(0deg)',
+              }}
+            >
+              ▶
+            </span>
+            <span className="truncate block font-medium heading" style={{ color: 'var(--text)' }}>
+              {e.title}
+            </span>
+          </div>
+        </td>
+
+        <td>
+          <span className="badge bg-orange">{e.category || '—'}</span>
+        </td>
+
+        <td>
+          <a
+            href={`https://kalshi.com/search?query=${encodeURIComponent((e.event_ticker || '').split('-')[0])}`}
+            target="_blank"
+            rel="noopener noreferrer"
+            className="mono text-xs hover:underline transition"
+            style={{ color: 'var(--cyan)' }}
+            onClick={ev => ev.stopPropagation()}
+          >
+            {e.event_ticker}
+          </a>
+        </td>
+
+        <td>
+          <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
+            {e.mutually_exclusive ? (
+              <span className="badge bg-blue">ME</span>
+            ) : '—'}
+          </span>
+        </td>
+
+        <td onClick={ev => ev.stopPropagation()}>
+          {expanded ? (
+            <span className="text-xs mono" style={{ color: 'var(--cyan)' }}>
+              {contracts.length || '...'} contracts
+            </span>
+          ) : (
+            <span className="text-xs transition" style={{ color: 'var(--text-muted)' }}>
+              View Contracts
+            </span>
+          )}
+        </td>
+      </tr>
+
+      {/* Contracts sub-table */}
+      {expanded && (
+        <tr>
+          <td colSpan={5} className="p-0">
+            <div
+              className="ml-6 fade-in"
+              style={{
+                borderLeft: '2px solid rgba(0,240,255,0.2)',
+                background: 'rgba(0,240,255,0.015)',
+              }}
+            >
+              {loading ? (
+                <div className="flex items-center justify-center py-8">
+                  <div
+                    className="animate-spin w-5 h-5 border-2 border-t-transparent rounded-full"
+                    style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+                  />
+                </div>
+              ) : contracts.length === 0 ? (
+                <div className="text-xs text-center py-4" style={{ color: 'var(--text-muted)' }}>
+                  No contracts found
+                </div>
+              ) : (
+                <table className="w-full">
+                  <thead>
+                    <tr>
+                      <th className="pl-8">Contract</th>
+                      <th>Ticker</th>
+                      <th>Yes Bid</th>
+                      <th>Yes Ask</th>
+                      <th>Volume</th>
+                      <th>Open Int.</th>
+                      <th>Book</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    {contracts.map(m => <ContractRow key={m.ticker} m={m} />)}
+                  </tbody>
+                </table>
+              )}
+            </div>
+          </td>
+        </tr>
+      )}
+    </>
+  );
+}
+
+/* ─── Main Markets page ─── */
+export default function Markets() {
+  const [events, setEvents] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [filter, setFilter] = useState('');
+  const [categoryFilter, setCategoryFilter] = useState('');
+
+  useEffect(() => { loadEvents(); }, []);
+
+  async function loadEvents() {
+    setLoading(true);
+    try {
+      const r = await post('/kalshi', { action: 'get_events', params: { status: 'open', limit: 100 } });
+      setEvents(r.events || []);
+    } catch {}
+    setLoading(false);
+  }
+
+  const categories = [...new Set(events.map(e => e.category).filter(Boolean))].sort();
+
+  const filtered = events.filter(e => {
+    const matchesText = !filter ||
+      (e.title || '').toLowerCase().includes(filter.toLowerCase()) ||
+      (e.category || '').toLowerCase().includes(filter.toLowerCase());
+    const matchesCat = !categoryFilter || e.category === categoryFilter;
+    return matchesText && matchesCat;
+  });
+
+  if (loading) {
+    return (
+      <div className="flex items-center justify-center h-64">
+        <div
+          className="animate-spin w-8 h-8 border-2 border-t-transparent rounded-full pulse-cyan"
+          style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+        />
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-6 fade-in">
+
+      {/* ── Page Header ── */}
+      <div className="flex items-center justify-between">
+        <div>
+          <div className="flex items-center gap-3">
+            <h1 className="heading text-3xl font-bold" style={{ color: 'var(--text)' }}>
+              Markets
+            </h1>
+            <span className="badge bg-cyan">
+              {events.length} events
+            </span>
+            {filtered.length !== events.length && (
+              <span className="badge bg-orange">
+                {filtered.length} filtered
+              </span>
+            )}
+          </div>
+          <p className="section-subtitle mt-1">Live Kalshi prediction markets</p>
+        </div>
+        <button onClick={loadEvents} className="btn btn-o btn-sm">
+          ↻ Refresh
+        </button>
+      </div>
+
+      {/* ── Search Bar ── */}
+      <div className="relative">
+        <span
+          className="absolute left-3 top-1/2 -translate-y-1/2 text-sm pointer-events-none"
+          style={{ color: 'var(--text-muted)' }}
+        >
+          ⌕
+        </span>
+        <input
+          type="text"
+          value={filter}
+          onChange={e => setFilter(e.target.value)}
+          placeholder="Search markets by name or category..."
+          className="input pl-8"
+        />
+      </div>
+
+      {/* ── Category Filter Pills ── */}
+      {categories.length > 0 && (
+        <div className="flex flex-wrap gap-2">
+          <button
+            onClick={() => setCategoryFilter('')}
+            className="badge transition cursor-pointer"
+            style={{
+              background: !categoryFilter ? 'var(--cyan-dim)' : 'rgba(42,42,53,0.6)',
+              color: !categoryFilter ? 'var(--cyan)' : 'var(--text-muted)',
+              border: !categoryFilter ? '1px solid rgba(0,240,255,0.3)' : '1px solid var(--border)',
+            }}
+          >
+            All
+          </button>
+          {categories.map(cat => (
+            <button
+              key={cat}
+              onClick={() => setCategoryFilter(cat === categoryFilter ? '' : cat)}
+              className="badge transition cursor-pointer"
+              style={{
+                background: categoryFilter === cat ? 'var(--orange-dim)' : 'rgba(42,42,53,0.6)',
+                color: categoryFilter === cat ? 'var(--orange)' : 'var(--text-muted)',
+                border: categoryFilter === cat ? '1px solid rgba(247,147,26,0.3)' : '1px solid var(--border)',
+              }}
+            >
+              {cat}
+            </button>
+          ))}
+        </div>
+      )}
+
+      {/* ── Events Table ── */}
+      <div className="card overflow-x-auto" style={{ padding: 0 }}>
+        <table>
+          <thead>
+            <tr>
+              <th className="pl-5">Event</th>
+              <th>Category</th>
+              <th>Ticker</th>
+              <th>Type</th>
+              <th>Contracts</th>
+            </tr>
+          </thead>
+          <tbody>
+            {filtered.length === 0 ? (
+              <tr>
+                <td colSpan={5} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
+                  <div className="flex flex-col items-center gap-2">
+                    <span className="text-2xl">⌕</span>
+                    <span className="text-sm">No markets match your filter</span>
+                  </div>
+                </td>
+              </tr>
+            ) : (
+              filtered.map(e => <EventRow key={e.event_ticker} e={e} />)
+            )}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Orbit.jsx b/kalshi-dash/src/pages/Orbit.jsx
new file mode 100644
index 0000000..f620abb
--- /dev/null
+++ b/kalshi-dash/src/pages/Orbit.jsx
@@ -0,0 +1,698 @@
+import React, { useState, useEffect, useMemo, useRef } from 'react';
+import { api } from '../api';
+
+// ── Orbit Ring Configuration ──
+const ORBIT_RINGS = [
+  { radius: 80,  label: 'Core',  color: '#00F0FF', opacity: 0.5,  description: 'Highest confidence' },
+  { radius: 160, label: 'Inner', color: '#00C389', opacity: 0.35, description: 'Strong signals' },
+  { radius: 240, label: 'Mid',   color: '#3B82F6', opacity: 0.25, description: 'Moderate signals' },
+  { radius: 320, label: 'Outer', color: '#A855F7', opacity: 0.18, description: 'Weak signals' },
+  { radius: 400, label: 'Edge',  color: '#FF5C5C', opacity: 0.12, description: 'Speculative' },
+];
+
+const DIRECTION_COLORS = {
+  BUY_YES: { fill: '#00C389', stroke: '#009E6F', glow: 'rgba(0,195,137,0.5)' },
+  BUY_NO:  { fill: '#FF5C5C', stroke: '#CC4A4A', glow: 'rgba(255,92,92,0.5)' },
+  YES:     { fill: '#00C389', stroke: '#009E6F', glow: 'rgba(0,195,137,0.5)' },
+  NO:      { fill: '#FF5C5C', stroke: '#CC4A4A', glow: 'rgba(255,92,92,0.5)' },
+};
+
+function getSignalColor(confidence) {
+  if (confidence >= 0.9) return '#00C389';
+  if (confidence >= 0.7) return '#F7931A';
+  if (confidence >= 0.5) return '#3B82F6';
+  if (confidence >= 0.3) return '#A855F7';
+  return '#FF5C5C';
+}
+
+function getOrbitRing(confidence) {
+  if (confidence >= 0.9)  return 0;
+  if (confidence >= 0.75) return 1;
+  if (confidence >= 0.6)  return 2;
+  if (confidence >= 0.4)  return 3;
+  return 4;
+}
+
+// ── Animated Star Field Background ──
+function StarField({ width, height }) {
+  const stars = useMemo(() => {
+    const s = [];
+    for (let i = 0; i < 80; i++) {
+      s.push({
+        x: Math.random() * width,
+        y: Math.random() * height,
+        r: Math.random() * 1.2 + 0.3,
+        opacity: Math.random() * 0.4 + 0.05,
+        delay: Math.random() * 3,
+      });
+    }
+    return s;
+  }, [width, height]);
+
+  return (
+    <>
+      {stars.map((star, i) => (
+        <circle
+          key={i}
+          cx={star.x}
+          cy={star.y}
+          r={star.r}
+          fill="white"
+          opacity={star.opacity}
+          className="animate-pulse"
+          style={{ animationDelay: `${star.delay}s`, animationDuration: `${2 + Math.random() * 3}s` }}
+        />
+      ))}
+    </>
+  );
+}
+
+// ── Planet/Market Node ──
+function MarketNode({ market, cx, cy, size, isSelected, onSelect, animate }) {
+  const dirColors = DIRECTION_COLORS[market.direction] || DIRECTION_COLORS.BUY_YES;
+  const confidence = parseFloat(market.confidence) || 0;
+  const signalColor = getSignalColor(confidence);
+  const pulseSize = size + 4;
+  const gradId = `gradient-${market.ticker?.replace(/[^a-zA-Z0-9]/g, '')}`;
+
+  return (
+    <g
+      onClick={() => onSelect(market)}
+      className="cursor-pointer"
+      style={{ transition: 'all 0.5s ease-out' }}
+    >
+      {/* Glow pulse ring */}
+      <circle
+        cx={cx}
+        cy={cy}
+        r={pulseSize}
+        fill="none"
+        stroke={dirColors.glow}
+        strokeWidth={1.5}
+        opacity={isSelected ? 0.9 : 0.3}
+        className={animate ? 'animate-ping' : ''}
+        style={{ animationDuration: '2s', transformOrigin: `${cx}px ${cy}px` }}
+      />
+
+      {/* Signal strength dashed ring */}
+      <circle
+        cx={cx}
+        cy={cy}
+        r={size + 2}
+        fill="none"
+        stroke={signalColor}
+        strokeWidth={2}
+        opacity={0.65}
+        strokeDasharray={`${confidence * 20} ${(1 - confidence) * 20}`}
+      />
+
+      {/* Main planet */}
+      <circle
+        cx={cx}
+        cy={cy}
+        r={size}
+        fill={`url(#${gradId})`}
+        stroke={isSelected ? '#00F0FF' : dirColors.stroke}
+        strokeWidth={isSelected ? 2.5 : 1}
+        className="transition-all duration-300"
+      />
+
+      {/* Gradient definition */}
+      <defs>
+        <radialGradient id={gradId}>
+          <stop offset="0%"   stopColor={dirColors.fill}   stopOpacity="0.9" />
+          <stop offset="70%"  stopColor={dirColors.fill}   stopOpacity="0.6" />
+          <stop offset="100%" stopColor={dirColors.stroke} stopOpacity="0.4" />
+        </radialGradient>
+      </defs>
+
+      {/* Edge % label */}
+      {size >= 8 && (
+        <text
+          x={cx}
+          y={cy + 1}
+          textAnchor="middle"
+          dominantBaseline="middle"
+          fill="white"
+          fontSize={Math.max(7, size * 0.6)}
+          fontWeight="bold"
+          fontFamily="'JetBrains Mono', monospace"
+          style={{ pointerEvents: 'none' }}
+        >
+          {market.edge_display || ''}
+        </text>
+      )}
+
+      {/* Ticker label */}
+      {size >= 10 && (
+        <text
+          x={cx}
+          y={cy + size + 12}
+          textAnchor="middle"
+          fill="#A8A8A8"
+          fontSize={8}
+          fontFamily="'JetBrains Mono', monospace"
+          style={{ pointerEvents: 'none' }}
+        >
+          {(market.shortTitle || '').substring(0, 18)}
+        </text>
+      )}
+    </g>
+  );
+}
+
+// ── Orbit Detail Panel ──
+function OrbitDetail({ market, onClose }) {
+  if (!market) return null;
+
+  const conf = parseFloat(market.confidence) || 0;
+  const edge = parseFloat(market.expected_edge) || 0;
+  const dirColors = DIRECTION_COLORS[market.direction] || DIRECTION_COLORS.BUY_YES;
+  const ringLabel = ORBIT_RINGS[getOrbitRing(conf)]?.label || 'Edge';
+
+  return (
+    <div className="card-glow fade-in">
+      <div className="flex items-start justify-between mb-4">
+        <div className="flex-1 min-w-0 mr-4">
+          <div className="flex items-center gap-2 mb-2">
+            <span className="inline-block w-3 h-3 rounded-full" style={{ backgroundColor: dirColors.fill }} />
+            <span
+              className="badge"
+              style={
+                market.direction === 'BUY_YES' || market.direction === 'YES'
+                  ? { background: 'var(--green-dim)', color: 'var(--green)' }
+                  : { background: 'var(--red-dim)', color: 'var(--red)' }
+              }
+            >
+              {market.direction}
+            </span>
+            {market.risk_approved ? (
+              <span className="badge bg-green">APPROVED</span>
+            ) : (
+              <span className="badge bg-red">BLOCKED</span>
+            )}
+          </div>
+          <h3 className="heading text-sm font-bold" style={{ color: 'var(--text)' }}>
+            {market.title || market.ticker}
+          </h3>
+          <p className="mono text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{market.ticker}</p>
+        </div>
+        <button onClick={onClose} className="btn btn-ghost text-lg leading-none" style={{ padding: '0 6px' }}>
+          &times;
+        </button>
+      </div>
+
+      <div className="grid grid-cols-3 gap-3 mb-4">
+        {[
+          { label: 'Edge',        value: `${(edge * 100).toFixed(1)}%`,  color: 'var(--orange)' },
+          { label: 'Confidence',  value: `${(conf * 100).toFixed(0)}%`,  color: getSignalColor(conf) },
+          { label: 'Orbit Ring',  value: ringLabel,                       color: 'var(--purple)' },
+        ].map(s => (
+          <div
+            key={s.label}
+            className="rounded-lg p-2.5 text-center"
+            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+          >
+            <p className="stat-label mb-1">{s.label}</p>
+            <p className="stat-v text-lg" style={{ color: s.color }}>{s.value}</p>
+          </div>
+        ))}
+      </div>
+
+      {market.reasoning && (
+        <div
+          className="text-[11px] rounded-lg px-3 py-2 mb-3 leading-relaxed"
+          style={{
+            color: 'var(--text-dim)',
+            background: 'var(--surface-el)',
+            border: '1px solid var(--border)',
+          }}
+        >
+          {market.reasoning}
+        </div>
+      )}
+
+      {market.sources && market.sources.length > 0 && (
+        <div className="space-y-1.5">
+          <p className="stat-label mb-1">Signal Sources</p>
+          {market.sources.map((src, i) => (
+            <div
+              key={i}
+              className="flex items-center gap-2 text-xs rounded-lg px-3 py-2"
+              style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+            >
+              <span className="badge bg-blue">{src.source}</span>
+              <span className="truncate flex-1" style={{ color: 'var(--text-dim)' }}>{src.headline}</span>
+              <span
+                className="mono text-[10px]"
+                style={{ color: src.sentiment > 0 ? 'var(--green)' : src.sentiment < 0 ? 'var(--red)' : 'var(--text-muted)' }}
+              >
+                {src.sentiment > 0 ? '+' : ''}{src.sentiment?.toFixed(2)}
+              </span>
+            </div>
+          ))}
+        </div>
+      )}
+
+      <div className="mt-3 text-[10px]" style={{ color: 'var(--text-muted)' }}>
+        Signal created:{' '}
+        {market.created_at
+          ? new Date(market.created_at).toLocaleString('en-US', {
+              timeZone: 'America/Los_Angeles',
+              hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric',
+            })
+          : 'Unknown'}
+      </div>
+    </div>
+  );
+}
+
+// ── Main Orbit Visualization ──
+export default function Orbit() {
+  const [data, setData] = useState(null);
+  const [signals, setSignals] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [selectedTicker, setSelectedTicker] = useState(null);
+  const [filter, setFilter] = useState('all'); // 'all' | 'approved' | 'yes' | 'no'
+  const [animating, setAnimating] = useState(true);
+  const svgRef = useRef(null);
+
+  useEffect(() => {
+    load();
+    const i = setInterval(load, 30000);
+    return () => clearInterval(i);
+  }, []);
+
+  async function load() {
+    try {
+      const [d, s, sd] = await Promise.all([
+        api('/dashboard'),
+        api('/signals'),
+        api('/signals/dashboard'),
+      ]);
+      setData(d);
+      setSignals({ signals: s?.signals || [], dashboard: sd });
+    } catch {}
+    setLoading(false);
+  }
+
+  const { orbitalMarkets, stats } = useMemo(() => {
+    if (!signals?.signals) return { orbitalMarkets: [], stats: {} };
+
+    let items = signals.signals.map(s => {
+      const conf = parseFloat(s.confidence) || 0;
+      const edge = parseFloat(s.expected_edge) || 0;
+      const ring = getOrbitRing(conf);
+
+      const parts = (s.ticker || '').split('-');
+      const shortTitle = parts[0]?.replace('KX', '').substring(0, 12) || s.ticker;
+
+      return {
+        ...s,
+        shortTitle,
+        title: s.reasoning?.split('"')[1] || s.ticker,
+        ring,
+        conf,
+        edge,
+        edge_display: `${(edge * 100).toFixed(0)}%`,
+        size: Math.max(6, Math.min(22, edge * 120 + 6)),
+      };
+    });
+
+    // Apply filter
+    if (filter === 'approved') items = items.filter(i => i.risk_approved);
+    if (filter === 'yes') items = items.filter(i => i.direction === 'BUY_YES');
+    if (filter === 'no') items = items.filter(i => i.direction === 'BUY_NO');
+
+    // Limit to top 60 by edge
+    items.sort((a, b) => b.edge - a.edge);
+    items = items.slice(0, 60);
+
+    const approved = items.filter(i => i.risk_approved).length;
+    const buyYes = items.filter(i => i.direction === 'BUY_YES').length;
+    const buyNo = items.filter(i => i.direction === 'BUY_NO').length;
+    const avgConf = items.length > 0 ? items.reduce((s, i) => s + i.conf, 0) / items.length : 0;
+    const avgEdge = items.length > 0 ? items.reduce((s, i) => s + i.edge, 0) / items.length : 0;
+
+    return {
+      orbitalMarkets: items,
+      stats: { total: items.length, approved, buyYes, buyNo, avgConf, avgEdge },
+    };
+  }, [signals, filter]);
+
+  const selectedMarket = orbitalMarkets.find(m => m.ticker === selectedTicker);
+
+  if (loading) return (
+    <div className="flex items-center justify-center h-64">
+      <div
+        className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
+        style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+      />
+    </div>
+  );
+
+  const CX = 450;
+  const CY = 430;
+  const SVG_W = 900;
+  const SVG_H = 860;
+
+  // Position markets on orbit rings with angular distribution
+  const ringCounts = {};
+  orbitalMarkets.forEach(m => {
+    ringCounts[m.ring] = (ringCounts[m.ring] || 0) + 1;
+  });
+  const ringIndices = {};
+
+  const positionedMarkets = orbitalMarkets.map(m => {
+    const ring = ORBIT_RINGS[m.ring];
+    if (!ringIndices[m.ring]) ringIndices[m.ring] = 0;
+    const idx = ringIndices[m.ring]++;
+    const count = ringCounts[m.ring];
+
+    const angleStep = (2 * Math.PI) / count;
+    const angle = angleStep * idx + (m.ring * 0.4);
+    const jitter = (Math.sin(idx * 7.3) * 0.15);
+    const effectiveAngle = angle + jitter;
+    const radiusJitter = ring.radius + (Math.cos(idx * 3.7) * 15);
+
+    return {
+      ...m,
+      cx: CX + radiusJitter * Math.cos(effectiveAngle),
+      cy: CY + radiusJitter * Math.sin(effectiveAngle),
+    };
+  });
+
+  const filterOptions = [
+    { key: 'all', label: 'All' },
+    { key: 'approved', label: 'Approved' },
+    { key: 'yes', label: 'BUY YES' },
+    { key: 'no', label: 'BUY NO' },
+  ];
+
+  const summaryStats = [
+    { label: 'Active Signals', value: stats.total || 0,                           color: 'var(--cyan)' },
+    { label: 'Risk Approved',  value: stats.approved || 0,                         color: 'var(--green)' },
+    { label: 'BUY YES',        value: stats.buyYes || 0,                           color: 'var(--green)' },
+    { label: 'BUY NO',         value: stats.buyNo || 0,                            color: 'var(--red)' },
+    { label: 'Avg Confidence', value: `${((stats.avgConf || 0) * 100).toFixed(0)}%`, color: 'var(--blue)' },
+    { label: 'Avg Edge',       value: `${((stats.avgEdge || 0) * 100).toFixed(1)}%`, color: 'var(--purple)' },
+  ];
+
+  return (
+    <div className="space-y-6">
+      {/* Page Header */}
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="heading text-2xl font-bold flex items-center gap-2">
+            <span style={{ color: 'var(--cyan)' }}>&#9680;</span>
+            <span className="gradient-cyan">Signal Orbit</span>
+          </h1>
+          <p className="section-subtitle mt-1">
+            Radial visualization — distance from center = confidence, size = edge strength
+          </p>
+        </div>
+        <div className="flex items-center gap-3">
+          {/* Filter toggle group */}
+          <div
+            className="flex items-center gap-0.5 rounded-lg p-0.5"
+            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+          >
+            {filterOptions.map(f => (
+              <button
+                key={f.key}
+                onClick={() => setFilter(f.key)}
+                className="btn btn-sm"
+                style={filter === f.key
+                  ? { background: 'var(--cyan)', color: 'var(--bg)', fontWeight: 700 }
+                  : { background: 'transparent', color: 'var(--text-muted)' }
+                }
+              >
+                {f.label}
+              </button>
+            ))}
+          </div>
+
+          <label className="flex items-center gap-2 text-xs cursor-pointer" style={{ color: 'var(--text-dim)' }}>
+            <input
+              type="checkbox"
+              checked={animating}
+              onChange={e => setAnimating(e.target.checked)}
+              className="rounded"
+              style={{ accentColor: 'var(--cyan)' }}
+            />
+            Animate
+          </label>
+
+          <button onClick={load} className="btn btn-o btn-sm">Refresh</button>
+        </div>
+      </div>
+
+      {/* Stats Row */}
+      <div className="grid grid-cols-6 gap-3">
+        {summaryStats.map(s => (
+          <div key={s.label} className="card">
+            <p className="stat-label mb-1">{s.label}</p>
+            <p className="stat-v" style={{ color: s.color }}>{s.value}</p>
+          </div>
+        ))}
+      </div>
+
+      {/* Orbit SVG Visualization */}
+      <div
+        className="card p-0 overflow-hidden relative"
+        style={{ background: 'radial-gradient(ellipse at center, #0D1B2E 0%, #080D14 60%, var(--bg) 100%)' }}
+      >
+        <svg
+          ref={svgRef}
+          viewBox={`0 0 ${SVG_W} ${SVG_H}`}
+          className="w-full"
+          style={{ maxHeight: '75vh' }}
+        >
+          {/* Star field */}
+          <StarField width={SVG_W} height={SVG_H} />
+
+          {/* Orbit rings */}
+          {ORBIT_RINGS.map((ring, i) => (
+            <g key={i}>
+              <circle
+                cx={CX}
+                cy={CY}
+                r={ring.radius}
+                fill="none"
+                stroke={ring.color}
+                strokeWidth={0.8}
+                opacity={ring.opacity}
+                strokeDasharray="4 4"
+              />
+              <text
+                x={CX + ring.radius + 6}
+                y={CY - 4}
+                fill={ring.color}
+                fontSize={9}
+                opacity={ring.opacity + 0.2}
+                fontFamily="'JetBrains Mono', monospace"
+              >
+                {ring.label}
+              </text>
+            </g>
+          ))}
+
+          {/* Center sun — cyan glow */}
+          <defs>
+            <radialGradient id="sun-gradient">
+              <stop offset="0%"   stopColor="#00F0FF" stopOpacity="1" />
+              <stop offset="50%"  stopColor="#00B8C8" stopOpacity="0.8" />
+              <stop offset="100%" stopColor="#006070" stopOpacity="0.4" />
+            </radialGradient>
+            <filter id="sun-glow">
+              <feGaussianBlur stdDeviation="10" result="blur" />
+              <feMerge>
+                <feMergeNode in="blur" />
+                <feMergeNode in="SourceGraphic" />
+              </feMerge>
+            </filter>
+          </defs>
+
+          <circle cx={CX} cy={CY} r={40} fill="url(#sun-gradient)" filter="url(#sun-glow)" opacity={0.55} />
+          <circle cx={CX} cy={CY} r={25} fill="url(#sun-gradient)" />
+
+          {/* KEN label in center */}
+          <text
+            x={CX} y={CY - 4}
+            textAnchor="middle"
+            fill="#0E0E10"
+            fontSize={12}
+            fontWeight="bold"
+            fontFamily="'JetBrains Mono', monospace"
+          >
+            KEN
+          </text>
+          <text
+            x={CX} y={CY + 10}
+            textAnchor="middle"
+            fill="#0E0E10"
+            fontSize={7}
+            fontFamily="'JetBrains Mono', monospace"
+            opacity={0.7}
+          >
+            TRADER
+          </text>
+
+          {/* Connection lines from markets to center */}
+          {positionedMarkets.map(m => {
+            const dirColors = DIRECTION_COLORS[m.direction] || DIRECTION_COLORS.BUY_YES;
+            return (
+              <line
+                key={`line-${m.ticker}`}
+                x1={CX}
+                y1={CY}
+                x2={m.cx}
+                y2={m.cy}
+                stroke={dirColors.fill}
+                strokeWidth={0.4}
+                opacity={selectedTicker === m.ticker ? 0.6 : 0.1}
+              />
+            );
+          })}
+
+          {/* Market nodes */}
+          {positionedMarkets.map(m => (
+            <MarketNode
+              key={m.ticker}
+              market={m}
+              cx={m.cx}
+              cy={m.cy}
+              size={m.size}
+              isSelected={selectedTicker === m.ticker}
+              onSelect={market => setSelectedTicker(market.ticker === selectedTicker ? null : market.ticker)}
+              animate={animating && m.risk_approved}
+            />
+          ))}
+        </svg>
+
+        {/* Bottom legend overlay */}
+        <div
+          className="absolute bottom-3 left-3 rounded-lg p-3"
+          style={{
+            background: 'rgba(14,14,16,0.85)',
+            backdropFilter: 'blur(8px)',
+            border: '1px solid var(--border)',
+          }}
+        >
+          <div className="flex items-center gap-4 text-[10px]">
+            <div className="flex items-center gap-1.5">
+              <div className="w-3 h-3 rounded-full" style={{ backgroundColor: 'var(--green)' }} />
+              <span style={{ color: 'var(--text-dim)' }}>BUY YES</span>
+            </div>
+            <div className="flex items-center gap-1.5">
+              <div className="w-3 h-3 rounded-full" style={{ backgroundColor: 'var(--red)' }} />
+              <span style={{ color: 'var(--text-dim)' }}>BUY NO</span>
+            </div>
+            <span style={{ color: 'var(--border-light)' }}>|</span>
+            <span style={{ color: 'var(--text-muted)' }}>Closer to center = higher confidence</span>
+            <span style={{ color: 'var(--text-muted)' }}>Larger = bigger edge</span>
+          </div>
+        </div>
+
+        {/* Top-right ring legend */}
+        <div
+          className="absolute top-3 right-3 rounded-lg p-2.5"
+          style={{
+            background: 'rgba(14,14,16,0.85)',
+            backdropFilter: 'blur(8px)',
+            border: '1px solid var(--border)',
+          }}
+        >
+          <p className="stat-label mb-1.5">Orbit Rings</p>
+          <div className="space-y-1">
+            {ORBIT_RINGS.map((ring, i) => (
+              <div key={i} className="flex items-center gap-2 text-[10px]">
+                <div className="w-2 h-2 rounded-full" style={{ backgroundColor: ring.color, opacity: ring.opacity + 0.3 }} />
+                <span style={{ color: ring.color }}>{ring.label}</span>
+                <span style={{ color: 'var(--text-muted)' }}>{ring.description}</span>
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+
+      {/* Selected Market Detail */}
+      {selectedMarket && (
+        <OrbitDetail market={selectedMarket} onClose={() => setSelectedTicker(null)} />
+      )}
+
+      {/* Top Signals Table */}
+      <div className="card">
+        <div className="flex items-center gap-2 mb-4">
+          <h2 className="section-title">Orbital Signal Registry</h2>
+          <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
+            {orbitalMarkets.length} signals in orbit
+          </span>
+        </div>
+        <div className="overflow-x-auto">
+          <table>
+            <thead>
+              <tr>
+                <th>Ring</th>
+                <th>Ticker</th>
+                <th>Direction</th>
+                <th>Edge</th>
+                <th>Confidence</th>
+                <th>Risk</th>
+                <th>Sources</th>
+              </tr>
+            </thead>
+            <tbody>
+              {orbitalMarkets.slice(0, 20).map(m => {
+                const ring = ORBIT_RINGS[m.ring];
+                return (
+                  <tr
+                    key={m.ticker}
+                    onClick={() => setSelectedTicker(m.ticker === selectedTicker ? null : m.ticker)}
+                    className="cursor-pointer"
+                    style={selectedTicker === m.ticker ? { background: 'var(--cyan-glow)' } : {}}
+                  >
+                    <td>
+                      <span className="text-xs font-bold heading" style={{ color: ring.color }}>
+                        {ring.label}
+                      </span>
+                    </td>
+                    <td className="mono text-xs" style={{ color: 'var(--text-dim)' }}>{m.ticker}</td>
+                    <td>
+                      <span
+                        className="badge"
+                        style={
+                          m.direction === 'BUY_YES'
+                            ? { background: 'var(--green-dim)', color: 'var(--green)' }
+                            : { background: 'var(--red-dim)', color: 'var(--red)' }
+                        }
+                      >
+                        {m.direction}
+                      </span>
+                    </td>
+                    <td className="mono font-bold text-sm" style={{ color: 'var(--orange)' }}>
+                      {(m.edge * 100).toFixed(1)}%
+                    </td>
+                    <td className="mono text-sm" style={{ color: getSignalColor(m.conf) }}>
+                      {(m.conf * 100).toFixed(0)}%
+                    </td>
+                    <td>
+                      {m.risk_approved ? (
+                        <span className="text-xs font-bold" style={{ color: 'var(--green)' }}>OK</span>
+                      ) : (
+                        <span className="text-xs font-bold" style={{ color: 'var(--red)' }}>BLOCKED</span>
+                      )}
+                    </td>
+                    <td className="text-xs" style={{ color: 'var(--text-muted)' }}>
+                      {m.sources?.length || 0}
+                    </td>
+                  </tr>
+                );
+              })}
+            </tbody>
+          </table>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Orders.jsx b/kalshi-dash/src/pages/Orders.jsx
new file mode 100644
index 0000000..286c612
--- /dev/null
+++ b/kalshi-dash/src/pages/Orders.jsx
@@ -0,0 +1,302 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../api';
+
+// ── Status badge mapping ──
+function statusBadge(status) {
+  if (status === 'filled') return 'bg-green';
+  if (status === 'resting') return 'bg-orange';
+  if (status === 'canceled') return 'bg-red';
+  return 'bg-blue';
+}
+
+// ── Empty state ──
+function EmptyState({ label }) {
+  return (
+    <tr>
+      <td colSpan={20}>
+        <div className="flex flex-col items-center justify-center py-16 gap-3">
+          <svg
+            width="40"
+            height="40"
+            viewBox="0 0 24 24"
+            fill="none"
+            stroke="currentColor"
+            strokeWidth="1.5"
+            strokeLinecap="round"
+            strokeLinejoin="round"
+            style={{ color: 'var(--text-muted)' }}
+          >
+            <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2" />
+            <rect x="9" y="3" width="6" height="4" rx="1" />
+            <path d="M9 12h6M9 16h4" />
+          </svg>
+          <p style={{ color: 'var(--text-muted)', fontFamily: 'var(--font-body)', fontSize: '0.875rem' }}>
+            {label}
+          </p>
+        </div>
+      </td>
+    </tr>
+  );
+}
+
+// ── Spinner ──
+function Spinner() {
+  return (
+    <div className="flex items-center justify-center h-64">
+      <div
+        className="w-9 h-9 rounded-full border-2 animate-spin"
+        style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }}
+      />
+    </div>
+  );
+}
+
+export default function Orders() {
+  const [orders, setOrders] = useState([]);
+  const [fills, setFills] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [tab, setTab] = useState('orders');
+
+  useEffect(() => { load(); }, []);
+
+  async function load() {
+    setLoading(true);
+    try {
+      const [o, f] = await Promise.all([
+        post('/kalshi', { action: 'get_orders' }),
+        post('/kalshi', { action: 'get_fills' }),
+      ]);
+      setOrders(o.orders || []);
+      setFills(f.fills || []);
+    } catch {}
+    setLoading(false);
+  }
+
+  async function cancelOrder(orderId) {
+    try {
+      await post('/kalshi', { action: 'cancel_order', order_id: orderId });
+      load();
+    } catch {}
+  }
+
+  if (loading) return <Spinner />;
+
+  return (
+    <div className="space-y-6 fade-in">
+
+      {/* ── Page Header ── */}
+      <div className="flex items-center justify-between">
+        <div className="flex items-center gap-3">
+          {/* Arrows icon */}
+          <div
+            className="flex items-center justify-center w-10 h-10 rounded-xl"
+            style={{ background: 'var(--cyan-dim)', color: 'var(--cyan)' }}
+          >
+            <svg
+              width="20"
+              height="20"
+              viewBox="0 0 24 24"
+              fill="none"
+              stroke="currentColor"
+              strokeWidth="2"
+              strokeLinecap="round"
+              strokeLinejoin="round"
+            >
+              <path d="M7 16V4m0 0L3 8m4-4l4 4" />
+              <path d="M17 8v12m0 0l4-4m-4 4l-4-4" />
+            </svg>
+          </div>
+          <div>
+            <h1 className="heading text-3xl" style={{ color: 'var(--text)' }}>Orders</h1>
+            <p className="section-subtitle mt-0.5">
+              {orders.length} orders &bull; {fills.length} fills
+            </p>
+          </div>
+        </div>
+        <button onClick={load} className="btn btn-o btn-sm">Refresh</button>
+      </div>
+
+      {/* ── Tab Switcher ── */}
+      <div
+        className="flex rounded-xl p-1 gap-1 w-fit"
+        style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+      >
+        <button
+          onClick={() => setTab('orders')}
+          className="btn btn-sm"
+          style={
+            tab === 'orders'
+              ? { background: 'var(--cyan)', color: 'var(--bg)' }
+              : { background: 'transparent', color: 'var(--text-muted)', border: 'none' }
+          }
+        >
+          Orders
+          <span
+            className="badge ml-1"
+            style={
+              tab === 'orders'
+                ? { background: 'rgba(0,0,0,0.2)', color: 'var(--bg)', fontSize: '0.65rem' }
+                : { background: 'var(--surface)', color: 'var(--text-muted)', fontSize: '0.65rem' }
+            }
+          >
+            {orders.length}
+          </span>
+        </button>
+        <button
+          onClick={() => setTab('fills')}
+          className="btn btn-sm"
+          style={
+            tab === 'fills'
+              ? { background: 'var(--cyan)', color: 'var(--bg)' }
+              : { background: 'transparent', color: 'var(--text-muted)', border: 'none' }
+          }
+        >
+          Fills
+          <span
+            className="badge ml-1"
+            style={
+              tab === 'fills'
+                ? { background: 'rgba(0,0,0,0.2)', color: 'var(--bg)', fontSize: '0.65rem' }
+                : { background: 'var(--surface)', color: 'var(--text-muted)', fontSize: '0.65rem' }
+            }
+          >
+            {fills.length}
+          </span>
+        </button>
+      </div>
+
+      {/* ── Orders Table ── */}
+      {tab === 'orders' && (
+        <div className="card overflow-x-auto" style={{ padding: 0 }}>
+          <table>
+            <thead>
+              <tr>
+                <th>Ticker</th>
+                <th>Side</th>
+                <th>Action</th>
+                <th>Type</th>
+                <th>Price</th>
+                <th>Qty</th>
+                <th>Filled</th>
+                <th>Status</th>
+                <th>Action</th>
+              </tr>
+            </thead>
+            <tbody>
+              {orders.length === 0 ? (
+                <EmptyState label="No orders yet" />
+              ) : (
+                orders.map((o, i) => (
+                  <tr key={i}>
+                    <td>
+                      <a
+                        href={`https://kalshi.com/search?query=${encodeURIComponent((o.ticker || '').split('-')[0])}`}
+                        target="_blank"
+                        rel="noopener noreferrer"
+                        className="mono text-xs hover:underline transition"
+                        style={{ color: 'var(--orange)' }}
+                      >
+                        {o.ticker}
+                      </a>
+                    </td>
+                    <td>
+                      <span className={`badge ${o.side === 'yes' ? 'bg-green' : 'bg-red'}`}>
+                        {(o.side || '').toUpperCase()}
+                      </span>
+                    </td>
+                    <td className="text-xs" style={{ color: 'var(--text-dim)' }}>
+                      {(o.action || '').toUpperCase()}
+                    </td>
+                    <td className="text-xs" style={{ color: 'var(--text-muted)' }}>{o.type}</td>
+                    <td className="mono" style={{ color: 'var(--cyan)' }}>
+                      {o.yes_price || o.no_price || '—'}¢
+                    </td>
+                    <td className="mono">{o.count || o.remaining_count || 0}</td>
+                    <td className="mono text-green">
+                      {o.count - (o.remaining_count || 0)}
+                    </td>
+                    <td>
+                      <span className={`badge ${statusBadge(o.status)}`}>{o.status}</span>
+                    </td>
+                    <td>
+                      {o.status === 'resting' && (
+                        <button
+                          onClick={() => cancelOrder(o.order_id)}
+                          className="btn btn-d btn-sm"
+                        >
+                          Cancel
+                        </button>
+                      )}
+                    </td>
+                  </tr>
+                ))
+              )}
+            </tbody>
+          </table>
+        </div>
+      )}
+
+      {/* ── Fills Table ── */}
+      {tab === 'fills' && (
+        <div className="card overflow-x-auto" style={{ padding: 0 }}>
+          <table>
+            <thead>
+              <tr>
+                <th>Ticker</th>
+                <th>Side</th>
+                <th>Action</th>
+                <th>Price</th>
+                <th>Qty</th>
+                <th>Maker/Taker</th>
+                <th>Time</th>
+              </tr>
+            </thead>
+            <tbody>
+              {fills.length === 0 ? (
+                <EmptyState label="No fills yet" />
+              ) : (
+                fills.map((f, i) => (
+                  <tr key={i}>
+                    <td>
+                      <a
+                        href={`https://kalshi.com/search?query=${encodeURIComponent((f.ticker || '').split('-')[0])}`}
+                        target="_blank"
+                        rel="noopener noreferrer"
+                        className="mono text-xs hover:underline transition"
+                        style={{ color: 'var(--orange)' }}
+                      >
+                        {f.ticker}
+                      </a>
+                    </td>
+                    <td>
+                      <span className={`badge ${f.side === 'yes' ? 'bg-green' : 'bg-red'}`}>
+                        {(f.side || '').toUpperCase()}
+                      </span>
+                    </td>
+                    <td className="text-xs" style={{ color: 'var(--text-dim)' }}>
+                      {(f.action || '').toUpperCase()}
+                    </td>
+                    <td className="mono" style={{ color: 'var(--cyan)' }}>
+                      {f.yes_price || f.no_price || '—'}¢
+                    </td>
+                    <td className="mono">{f.count}</td>
+                    <td>
+                      {f.is_taker ? (
+                        <span className="badge bg-orange">TAKER</span>
+                      ) : (
+                        <span className="badge bg-green">MAKER</span>
+                      )}
+                    </td>
+                    <td className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
+                      {f.created_time ? new Date(f.created_time).toLocaleString() : '—'}
+                    </td>
+                  </tr>
+                ))
+              )}
+            </tbody>
+          </table>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Positions.jsx b/kalshi-dash/src/pages/Positions.jsx
new file mode 100644
index 0000000..6d7750b
--- /dev/null
+++ b/kalshi-dash/src/pages/Positions.jsx
@@ -0,0 +1,969 @@
+import React, { useState, useEffect, useCallback } from 'react';
+import { api, post } from '../api';
+import MiniCandlestick, { genCandles } from '../components/MiniCandlestick';
+import {
+  BarChart, Bar, Cell, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer,
+} from 'recharts';
+
+// ── Helpers ──
+const fmt = (cents) => `$${(cents / 100).toFixed(2)}`;
+const fmtPnl = (cents) => {
+  const val = cents / 100;
+  const prefix = val >= 0 ? '+' : '';
+  return `${prefix}$${val.toFixed(2)}`;
+};
+const pnlColor = (cents) => cents >= 0 ? 'text-green' : 'text-red';
+const sideBadge = (dir) => dir === 'BUY_YES' ? 'bg-green' : 'bg-red';
+const sideLabel = (dir) => dir === 'BUY_YES' ? 'YES' : 'NO';
+const fmtCents = (cents) => `${cents}¢`;
+const calcExitPrice = (entryPriceCents, pnlCents, contracts) => {
+  if (!contracts || contracts === 0) return null;
+  return Math.round(entryPriceCents + (pnlCents / contracts));
+};
+const kalshiLink = (ticker) =>
+  `https://kalshi.com/search?query=${encodeURIComponent((ticker || '').split('-')[0])}`;
+const timeAgo = (d) => {
+  if (!d) return '';
+  const ms = Date.now() - new Date(d).getTime();
+  if (ms < 60000) return 'just now';
+  if (ms < 3600000) return `${Math.floor(ms / 60000)}m ago`;
+  if (ms < 86400000) return `${Math.floor(ms / 3600000)}h ago`;
+  return `${Math.floor(ms / 86400000)}d ago`;
+};
+
+/* ── Chart 6: P&L Distribution Histogram ── */
+function PnlDistributionChart({ positions }) {
+  if (!positions || !positions.length) return null;
+
+  /* Define buckets */
+  const BUCKETS = [
+    { label: '<-$50',    min: -Infinity,  max: -5000, color: '#FF5C5C' },
+    { label: '-$20-50',  min: -5000,      max: -2000, color: '#FF7A7A' },
+    { label: '-$1-20',   min: -2000,      max: -100,  color: '#FF9999' },
+    { label: '$0',       min: -100,       max: 100,   color: '#6B7280' },
+    { label: '$1-20',    min: 100,        max: 2000,  color: '#88DDBB' },
+    { label: '$20-50',   min: 2000,       max: 5000,  color: '#44CC99' },
+    { label: '>$50',     min: 5000,       max: Infinity, color: '#00C389' },
+  ];
+
+  const bucketed = BUCKETS.map(b => {
+    const count = positions.filter(p => {
+      const v = p.realized_pnl_cents || 0;
+      return v > b.min && v <= b.max;
+    }).length;
+    return { ...b, count };
+  }).filter(b => b.count > 0);
+
+  if (!bucketed.length) return null;
+
+  const CustomTooltip = ({ active, payload, label }) => {
+    if (!active || !payload?.length) return null;
+    const d = payload[0]?.payload || {};
+    return (
+      <div style={{
+        background: '#1C1C24', border: '1px solid #2A2A35', borderRadius: 8,
+        padding: '8px 12px', fontSize: 12, fontFamily: "'Inter', sans-serif",
+      }}>
+        <p style={{ color: '#E0E0E0', fontWeight: 700, marginBottom: 4 }}>{label}</p>
+        <p style={{ color: d.color, fontFamily: "'JetBrains Mono', monospace", fontWeight: 700 }}>
+          {d.count} position{d.count !== 1 ? 's' : ''}
+        </p>
+      </div>
+    );
+  };
+
+  return (
+    <div className="card">
+      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
+        <h2 className="section-title" style={{ fontSize: '1.05rem' }}>P&L Distribution</h2>
+        <span style={{ fontSize: 11, color: '#6B7280' }}>{positions.length} positions total</span>
+      </div>
+      <ResponsiveContainer width="100%" height={180}>
+        <BarChart data={bucketed} margin={{ top: 4, right: 12, left: 0, bottom: 4 }}>
+          <CartesianGrid
+            strokeDasharray="3 3"
+            stroke="#2A2A35"
+            strokeOpacity={0.6}
+            vertical={false}
+          />
+          <XAxis
+            dataKey="label"
+            stroke="#2A2A35"
+            tick={{ fill: '#6B7280', fontSize: 10, fontFamily: "'Inter', sans-serif" }}
+            tickLine={false}
+            axisLine={{ stroke: '#2A2A35' }}
+          />
+          <YAxis
+            stroke="#2A2A35"
+            tick={{ fill: '#6B7280', fontSize: 11, fontFamily: "'Inter', sans-serif" }}
+            tickLine={false}
+            axisLine={false}
+            allowDecimals={false}
+            width={30}
+          />
+          <Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(0,240,255,0.04)' }} />
+          <Bar dataKey="count" radius={[4, 4, 0, 0]} maxBarSize={48}>
+            {bucketed.map((entry, i) => (
+              <Cell key={i} fill={entry.color} fillOpacity={0.85} />
+            ))}
+          </Bar>
+        </BarChart>
+      </ResponsiveContainer>
+    </div>
+  );
+}
+
+// ── Loading Spinner ──
+function Spinner() {
+  return (
+    <div className="flex items-center justify-center h-64">
+      <div
+        className="w-9 h-9 rounded-full border-2 animate-spin"
+        style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }}
+      />
+    </div>
+  );
+}
+
+// ── Trade Sub-table ──
+function TradeSubTable({ p, trades, loading }) {
+  return (
+    <div style={{ background: 'var(--bg)', borderTop: '1px solid var(--border)', borderBottom: '1px solid var(--border)' }} className="px-4 py-3">
+      <div className="flex items-center justify-between mb-3">
+        <span className="mono text-xs font-bold" style={{ color: 'var(--cyan)' }}>
+          Trade History — {p.ticker}
+        </span>
+        <div className="flex gap-4 text-xs" style={{ color: 'var(--text-muted)' }}>
+          <span>Open: <b style={{ color: 'var(--blue)' }}>{p.open_trades}</b></span>
+          <span>Closed: <b style={{ color: 'var(--text-dim)' }}>{p.closed_trades}</b></span>
+          <span>
+            Entry Range:{' '}
+            <b style={{ color: 'var(--orange)' }}>
+              {p.min_entry}¢ – {p.max_entry}¢
+            </b>
+          </span>
+          <span>First: <b>{new Date(p.first_trade_at).toLocaleDateString()}</b></span>
+        </div>
+      </div>
+      {loading ? (
+        <div className="flex justify-center py-4">
+          <div className="w-5 h-5 rounded-full border-2 animate-spin" style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }} />
+        </div>
+      ) : trades.length > 0 ? (
+        <div className="overflow-x-auto" style={{ maxHeight: 200, overflowY: 'auto' }}>
+          <table className="text-xs">
+            <thead>
+              <tr>
+                <th>Time</th>
+                <th>Direction</th>
+                <th>Contracts</th>
+                <th>Entry</th>
+                <th>Exit</th>
+                <th>Cost</th>
+                <th>Profit</th>
+                <th>Status</th>
+              </tr>
+            </thead>
+            <tbody>
+              {trades.map((t, i) => {
+                const entryC = parseInt(t.entry_price_cents) || 0;
+                const pnlC = parseInt(t.pnl_cents) || 0;
+                const ctrs = parseInt(t.contracts) || 0;
+                const isClosed = t.status === 'closed' || t.status === 'expired';
+                const exitPrice = isClosed && ctrs > 0 ? calcExitPrice(entryC, pnlC, ctrs) : null;
+                return (
+                  <tr key={i}>
+                    <td className="mono" style={{ color: 'var(--text-muted)' }}>
+                      {new Date(t.created_at).toLocaleString()}
+                    </td>
+                    <td>
+                      <span className={`badge ${sideBadge(t.direction)}`}>{sideLabel(t.direction)}</span>
+                    </td>
+                    <td className="mono">{t.contracts}</td>
+                    <td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
+                      {entryC}¢
+                    </td>
+                    <td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace", color: exitPrice != null ? 'var(--text-dim)' : 'var(--text-muted)' }}>
+                      {exitPrice != null ? `${exitPrice}¢` : '—'}
+                    </td>
+                    <td className="mono" style={{ color: 'var(--text-dim)' }}>{fmt(t.cost_cents || 0)}</td>
+                    <td className="mono" style={{
+                      fontFamily: "'JetBrains Mono', monospace",
+                      fontWeight: 700,
+                      color: pnlC > 0 ? 'var(--green)' : pnlC < 0 ? 'var(--red)' : 'var(--text-muted)',
+                    }}>
+                      {t.pnl_cents != null && isClosed ? fmtPnl(pnlC) : '—'}
+                    </td>
+                    <td>
+                      <span className={`badge ${t.status === 'open' ? 'bg-blue' : t.status === 'closed' ? 'bg-green' : 'bg-orange'}`}>
+                        {t.status}
+                      </span>
+                    </td>
+                  </tr>
+                );
+              })}
+            </tbody>
+          </table>
+        </div>
+      ) : (
+        <p className="text-center py-3 text-xs" style={{ color: 'var(--text-muted)' }}>
+          No trade details available
+        </p>
+      )}
+    </div>
+  );
+}
+
+export default function Positions() {
+  const [positions, setPositions] = useState([]);
+  const [grouped, setGrouped] = useState([]);
+  const [summary, setSummary] = useState({});
+  const [loading, setLoading] = useState(true);
+  const [expandedGroup, setExpandedGroup] = useState(null);
+  const [expandedPos, setExpandedPos] = useState(null);
+  const [trades, setTrades] = useState({});
+  const [tradesLoading, setTradesLoading] = useState(null);
+  const [viewMode, setViewMode] = useState('grouped'); // grouped | flat | pnl
+  const [sortBy, setSortBy] = useState('cost'); // cost | pnl | contracts
+
+  const load = useCallback(async () => {
+    try {
+      const r = await api('/positions');
+      if (r.success) {
+        setPositions(r.positions || []);
+        setGrouped(r.grouped || []);
+        setSummary(r.summary || {});
+      }
+    } catch {}
+    setLoading(false);
+  }, []);
+
+  useEffect(() => {
+    load();
+    const i = setInterval(load, 30000);
+    return () => clearInterval(i);
+  }, [load]);
+
+  async function loadTrades(ticker, direction) {
+    const key = `${ticker}_${direction}`;
+    if (trades[key]) return;
+    setTradesLoading(key);
+    try {
+      const r = await post('/positions/trades', { ticker, direction, limit: 50 });
+      if (r.success) {
+        setTrades(prev => ({ ...prev, [key]: r.trades || [] }));
+      }
+    } catch {}
+    setTradesLoading(null);
+  }
+
+  function toggleGroup(event) {
+    setExpandedGroup(prev => (prev === event ? null : event));
+    setExpandedPos(null);
+  }
+
+  function togglePos(ticker, direction) {
+    const key = `${ticker}_${direction}`;
+    if (expandedPos === key) {
+      setExpandedPos(null);
+    } else {
+      setExpandedPos(key);
+      loadTrades(ticker, direction);
+    }
+  }
+
+  const sortedPositions = [...positions].sort((a, b) => {
+    if (sortBy === 'pnl') return b.realized_pnl_cents - a.realized_pnl_cents;
+    if (sortBy === 'contracts') return b.open_contracts - a.open_contracts;
+    return b.open_cost_cents - a.open_cost_cents;
+  });
+
+  const winners = positions
+    .filter(p => p.realized_pnl_cents > 0)
+    .sort((a, b) => b.realized_pnl_cents - a.realized_pnl_cents);
+  const losers = positions
+    .filter(p => p.realized_pnl_cents < 0)
+    .sort((a, b) => a.realized_pnl_cents - b.realized_pnl_cents);
+  const flat = positions.filter(p => p.realized_pnl_cents === 0);
+
+  const winRate =
+    positions.length > 0 && winners.length + losers.length > 0
+      ? `${Math.round((winners.length / (winners.length + losers.length)) * 100)}%`
+      : '—';
+
+  if (loading) return <Spinner />;
+
+  return (
+    <div className="space-y-6 fade-in">
+
+      {/* ── Page Header ── */}
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="heading text-3xl" style={{ color: 'var(--text)' }}>
+            Positions
+            <span className="badge bg-cyan ml-3 align-middle text-base">
+              {summary.positionCount || 0}
+            </span>
+          </h1>
+          <p className="section-subtitle mt-1">
+            {summary.positionCount || 0} active positions &bull;{' '}
+            {summary.totalTrades?.toLocaleString() || 0} total trades
+          </p>
+        </div>
+        <div className="flex items-center gap-2">
+          {/* View mode toggle */}
+          <div
+            className="flex rounded-lg p-0.5 gap-0.5"
+            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+          >
+            {[['grouped', 'Grouped'], ['flat', 'All'], ['pnl', 'P&L']].map(([v, label]) => (
+              <button
+                key={v}
+                onClick={() => setViewMode(v)}
+                className="px-3 py-1 text-xs font-semibold rounded-md transition"
+                style={
+                  viewMode === v
+                    ? { background: 'var(--cyan)', color: 'var(--bg)', fontFamily: 'var(--font-body)' }
+                    : { color: 'var(--text-muted)', fontFamily: 'var(--font-body)' }
+                }
+              >
+                {label}
+              </button>
+            ))}
+          </div>
+          <button onClick={load} className="btn btn-o btn-sm">Refresh</button>
+        </div>
+      </div>
+
+      {/* ── Summary Stat Cards ── */}
+      <div className="grid grid-cols-2 md:grid-cols-5 gap-3">
+        <div className="card card-accent-cyan">
+          <p className="stat-label">Positions</p>
+          <p className="stat-v mono mt-2" style={{ color: 'var(--cyan)' }}>{summary.positionCount || 0}</p>
+        </div>
+        <div className="card card-accent-orange">
+          <p className="stat-label">Total Cost</p>
+          <p className="stat-v mono mt-2" style={{ color: 'var(--text)' }}>{fmt(summary.totalCost || 0)}</p>
+        </div>
+        <div className={`card ${(summary.totalPnl || 0) >= 0 ? 'card-accent-green' : 'card-accent-red'}`}>
+          <p className="stat-label">Realized P&L</p>
+          <p className={`stat-v mono mt-2 ${(summary.totalPnl || 0) >= 0 ? 'pnl-positive' : 'pnl-negative'}`}>
+            {fmtPnl(summary.totalPnl || 0)}
+          </p>
+        </div>
+        <div className="card card-accent-blue">
+          <p className="stat-label">Contracts</p>
+          <p className="stat-v mono mt-2" style={{ color: 'var(--blue)' }}>
+            {(summary.totalContracts || 0).toLocaleString()}
+          </p>
+        </div>
+        <div className="card card-accent-green">
+          <p className="stat-label">Win Rate</p>
+          <p className="stat-v mono mt-2 text-green">{winRate}</p>
+        </div>
+      </div>
+
+      {/* ── P&L Breakdown Bar ── */}
+      {summary.totalPnl !== 0 && (
+        <div className="card py-3">
+          <div className="flex items-center justify-between text-xs mb-2">
+            <span className="font-semibold text-green">{winners.length} winners</span>
+            <span style={{ color: 'var(--text-muted)' }}>{flat.length} flat</span>
+            <span className="font-semibold text-red">{losers.length} losers</span>
+          </div>
+          <div className="progress-bar" style={{ height: 8 }}>
+            {(() => {
+              const totalAbs =
+                winners.reduce((s, p) => s + p.realized_pnl_cents, 0) +
+                Math.abs(losers.reduce((s, p) => s + p.realized_pnl_cents, 0));
+              if (!totalAbs) return null;
+              const winPct = (winners.reduce((s, p) => s + p.realized_pnl_cents, 0) / totalAbs) * 100;
+              return (
+                <div className="flex h-full w-full">
+                  <div className="bar-win-glow" style={{ width: `${winPct}%`, background: 'var(--green)', borderRadius: '2px 0 0 2px' }} />
+                  <div className="bar-loss-glow" style={{ width: `${100 - winPct}%`, background: 'var(--red)', borderRadius: '0 2px 2px 0' }} />
+                </div>
+              );
+            })()}
+          </div>
+        </div>
+      )}
+
+      {/* ── Chart 6: P&L Distribution Histogram ── */}
+      <PnlDistributionChart positions={positions} />
+
+      {/* ══════════════════════════════
+          GROUPED VIEW
+          ══════════════════════════════ */}
+      {viewMode === 'grouped' && (
+        <div className="space-y-3">
+          {grouped.map((group) => {
+            const isExpanded = expandedGroup === group.event;
+            return (
+              <div
+                key={group.event}
+                className="card p-0 overflow-hidden"
+                style={{ padding: 0 }}
+              >
+                {/* Group Header */}
+                <button
+                  onClick={() => toggleGroup(group.event)}
+                  className="w-full flex items-center gap-3 p-4 text-left transition"
+                  style={{ background: 'transparent' }}
+                  onMouseEnter={e => (e.currentTarget.style.background = 'var(--cyan-glow)')}
+                  onMouseLeave={e => (e.currentTarget.style.background = 'transparent')}
+                >
+                  <span
+                    className="text-xs inline-block transition-transform"
+                    style={{
+                      color: 'var(--cyan)',
+                      transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
+                    }}
+                  >
+                    ▶
+                  </span>
+                  <div className="flex-1 min-w-0">
+                    <div className="flex items-center gap-2">
+                      <span className="mono text-sm font-bold" style={{ color: 'var(--orange)' }}>
+                        {group.event}
+                      </span>
+                      <span className="badge bg-cyan">{group.positions.length}</span>
+                    </div>
+                    <p className="text-xs truncate mt-0.5" style={{ color: 'var(--text-muted)' }}>
+                      {group.title}
+                    </p>
+                  </div>
+                  <div className="flex items-center gap-6 text-right flex-shrink-0">
+                    <div>
+                      <p className="text-xs" style={{ color: 'var(--text-muted)' }}>Cost</p>
+                      <p className="mono text-sm" style={{ color: 'var(--text)' }}>{fmt(group.totalCost)}</p>
+                    </div>
+                    <div>
+                      <p className="text-xs" style={{ color: 'var(--text-muted)' }}>Contracts</p>
+                      <p className="mono text-sm" style={{ color: 'var(--blue)' }}>
+                        {group.totalContracts.toLocaleString()}
+                      </p>
+                    </div>
+                    <div>
+                      <p className="text-xs" style={{ color: 'var(--text-muted)' }}>Realized P&L</p>
+                      <p className={`mono text-sm font-bold ${pnlColor(group.totalPnl)}`}>
+                        {fmtPnl(group.totalPnl)}
+                      </p>
+                    </div>
+                  </div>
+                </button>
+
+                {/* Expanded Positions Table */}
+                {isExpanded && (
+                  <div style={{ borderTop: '1px solid var(--border)' }}>
+                    <table>
+                      <thead>
+                        <tr>
+                          <th></th>
+                          <th>Ticker</th>
+                          <th>Side</th>
+                          <th>Contracts</th>
+                          <th>Bought At</th>
+                          <th>Sold At</th>
+                          <th>Cost</th>
+                          <th>Profit</th>
+                          <th>Trades</th>
+                          <th>Last Trade</th>
+                        </tr>
+                      </thead>
+                      <tbody>
+                        {group.positions.map((p) => {
+                          const posKey = `${p.ticker}_${p.direction}`;
+                          const isPosExpanded = expandedPos === posKey;
+                          const posTrades = trades[posKey] || [];
+                          const isLoadingTrades = tradesLoading === posKey;
+                          const isWinning = (p.realized_pnl_cents || 0) > 0;
+                          const isLosing  = (p.realized_pnl_cents || 0) < 0;
+                          // Deterministic seed from ticker string
+                          const tickerSeed = p.ticker.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
+                          const candleTrend = isWinning ? 'up' : isLosing ? 'down' : 'flat';
+                          const candleData = genCandles(tickerSeed, 9, candleTrend);
+                          const rowClass = isWinning ? 'cursor-pointer position-winning' : isLosing ? 'cursor-pointer position-losing' : 'cursor-pointer';
+                          return (
+                            <React.Fragment key={posKey}>
+                              <tr
+                                className={rowClass}
+                                onClick={() => togglePos(p.ticker, p.direction)}
+                                style={{ position: 'relative' }}
+                              >
+                                <td style={{ width: 24 }}>
+                                  <span
+                                    className="text-xs inline-block transition-transform"
+                                    style={{
+                                      color: 'var(--cyan)',
+                                      transform: isPosExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
+                                    }}
+                                  >
+                                    ▶
+                                  </span>
+                                </td>
+                                <td>
+                                  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+                                    <a
+                                      href={kalshiLink(p.ticker)}
+                                      target="_blank"
+                                      rel="noopener noreferrer"
+                                      className="mono text-sm hover:underline"
+                                      style={{ color: 'var(--orange)' }}
+                                      onClick={e => e.stopPropagation()}
+                                    >
+                                      {p.ticker}
+                                    </a>
+                                    <MiniCandlestick data={candleData} width={72} height={28} />
+                                  </div>
+                                </td>
+                                <td>
+                                  <span className={`badge ${sideBadge(p.direction)}`}>
+                                    {sideLabel(p.direction)}
+                                  </span>
+                                </td>
+                                <td className="mono">{p.open_contracts?.toLocaleString()}</td>
+                                <td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
+                                  {p.avg_entry_price}¢
+                                </td>
+                                <td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
+                                  {(() => {
+                                    const closedCount = parseInt(p.closed_trades) || 0;
+                                    const openCount = parseInt(p.open_trades) || 0;
+                                    if (closedCount > 0 && openCount === 0) {
+                                      const exit = calcExitPrice(p.avg_entry_price, p.realized_pnl_cents, parseInt(p.open_contracts) || parseInt(p.total_trades) || 1);
+                                      return <span style={{ color: 'var(--text-dim)' }}>{exit}¢</span>;
+                                    }
+                                    return <span className="badge bg-blue" style={{ fontSize: '0.65rem' }}>Open</span>;
+                                  })()}
+                                </td>
+                                <td className="mono" style={{ color: 'var(--text)' }}>
+                                  {fmt(p.open_cost_cents)}
+                                </td>
+                                <td className="mono" style={{
+                                  fontFamily: "'JetBrains Mono', monospace",
+                                  fontWeight: 700,
+                                  color: p.realized_pnl_cents > 0 ? 'var(--green)' : p.realized_pnl_cents < 0 ? 'var(--red)' : 'var(--text-muted)',
+                                }}>
+                                  {p.realized_pnl_cents !== 0 ? fmtPnl(p.realized_pnl_cents) : '—'}
+                                </td>
+                                <td className="mono" style={{ color: 'var(--text-muted)' }}>
+                                  {p.total_trades?.toLocaleString()}
+                                </td>
+                                <td className="text-xs" style={{ color: 'var(--text-muted)' }}>
+                                  {timeAgo(p.last_trade_at)}
+                                </td>
+                              </tr>
+                              {isPosExpanded && (
+                                <tr>
+                                  <td colSpan={10} style={{ padding: 0 }}>
+                                    <TradeSubTable
+                                      p={p}
+                                      trades={posTrades}
+                                      loading={isLoadingTrades}
+                                    />
+                                  </td>
+                                </tr>
+                              )}
+                            </React.Fragment>
+                          );
+                        })}
+                      </tbody>
+                    </table>
+                  </div>
+                )}
+              </div>
+            );
+          })}
+        </div>
+      )}
+
+      {/* ══════════════════════════════
+          FLAT VIEW
+          ══════════════════════════════ */}
+      {viewMode === 'flat' && (
+        <div className="card overflow-x-auto" style={{ padding: 0 }}>
+          {/* Sort controls */}
+          <div
+            className="flex items-center gap-3 px-4 py-3"
+            style={{ borderBottom: '1px solid var(--border)' }}
+          >
+            <span className="text-xs" style={{ color: 'var(--text-muted)' }}>Sort by:</span>
+            {[['cost', 'Cost'], ['pnl', 'P&L'], ['contracts', 'Contracts']].map(([v, label]) => (
+              <button
+                key={v}
+                onClick={() => setSortBy(v)}
+                className="text-xs font-semibold px-2.5 py-1 rounded-md transition"
+                style={
+                  sortBy === v
+                    ? { background: 'var(--cyan-dim)', color: 'var(--cyan)' }
+                    : { color: 'var(--text-muted)' }
+                }
+              >
+                {label}
+              </button>
+            ))}
+          </div>
+          <table>
+            <thead>
+              <tr>
+                <th>#</th>
+                <th>Ticker</th>
+                <th>Market</th>
+                <th>Side</th>
+                <th>Contracts</th>
+                <th>Bought At</th>
+                <th>Sold At</th>
+                <th>Cost</th>
+                <th>Profit</th>
+                <th>Trades</th>
+                <th>Last Trade</th>
+              </tr>
+            </thead>
+            <tbody>
+              {sortedPositions.length === 0 ? (
+                <tr>
+                  <td colSpan={11} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
+                    No open positions
+                  </td>
+                </tr>
+              ) : (
+                sortedPositions.map((p, i) => {
+                  const posKey = `${p.ticker}_${p.direction}`;
+                  const isPosExpanded = expandedPos === posKey;
+                  const posTrades = trades[posKey] || [];
+                  const isLoadingTrades = tradesLoading === posKey;
+                  const isWinningFlat = (p.realized_pnl_cents || 0) > 0;
+                  const isLosingFlat  = (p.realized_pnl_cents || 0) < 0;
+                  const tickerSeedFlat = p.ticker.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
+                  const candleTrendFlat = isWinningFlat ? 'up' : isLosingFlat ? 'down' : 'flat';
+                  const candleDataFlat = genCandles(tickerSeedFlat + 100, 9, candleTrendFlat);
+                  const flatRowClass = isWinningFlat ? 'cursor-pointer position-winning' : isLosingFlat ? 'cursor-pointer position-losing' : 'cursor-pointer';
+                  return (
+                    <React.Fragment key={posKey}>
+                      <tr
+                        className={flatRowClass}
+                        onClick={() => togglePos(p.ticker, p.direction)}
+                        style={{ position: 'relative' }}
+                      >
+                        <td style={{ color: 'var(--text-muted)' }}>{i + 1}</td>
+                        <td>
+                          <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+                            <a
+                              href={kalshiLink(p.ticker)}
+                              target="_blank"
+                              rel="noopener noreferrer"
+                              className="mono text-sm hover:underline"
+                              style={{ color: 'var(--orange)' }}
+                              onClick={e => e.stopPropagation()}
+                            >
+                              {p.ticker}
+                            </a>
+                            <MiniCandlestick data={candleDataFlat} width={72} height={28} />
+                          </div>
+                        </td>
+                        <td
+                          className="text-xs max-w-[192px] truncate"
+                          style={{ color: 'var(--text-dim)' }}
+                        >
+                          {p.title}
+                        </td>
+                        <td>
+                          <span className={`badge ${sideBadge(p.direction)}`}>
+                            {sideLabel(p.direction)}
+                          </span>
+                        </td>
+                        <td className="mono">{p.open_contracts?.toLocaleString()}</td>
+                        <td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
+                          {p.avg_entry_price}¢
+                        </td>
+                        <td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
+                          {(() => {
+                            const closedCount = parseInt(p.closed_trades) || 0;
+                            const openCount = parseInt(p.open_trades) || 0;
+                            if (closedCount > 0 && openCount === 0) {
+                              const exit = calcExitPrice(p.avg_entry_price, p.realized_pnl_cents, parseInt(p.open_contracts) || parseInt(p.total_trades) || 1);
+                              return <span style={{ color: 'var(--text-dim)' }}>{exit}¢</span>;
+                            }
+                            return <span className="badge bg-blue" style={{ fontSize: '0.65rem' }}>Open</span>;
+                          })()}
+                        </td>
+                        <td className="mono" style={{ color: 'var(--text)' }}>
+                          {fmt(p.open_cost_cents)}
+                        </td>
+                        <td className="mono" style={{
+                          fontFamily: "'JetBrains Mono', monospace",
+                          fontWeight: 700,
+                          color: p.realized_pnl_cents > 0 ? 'var(--green)' : p.realized_pnl_cents < 0 ? 'var(--red)' : 'var(--text-muted)',
+                        }}>
+                          {p.realized_pnl_cents !== 0 ? fmtPnl(p.realized_pnl_cents) : '—'}
+                        </td>
+                        <td className="mono" style={{ color: 'var(--text-muted)' }}>
+                          {p.total_trades?.toLocaleString()}
+                        </td>
+                        <td className="text-xs" style={{ color: 'var(--text-muted)' }}>
+                          {timeAgo(p.last_trade_at)}
+                        </td>
+                      </tr>
+                      {isPosExpanded && (
+                        <tr>
+                          <td colSpan={11} style={{ padding: 0 }}>
+                            <TradeSubTable p={p} trades={posTrades} loading={isLoadingTrades} />
+                          </td>
+                        </tr>
+                      )}
+                    </React.Fragment>
+                  );
+                })
+              )}
+            </tbody>
+          </table>
+        </div>
+      )}
+
+      {/* ══════════════════════════════
+          P&L VIEW
+          ══════════════════════════════ */}
+      {viewMode === 'pnl' && (
+        <div className="space-y-4">
+
+          {/* Winners */}
+          <div className="card overflow-hidden" style={{ padding: 0 }}>
+            <div
+              className="flex items-center gap-3 px-4 py-3"
+              style={{
+                background: 'rgba(0,195,137,0.06)',
+                borderBottom: '1px solid rgba(0,195,137,0.2)',
+                borderLeft: '4px solid var(--green)',
+              }}
+            >
+              <span className="text-sm font-bold text-green" style={{ textShadow: '0 0 10px rgba(0,195,137,0.4)' }}>Winners</span>
+              <span className="badge bg-green">{winners.length}</span>
+              <div className="flex-1" />
+              <span className="mono font-bold pnl-positive">
+                {fmtPnl(winners.reduce((s, p) => s + p.realized_pnl_cents, 0))}
+              </span>
+            </div>
+            <table>
+              <thead>
+                <tr>
+                  <th>Ticker</th>
+                  <th>Market</th>
+                  <th>Side</th>
+                  <th>Contracts</th>
+                  <th>Bought At</th>
+                  <th>Sold At</th>
+                  <th>Cost</th>
+                  <th>Profit</th>
+                  <th>ROI</th>
+                </tr>
+              </thead>
+              <tbody>
+                {winners.map(p => {
+                  const roi =
+                    p.open_cost_cents > 0
+                      ? `${((p.realized_pnl_cents / p.open_cost_cents) * 100).toFixed(0)}%`
+                      : '∞';
+                  const wTickerSeed = p.ticker.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
+                  const wCandles = genCandles(wTickerSeed + 200, 9, 'up');
+                  const closedCount = parseInt(p.closed_trades) || 0;
+                  const openCount = parseInt(p.open_trades) || 0;
+                  const exitPrice = (closedCount > 0 && openCount === 0)
+                    ? calcExitPrice(p.avg_entry_price, p.realized_pnl_cents, parseInt(p.open_contracts) || parseInt(p.total_trades) || 1)
+                    : null;
+                  return (
+                    <tr key={`${p.ticker}_${p.direction}`} className="position-winning" style={{ position: 'relative' }}>
+                      <td>
+                        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+                          <a
+                            href={kalshiLink(p.ticker)}
+                            target="_blank"
+                            rel="noopener"
+                            className="mono text-sm hover:underline"
+                            style={{ color: 'var(--orange)' }}
+                          >
+                            {p.ticker}
+                          </a>
+                          <MiniCandlestick data={wCandles} width={64} height={26} />
+                        </div>
+                      </td>
+                      <td className="text-xs max-w-[192px] truncate" style={{ color: 'var(--text-dim)' }}>
+                        {p.title}
+                      </td>
+                      <td>
+                        <span className={`badge ${sideBadge(p.direction)}`}>
+                          {sideLabel(p.direction)}
+                        </span>
+                      </td>
+                      <td className="mono">{p.open_contracts?.toLocaleString()}</td>
+                      <td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
+                        {p.avg_entry_price}¢
+                      </td>
+                      <td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace", color: exitPrice != null ? 'var(--text-dim)' : 'var(--text-muted)' }}>
+                        {exitPrice != null ? `${exitPrice}¢` : <span className="badge bg-blue" style={{ fontSize: '0.65rem' }}>Open</span>}
+                      </td>
+                      <td className="mono" style={{ color: 'var(--text-dim)' }}>
+                        {fmt(p.open_cost_cents)}
+                      </td>
+                      <td className="mono font-bold pnl-positive" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
+                        {fmtPnl(p.realized_pnl_cents)}
+                      </td>
+                      <td className="mono pnl-positive">{roi}</td>
+                    </tr>
+                  );
+                })}
+              </tbody>
+            </table>
+          </div>
+
+          {/* Losers */}
+          <div className="card overflow-hidden" style={{ padding: 0 }}>
+            <div
+              className="flex items-center gap-3 px-4 py-3"
+              style={{
+                background: 'rgba(255,92,92,0.06)',
+                borderBottom: '1px solid rgba(255,92,92,0.2)',
+                borderLeft: '4px solid var(--red)',
+              }}
+            >
+              <span className="text-sm font-bold text-red" style={{ textShadow: '0 0 10px rgba(255,92,92,0.4)' }}>Losers</span>
+              <span className="badge bg-red">{losers.length}</span>
+              <div className="flex-1" />
+              <span className="mono font-bold pnl-negative">
+                {fmtPnl(losers.reduce((s, p) => s + p.realized_pnl_cents, 0))}
+              </span>
+            </div>
+            <table>
+              <thead>
+                <tr>
+                  <th>Ticker</th>
+                  <th>Market</th>
+                  <th>Side</th>
+                  <th>Contracts</th>
+                  <th>Bought At</th>
+                  <th>Sold At</th>
+                  <th>Cost</th>
+                  <th>Profit</th>
+                  <th>ROI</th>
+                </tr>
+              </thead>
+              <tbody>
+                {losers.map(p => {
+                  const roi =
+                    p.open_cost_cents > 0
+                      ? `${((p.realized_pnl_cents / p.open_cost_cents) * 100).toFixed(0)}%`
+                      : '—';
+                  const lTickerSeed = p.ticker.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
+                  const lCandles = genCandles(lTickerSeed + 300, 9, 'down');
+                  const closedCount = parseInt(p.closed_trades) || 0;
+                  const openCount = parseInt(p.open_trades) || 0;
+                  const exitPrice = (closedCount > 0 && openCount === 0)
+                    ? calcExitPrice(p.avg_entry_price, p.realized_pnl_cents, parseInt(p.open_contracts) || parseInt(p.total_trades) || 1)
+                    : null;
+                  return (
+                    <tr key={`${p.ticker}_${p.direction}`} className="position-losing" style={{ position: 'relative' }}>
+                      <td>
+                        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+                          <a
+                            href={kalshiLink(p.ticker)}
+                            target="_blank"
+                            rel="noopener"
+                            className="mono text-sm hover:underline"
+                            style={{ color: 'var(--orange)' }}
+                          >
+                            {p.ticker}
+                          </a>
+                          <MiniCandlestick data={lCandles} width={64} height={26} />
+                        </div>
+                      </td>
+                      <td className="text-xs max-w-[192px] truncate" style={{ color: 'var(--text-dim)' }}>
+                        {p.title}
+                      </td>
+                      <td>
+                        <span className={`badge ${sideBadge(p.direction)}`}>
+                          {sideLabel(p.direction)}
+                        </span>
+                      </td>
+                      <td className="mono">{p.open_contracts?.toLocaleString()}</td>
+                      <td className="mono" style={{ color: 'var(--orange)', fontFamily: "'JetBrains Mono', monospace" }}>
+                        {p.avg_entry_price}¢
+                      </td>
+                      <td className="mono" style={{ fontFamily: "'JetBrains Mono', monospace", color: exitPrice != null ? 'var(--text-dim)' : 'var(--text-muted)' }}>
+                        {exitPrice != null ? `${exitPrice}¢` : <span className="badge bg-blue" style={{ fontSize: '0.65rem' }}>Open</span>}
+                      </td>
+                      <td className="mono" style={{ color: 'var(--text-dim)' }}>
+                        {fmt(p.open_cost_cents)}
+                      </td>
+                      <td className="mono font-bold pnl-negative" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
+                        {fmtPnl(p.realized_pnl_cents)}
+                      </td>
+                      <td className="mono pnl-negative">{roi}</td>
+                    </tr>
+                  );
+                })}
+              </tbody>
+            </table>
+          </div>
+
+          {/* Flat */}
+          {flat.length > 0 && (
+            <div className="card overflow-hidden" style={{ padding: 0 }}>
+              <div
+                className="flex items-center gap-3 px-4 py-3"
+                style={{
+                  background: 'var(--surface-el)',
+                  borderBottom: '1px solid var(--border)',
+                }}
+              >
+                <span className="text-sm font-bold" style={{ color: 'var(--text-dim)' }}>
+                  Flat (No Realized P&L)
+                </span>
+                <span className="badge bg-orange">{flat.length}</span>
+              </div>
+              <table>
+                <thead>
+                  <tr>
+                    <th>Ticker</th>
+                    <th>Market</th>
+                    <th>Side</th>
+                    <th>Contracts</th>
+                    <th>Cost</th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {flat.map(p => (
+                    <tr key={`${p.ticker}_${p.direction}`}>
+                      <td>
+                        <a
+                          href={kalshiLink(p.ticker)}
+                          target="_blank"
+                          rel="noopener"
+                          className="mono text-sm hover:underline"
+                          style={{ color: 'var(--orange)' }}
+                        >
+                          {p.ticker}
+                        </a>
+                      </td>
+                      <td className="text-xs max-w-[192px] truncate" style={{ color: 'var(--text-dim)' }}>
+                        {p.title}
+                      </td>
+                      <td>
+                        <span className={`badge ${sideBadge(p.direction)}`}>
+                          {sideLabel(p.direction)}
+                        </span>
+                      </td>
+                      <td className="mono">{p.open_contracts?.toLocaleString()}</td>
+                      <td className="mono" style={{ color: 'var(--text-dim)' }}>
+                        {fmt(p.open_cost_cents)}
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          )}
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Pulse.jsx b/kalshi-dash/src/pages/Pulse.jsx
new file mode 100644
index 0000000..32f8620
--- /dev/null
+++ b/kalshi-dash/src/pages/Pulse.jsx
@@ -0,0 +1,748 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { api } from '../api';
+
+// ── Pulse Category Grid Configuration ──
+const PULSE_CATEGORIES = [
+  {
+    name: 'Economy',
+    icon: '\u{1F4B0}',
+    subcategories: ['GDP', 'Inflation', 'Jobs', 'Fed Rate', 'Recession', 'Trade', 'Debt', 'Markets'],
+    keywords: ['gdp', 'inflation', 'cpi', 'unemployment', 'jobs', 'payroll', 'fed', 'interest rate', 'recession', 'stock', 'sp500', 'nasdaq', 'dow', 'treasury', 'bond', 'debt', 'trade', 'tariff', 'deficit'],
+    cssColor: 'var(--green)',
+    dimColor: 'var(--green-dim)',
+    rgbColor: [0, 195, 137],
+  },
+  {
+    name: 'Politics',
+    icon: '\u{1F3DB}',
+    subcategories: ['White House', 'Congress', 'Elections', 'SCOTUS', 'Cabinet', 'Polls', 'Policy', 'Foreign'],
+    keywords: ['trump', 'president', 'impeach', 'congress', 'senate', 'house', 'governor', 'election', 'democrat', 'republican', 'vote', 'biden', 'vance', 'cabinet', 'scotus', 'supreme', 'pardon', 'approval', 'poll', 'secretary', 'defense', 'attorney'],
+    cssColor: 'var(--blue)',
+    dimColor: 'var(--blue-dim)',
+    rgbColor: [59, 130, 246],
+  },
+  {
+    name: 'Weather',
+    icon: '\u{26C8}',
+    subcategories: ['Temperature', 'Storms', 'Snow', 'Rain', 'Hurricanes', 'Heat', 'Wind', 'Climate'],
+    keywords: ['temperature', 'rain', 'snow', 'hurricane', 'weather', 'tornado', 'flood', 'heat', 'cold', 'storm', 'wind', 'precipitation', 'climate'],
+    cssColor: 'var(--cyan)',
+    dimColor: 'var(--cyan-dim)',
+    rgbColor: [0, 240, 255],
+  },
+  {
+    name: 'Technology',
+    icon: '\u{1F4BB}',
+    subcategories: ['AI', 'SpaceX', 'Tesla', 'Big Tech', 'Startups', 'Chips', 'Cyber', 'Social'],
+    keywords: ['ai', 'spacex', 'tesla', 'apple', 'google', 'meta', 'openai', 'chatgpt', 'tech', 'rocket', 'launch'],
+    cssColor: 'var(--cyan)',
+    dimColor: 'var(--cyan-dim)',
+    rgbColor: [0, 240, 255],
+  },
+  {
+    name: 'Crypto',
+    icon: '\u{1FA99}',
+    subcategories: ['Bitcoin', 'Ethereum', 'DeFi', 'NFTs', 'Regulation', 'Mining', 'Stablecoins', 'Altcoins'],
+    keywords: ['bitcoin', 'btc', 'ethereum', 'eth', 'crypto', 'token', 'blockchain', 'defi'],
+    cssColor: 'var(--orange)',
+    dimColor: 'var(--orange-dim)',
+    rgbColor: [247, 147, 26],
+  },
+  {
+    name: 'Sports',
+    icon: '\u{1F3C8}',
+    subcategories: ['NFL', 'NBA', 'MLB', 'Soccer', 'Tennis', 'UFC', 'Olympics', 'Playoffs'],
+    keywords: ['nfl', 'nba', 'mlb', 'nhl', 'soccer', 'tennis', 'super bowl', 'championship', 'game', 'match', 'playoff', 'world series'],
+    cssColor: 'var(--purple)',
+    dimColor: 'var(--purple-dim)',
+    rgbColor: [168, 85, 247],
+  },
+  {
+    name: 'Energy',
+    icon: '\u{26A1}',
+    subcategories: ['Oil', 'Gas', 'Solar', 'Nuclear', 'OPEC', 'EVs', 'Grid', 'Coal'],
+    keywords: ['oil', 'gas', 'energy', 'solar', 'opec', 'crude', 'coal', 'renewable'],
+    cssColor: 'var(--orange)',
+    dimColor: 'var(--orange-dim)',
+    rgbColor: [247, 147, 26],
+  },
+  {
+    name: 'Culture',
+    icon: '\u{1F3AC}',
+    subcategories: ['Movies', 'Music', 'Awards', 'TV', 'Social Media', 'Trends', 'Celebrity', 'Gaming'],
+    keywords: ['oscar', 'movie', 'tv', 'celebrity', 'grammy', 'award', 'music', 'entertainment'],
+    cssColor: 'var(--orange)',
+    dimColor: 'var(--orange-dim)',
+    rgbColor: [247, 147, 26],
+  },
+];
+
+// Keep original baseColor arrays for data logic compatibility
+PULSE_CATEGORIES.forEach((cat, i) => {
+  cat.baseColor = cat.rgbColor;
+});
+
+function getHeatColor(intensity, baseColor) {
+  const [r, g, b] = baseColor;
+  const alpha = Math.max(0.05, Math.min(0.9, intensity));
+  return `rgba(${r},${g},${b},${alpha})`;
+}
+
+function getHeatBorder(intensity, baseColor) {
+  const [r, g, b] = baseColor;
+  const alpha = Math.max(0.1, Math.min(0.6, intensity * 0.8));
+  return `rgba(${r},${g},${b},${alpha})`;
+}
+
+function getTextColor(intensity, baseColor) {
+  const [r, g, b] = baseColor;
+  if (intensity > 0.5) return `rgb(${Math.min(255, r + 60)},${Math.min(255, g + 60)},${Math.min(255, b + 60)})`;
+  if (intensity > 0.2) return `rgb(${r},${g},${b})`;
+  return 'var(--text-muted)';
+}
+
+// ── Pulse Cell Component ──
+function PulseCell({ subcategory, intensity, signalCount, sentiment, baseColor, onClick, isSelected }) {
+  const bg = getHeatColor(intensity, baseColor);
+  const border = getHeatBorder(intensity, baseColor);
+  const textColor = getTextColor(intensity, baseColor);
+  const scanLines = intensity > 0.3;
+
+  return (
+    <div
+      onClick={onClick}
+      className={`relative overflow-hidden rounded-lg cursor-pointer group ${isSelected ? 'z-10' : ''}`}
+      style={{
+        backgroundColor: bg,
+        border: isSelected
+          ? '2px solid var(--cyan)'
+          : `1px solid ${border}`,
+        minHeight: 70,
+        transform: isSelected ? 'scale(1.05)' : undefined,
+        transition: 'all 200ms ease',
+        boxShadow: isSelected ? '0 0 16px rgba(0,240,255,0.2)' : undefined,
+      }}
+    >
+      {/* Thermal scan lines effect */}
+      {scanLines && (
+        <div
+          className="absolute inset-0 opacity-10 pointer-events-none"
+          style={{
+            backgroundImage: `repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255,255,255,0.03) 2px, rgba(255,255,255,0.03) 4px)`,
+          }}
+        />
+      )}
+
+      {/* Pulse animation for high intensity */}
+      {intensity > 0.6 && (
+        <div
+          className="absolute inset-0 animate-pulse opacity-30 pointer-events-none"
+          style={{
+            background: `radial-gradient(circle at center, rgba(${baseColor.join(',')},0.4), transparent 70%)`,
+            animationDuration: `${3 - intensity * 2}s`,
+          }}
+        />
+      )}
+
+      {/* Hover glow */}
+      <div
+        className="absolute inset-0 opacity-0 group-hover:opacity-100 pointer-events-none"
+        style={{
+          background: `radial-gradient(circle at center, rgba(${baseColor.join(',')},0.08), transparent)`,
+          transition: 'opacity 150ms ease',
+        }}
+      />
+
+      <div className="relative p-2.5 h-full flex flex-col justify-between z-10">
+        <div>
+          <p className="text-[10px] font-bold uppercase tracking-wider" style={{ color: textColor }}>
+            {subcategory}
+          </p>
+        </div>
+        <div className="flex items-end justify-between mt-1">
+          <div>
+            {signalCount > 0 ? (
+              <p className="text-lg font-bold mono" style={{ color: textColor }}>
+                {signalCount}
+              </p>
+            ) : (
+              <p className="text-sm" style={{ color: 'var(--text-muted)' }}>--</p>
+            )}
+          </div>
+          {sentiment !== 0 && (
+            <div
+              className="text-[9px] mono font-bold px-1.5 py-0.5 rounded"
+              style={
+                sentiment > 0
+                  ? { background: 'var(--green-dim)', color: 'var(--green)' }
+                  : { background: 'var(--red-dim)', color: 'var(--red)' }
+              }
+            >
+              {sentiment > 0 ? '+' : ''}{sentiment.toFixed(2)}
+            </div>
+          )}
+        </div>
+      </div>
+
+      {/* Intensity bar at bottom */}
+      <div
+        className="absolute bottom-0 left-0 right-0 h-0.5"
+        style={{
+          background: `linear-gradient(90deg, transparent, rgba(${baseColor.join(',')},${intensity}), transparent)`,
+        }}
+      />
+    </div>
+  );
+}
+
+// ── Category Row ──
+function CategoryRow({ category, subcategoryData, onCellClick, selectedCell }) {
+  const totalSignals = subcategoryData.reduce((s, d) => s + d.signalCount, 0);
+  const maxIntensity = Math.max(...subcategoryData.map(d => d.intensity), 0);
+  const avgSentiment = subcategoryData.reduce((s, d) => s + d.sentiment, 0) / subcategoryData.length;
+
+  return (
+    <div className="space-y-2">
+      {/* Category Header */}
+      <div className="flex items-center justify-between">
+        <div className="flex items-center gap-2">
+          <span className="text-lg">{category.icon}</span>
+          <h3 className="text-sm font-bold heading" style={{ color: category.cssColor }}>
+            {category.name}
+          </h3>
+          <span className="text-[10px]" style={{ color: 'var(--text-muted)' }}>{totalSignals} signals</span>
+        </div>
+        <div className="flex items-center gap-3 text-[10px]">
+          <span style={{ color: 'var(--text-muted)' }}>
+            Peak:{' '}
+            <span className="font-bold" style={{ color: category.cssColor }}>
+              {(maxIntensity * 100).toFixed(0)}%
+            </span>
+          </span>
+          <span
+            className="mono"
+            style={{ color: avgSentiment > 0 ? 'var(--green)' : avgSentiment < 0 ? 'var(--red)' : 'var(--text-muted)' }}
+          >
+            Sentiment: {avgSentiment > 0 ? '+' : ''}{avgSentiment.toFixed(3)}
+          </span>
+        </div>
+      </div>
+
+      {/* Subcategory Grid */}
+      <div className="grid grid-cols-8 gap-2">
+        {subcategoryData.map(sub => (
+          <PulseCell
+            key={sub.name}
+            subcategory={sub.name}
+            intensity={sub.intensity}
+            signalCount={sub.signalCount}
+            sentiment={sub.sentiment}
+            baseColor={category.baseColor}
+            onClick={() => onCellClick({ category: category.name, subcategory: sub.name, ...sub })}
+            isSelected={selectedCell?.category === category.name && selectedCell?.subcategory === sub.name}
+          />
+        ))}
+      </div>
+    </div>
+  );
+}
+
+// ── Intensity Scale ──
+function IntensityScale() {
+  const steps = 10;
+  return (
+    <div className="flex items-center gap-2">
+      <span className="stat-label">Low</span>
+      <div className="flex gap-px">
+        {Array.from({ length: steps }).map((_, i) => (
+          <div
+            key={i}
+            className="w-5 h-3 rounded-sm"
+            style={{
+              backgroundColor: `rgba(0,240,255,${(i + 1) / steps * 0.7 + 0.05})`,
+            }}
+          />
+        ))}
+      </div>
+      <span className="stat-label">High</span>
+    </div>
+  );
+}
+
+// ── Cell Detail Panel ──
+function CellDetail({ cell, signals, onClose }) {
+  if (!cell) return null;
+
+  const cellSignals = signals.filter(s => {
+    const lower = (s.reasoning || s.ticker || '').toLowerCase();
+    return cell.matchingKeywords?.some(kw => lower.includes(kw));
+  }).slice(0, 10);
+
+  return (
+    <div className="card-glow fade-in">
+      <div className="flex items-start justify-between mb-3">
+        <div>
+          <div className="flex items-center gap-2 mb-1">
+            <span className="text-lg">{cell.categoryIcon}</span>
+            <h3 className="heading text-sm font-bold" style={{ color: 'var(--text)' }}>
+              {cell.category} / {cell.subcategory}
+            </h3>
+          </div>
+          <p className="text-xs" style={{ color: 'var(--text-muted)' }}>
+            {cell.signalCount} signals | Intensity: {(cell.intensity * 100).toFixed(0)}% | Sentiment:{' '}
+            <span style={{ color: cell.sentiment > 0 ? 'var(--green)' : cell.sentiment < 0 ? 'var(--red)' : 'var(--text-dim)' }}>
+              {cell.sentiment > 0 ? '+' : ''}{cell.sentiment.toFixed(3)}
+            </span>
+          </p>
+        </div>
+        <button onClick={onClose} className="btn btn-ghost text-lg leading-none" style={{ padding: '0 6px' }}>
+          &times;
+        </button>
+      </div>
+
+      {cellSignals.length > 0 ? (
+        <div className="space-y-2">
+          <p className="stat-label mb-1">Related Signals</p>
+          {cellSignals.map((sig, i) => (
+            <div
+              key={i}
+              className="flex items-center gap-3 text-xs rounded-lg px-3 py-2"
+              style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+            >
+              <span
+                className="badge"
+                style={
+                  sig.direction === 'BUY_YES'
+                    ? { background: 'var(--green-dim)', color: 'var(--green)' }
+                    : { background: 'var(--red-dim)', color: 'var(--red)' }
+                }
+              >
+                {sig.direction}
+              </span>
+              <span className="mono truncate flex-1" style={{ color: 'var(--text-dim)' }}>{sig.ticker}</span>
+              <span className="mono font-bold" style={{ color: 'var(--orange)' }}>
+                {(parseFloat(sig.expected_edge) * 100).toFixed(1)}%
+              </span>
+              <span className="mono" style={{ color: 'var(--blue)' }}>
+                {(parseFloat(sig.confidence) * 100).toFixed(0)}%
+              </span>
+              {sig.risk_approved ? (
+                <span className="text-[9px] font-bold" style={{ color: 'var(--green)' }}>OK</span>
+              ) : (
+                <span className="text-[9px] font-bold" style={{ color: 'var(--red)' }}>BLOCKED</span>
+              )}
+            </div>
+          ))}
+        </div>
+      ) : (
+        <p className="text-sm text-center py-4" style={{ color: 'var(--text-muted)' }}>
+          No matching signals for this subcategory
+        </p>
+      )}
+    </div>
+  );
+}
+
+// ── National Overview Bar ──
+function NationalOverview({ categoryStats }) {
+  const maxSignals = Math.max(...categoryStats.map(c => c.totalSignals), 1);
+
+  return (
+    <div className="card">
+      <div className="flex items-center gap-2 mb-4">
+        <h2 className="section-title">National Activity Overview</h2>
+        <span className="section-subtitle">Signal density across categories</span>
+      </div>
+      <div className="space-y-2">
+        {categoryStats.sort((a, b) => b.totalSignals - a.totalSignals).map(cat => {
+          const pct = (cat.totalSignals / maxSignals) * 100;
+          return (
+            <div key={cat.name} className="flex items-center gap-3">
+              <span className="text-sm w-5">{cat.icon}</span>
+              <span className="text-xs font-medium w-20 heading" style={{ color: cat.cssColor || `rgb(${cat.baseColor.join(',')})` }}>
+                {cat.name}
+              </span>
+              <div
+                className="flex-1 h-5 rounded-full overflow-hidden relative"
+                style={{ background: 'var(--surface-el)' }}
+              >
+                <div
+                  className="h-full rounded-full transition-all duration-1000 relative overflow-hidden"
+                  style={{
+                    width: `${Math.max(pct, 2)}%`,
+                    background: `linear-gradient(90deg, rgba(${cat.baseColor.join(',')},0.3), rgba(${cat.baseColor.join(',')},0.6))`,
+                  }}
+                >
+                  {/* Animated shimmer */}
+                  <div
+                    className="absolute inset-0"
+                    style={{
+                      background: 'linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.1) 50%, transparent 100%)',
+                      animation: 'shimmer 2s infinite',
+                    }}
+                  />
+                </div>
+                <span
+                  className="absolute right-2 top-0 bottom-0 flex items-center text-[10px] mono"
+                  style={{ color: 'var(--text-dim)' }}
+                >
+                  {cat.totalSignals}
+                </span>
+              </div>
+              <span
+                className="text-[10px] mono w-12 text-right"
+                style={{ color: cat.avgSentiment > 0 ? 'var(--green)' : cat.avgSentiment < 0 ? 'var(--red)' : 'var(--text-muted)' }}
+              >
+                {cat.avgSentiment > 0 ? '+' : ''}{cat.avgSentiment.toFixed(2)}
+              </span>
+            </div>
+          );
+        })}
+      </div>
+    </div>
+  );
+}
+
+// ── Main Pulse Page ──
+export default function Pulse() {
+  const [data, setData] = useState(null);
+  const [signals, setSignals] = useState(null);
+  const [signalsDash, setSignalsDash] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [selectedCell, setSelectedCell] = useState(null);
+  const [timeWindow, setTimeWindow] = useState('24h');
+
+  useEffect(() => {
+    load();
+    const i = setInterval(load, 30000);
+    return () => clearInterval(i);
+  }, []);
+
+  async function load() {
+    try {
+      const [d, s, sd] = await Promise.all([
+        api('/dashboard'),
+        api('/signals'),
+        api('/signals/dashboard'),
+      ]);
+      setData(d);
+      setSignals(s?.signals || []);
+      setSignalsDash(sd);
+    } catch {}
+    setLoading(false);
+  }
+
+  const { categoryData, categoryStats, overallStats } = useMemo(() => {
+    if (!signals) return { categoryData: [], categoryStats: [], overallStats: {} };
+
+    const allSignals = signals || [];
+    const maxSubSignals = Math.max(
+      ...PULSE_CATEGORIES.flatMap(cat =>
+        cat.subcategories.map(sub => {
+          const keywords = cat.keywords.filter(kw => sub.toLowerCase().includes(kw.split(' ')[0]) || kw.includes(sub.toLowerCase()));
+          const matching = allSignals.filter(s => {
+            const lower = (s.reasoning || s.ticker || '').toLowerCase();
+            return cat.keywords.some(kw => lower.includes(kw));
+          });
+          return matching.length;
+        })
+      ),
+      1
+    );
+
+    const catData = PULSE_CATEGORIES.map(category => {
+      const categorySignals = allSignals.filter(s => {
+        const lower = (s.reasoning || s.ticker || '').toLowerCase();
+        return category.keywords.some(kw => lower.includes(kw));
+      });
+
+      const totalCatSignals = categorySignals.length;
+      const avgCatSentiment = categorySignals.length > 0
+        ? categorySignals.reduce((s, sig) => {
+            const sentSum = (sig.sources || []).reduce((ss, src) => ss + (src.sentiment || 0), 0);
+            return s + sentSum;
+          }, 0) / Math.max(categorySignals.length, 1)
+        : 0;
+
+      const subcategoryData = category.subcategories.map(sub => {
+        const subKeywords = category.keywords.filter(kw => {
+          const subLower = sub.toLowerCase();
+          return kw.includes(subLower) || subLower.includes(kw.split(' ')[0]);
+        });
+
+        let matchingSignals;
+        if (subKeywords.length > 0) {
+          matchingSignals = allSignals.filter(s => {
+            const lower = (s.reasoning || s.ticker || '').toLowerCase();
+            return subKeywords.some(kw => lower.includes(kw));
+          });
+        } else {
+          const hash = sub.split('').reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0);
+          matchingSignals = categorySignals.filter((_, i) => i % category.subcategories.length === Math.abs(hash) % category.subcategories.length);
+        }
+
+        const signalCount = matchingSignals.length;
+        const sentiment = matchingSignals.reduce((s, sig) => {
+          const sentSum = (sig.sources || []).reduce((ss, src) => ss + (src.sentiment || 0), 0);
+          return s + sentSum;
+        }, 0) / Math.max(matchingSignals.length, 1);
+
+        const intensity = Math.min(1, signalCount / Math.max(maxSubSignals * 0.5, 1));
+
+        return {
+          name: sub,
+          signalCount,
+          sentiment,
+          intensity,
+          matchingKeywords: subKeywords.length > 0 ? subKeywords : category.keywords,
+          categoryIcon: category.icon,
+        };
+      });
+
+      return { category, subcategoryData };
+    });
+
+    const catStats = PULSE_CATEGORIES.map(cat => {
+      const catSignals = allSignals.filter(s => {
+        const lower = (s.reasoning || s.ticker || '').toLowerCase();
+        return cat.keywords.some(kw => lower.includes(kw));
+      });
+      return {
+        name: cat.name,
+        icon: cat.icon,
+        baseColor: cat.baseColor,
+        cssColor: cat.cssColor,
+        totalSignals: catSignals.length,
+        avgSentiment: catSignals.length > 0
+          ? catSignals.reduce((s, sig) => s + (sig.sources || []).reduce((ss, src) => ss + (src.sentiment || 0), 0), 0) / catSignals.length
+          : 0,
+      };
+    });
+
+    const overall = {
+      totalSignals: allSignals.length,
+      buyYes: allSignals.filter(s => s.direction === 'BUY_YES').length,
+      buyNo: allSignals.filter(s => s.direction === 'BUY_NO').length,
+      approved: allSignals.filter(s => s.risk_approved).length,
+      avgEdge: allSignals.length > 0 ? allSignals.reduce((s, sig) => s + parseFloat(sig.expected_edge || 0), 0) / allSignals.length : 0,
+      avgConf: allSignals.length > 0 ? allSignals.reduce((s, sig) => s + parseFloat(sig.confidence || 0), 0) / allSignals.length : 0,
+      hottest: [...catStats].sort((a, b) => b.totalSignals - a.totalSignals)[0]?.name || 'None',
+    };
+
+    return { categoryData: catData, categoryStats: catStats, overallStats: overall };
+  }, [signals]);
+
+  if (loading) return (
+    <div className="flex items-center justify-center h-64">
+      <div
+        className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
+        style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+      />
+    </div>
+  );
+
+  const overallStatsConfig = [
+    { label: 'Total Signals',   value: overallStats.totalSignals || 0,                              color: 'var(--cyan)' },
+    { label: 'BUY YES',         value: overallStats.buyYes || 0,                                     color: 'var(--green)' },
+    { label: 'BUY NO',          value: overallStats.buyNo || 0,                                      color: 'var(--red)' },
+    { label: 'Approved',        value: overallStats.approved || 0,                                   color: 'var(--green)' },
+    { label: 'Avg Edge',        value: `${((overallStats.avgEdge || 0) * 100).toFixed(1)}%`,         color: 'var(--orange)' },
+    { label: 'Avg Confidence',  value: `${((overallStats.avgConf || 0) * 100).toFixed(0)}%`,         color: 'var(--blue)' },
+    { label: 'Hottest Sector',  value: overallStats.hottest,                                         color: 'var(--red)' },
+  ];
+
+  return (
+    <div className="space-y-6">
+      {/* Shimmer + hover scale CSS */}
+      <style>{`
+        @keyframes shimmer {
+          0% { transform: translateX(-100%); }
+          100% { transform: translateX(200%); }
+        }
+      `}</style>
+
+      {/* Page Header */}
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="heading text-2xl font-bold flex items-center gap-3">
+            {/* Live pulsing indicator */}
+            <span
+              className="inline-flex items-center justify-center w-3 h-3 rounded-full pulse-cyan"
+              style={{ backgroundColor: 'var(--cyan)', flexShrink: 0 }}
+            />
+            <span className="gradient-cyan">Pulse of America</span>
+          </h1>
+          <p className="section-subtitle mt-1">
+            Sentiment and activity heatmap across American prediction markets
+          </p>
+        </div>
+        <div className="flex items-center gap-3">
+          <div
+            className="flex items-center gap-0.5 rounded-lg p-0.5"
+            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+          >
+            {['24h', '7d', '30d'].map(tw => (
+              <button
+                key={tw}
+                onClick={() => setTimeWindow(tw)}
+                className="btn btn-sm"
+                style={timeWindow === tw
+                  ? { background: 'var(--cyan)', color: 'var(--bg)', fontWeight: 700 }
+                  : { background: 'transparent', color: 'var(--text-muted)' }
+                }
+              >
+                {tw}
+              </button>
+            ))}
+          </div>
+          <button onClick={load} className="btn btn-o btn-sm">Refresh</button>
+        </div>
+      </div>
+
+      {/* Overall Stats */}
+      <div className="grid grid-cols-7 gap-3">
+        {overallStatsConfig.map(s => (
+          <div key={s.label} className="card">
+            <p className="stat-label mb-1">{s.label}</p>
+            <p className="stat-v text-lg" style={{ color: s.color }}>{s.value}</p>
+          </div>
+        ))}
+      </div>
+
+      {/* National Overview */}
+      <NationalOverview categoryStats={categoryStats} />
+
+      {/* Live Signal Heatmap Grid */}
+      <div className="card p-4">
+        <div className="flex items-center justify-between mb-5">
+          <div className="flex items-center gap-2">
+            <div
+              className="w-2 h-2 rounded-full animate-pulse"
+              style={{ backgroundColor: 'var(--red)' }}
+            />
+            <h2 className="section-title">Live Signal Heatmap</h2>
+          </div>
+          <IntensityScale />
+        </div>
+
+        <div className="space-y-5">
+          {categoryData.map(({ category, subcategoryData }) => (
+            <CategoryRow
+              key={category.name}
+              category={category}
+              subcategoryData={subcategoryData}
+              onCellClick={cell => setSelectedCell(
+                selectedCell?.category === cell.category && selectedCell?.subcategory === cell.subcategory
+                  ? null
+                  : cell
+              )}
+              selectedCell={selectedCell}
+            />
+          ))}
+        </div>
+      </div>
+
+      {/* Cell Detail */}
+      {selectedCell && (
+        <CellDetail
+          cell={selectedCell}
+          signals={signals || []}
+          onClose={() => setSelectedCell(null)}
+        />
+      )}
+
+      {/* Signal Source Breakdown */}
+      <div className="card">
+        <div className="flex items-center gap-2 mb-4">
+          <h2 className="section-title">Signal Source Breakdown</h2>
+        </div>
+        <div className="grid grid-cols-4 gap-3">
+          {(signalsDash?.sourceBreakdown || []).slice(0, 8).map(src => {
+            const sent = parseFloat(src.avg_sent) || 0;
+            const count = parseInt(src.cnt) || 0;
+            return (
+              <div
+                key={src.source}
+                className="rounded-lg p-3"
+                style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+              >
+                <div className="flex items-center justify-between mb-2">
+                  <span className="text-xs font-bold heading" style={{ color: 'var(--text-dim)' }}>
+                    {src.source.replace('_', ' ').toUpperCase()}
+                  </span>
+                  <span className="mono text-xs font-bold" style={{ color: 'var(--orange)' }}>{count}</span>
+                </div>
+                <div className="flex items-center gap-2">
+                  <div className="progress-bar flex-1">
+                    <div
+                      className="progress-fill"
+                      style={{
+                        width: `${Math.min(100, Math.abs(sent) * 5000 + 10)}%`,
+                        background: sent > 0 ? 'var(--green)' : sent < 0 ? 'var(--red)' : 'var(--text-muted)',
+                      }}
+                    />
+                  </div>
+                  <span
+                    className="text-[10px] mono"
+                    style={{ color: sent > 0 ? 'var(--green)' : sent < 0 ? 'var(--red)' : 'var(--text-muted)' }}
+                  >
+                    {sent > 0 ? '+' : ''}{sent.toFixed(3)}
+                  </span>
+                </div>
+              </div>
+            );
+          })}
+        </div>
+      </div>
+
+      {/* Market Pulse Metrics from Dashboard */}
+      {data && (
+        <div className="card">
+          <div className="flex items-center gap-2 mb-4">
+            <h2 className="section-title">Market Pulse Metrics</h2>
+          </div>
+          <div className="grid grid-cols-5 gap-3 text-center">
+            {[
+              {
+                label: 'Balance',
+                value: `$${data.balance ? (data.balance / 100).toLocaleString(undefined, { minimumFractionDigits: 0 }) : '0'}`,
+                color: 'var(--green)',
+              },
+              {
+                label: "Today's P&L",
+                value: `${(data.daily_pnl || 0) >= 0 ? '+' : ''}$${((data.daily_pnl || 0) / 100).toFixed(2)}`,
+                color: (data.daily_pnl || 0) >= 0 ? 'var(--green)' : 'var(--red)',
+              },
+              {
+                label: "Today's Trades",
+                value: (data.today_trades || 0).toLocaleString(),
+                color: 'var(--orange)',
+              },
+              {
+                label: 'Win Rate',
+                value: `${data.today_trades > 0 ? ((data.today_wins / data.today_trades) * 100).toFixed(1) : '0'}%`,
+                color: data.today_trades > 0 && (data.today_wins / data.today_trades * 100) >= 50
+                  ? 'var(--green)'
+                  : 'var(--red)',
+              },
+              {
+                label: 'Markets Tracked',
+                value: data.markets_tracked || 0,
+                color: 'var(--purple)',
+              },
+            ].map(s => (
+              <div
+                key={s.label}
+                className="rounded-lg p-3"
+                style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+              >
+                <p className="stat-label mb-1">{s.label}</p>
+                <p className="stat-v text-lg" style={{ color: s.color }}>{s.value}</p>
+              </div>
+            ))}
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Risk.jsx b/kalshi-dash/src/pages/Risk.jsx
new file mode 100644
index 0000000..d4e0161
--- /dev/null
+++ b/kalshi-dash/src/pages/Risk.jsx
@@ -0,0 +1,755 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import { api, post } from '../api';
+import {
+  BarChart, Bar, AreaChart, Area,
+  XAxis, YAxis, Tooltip, ResponsiveContainer,
+  Cell, PieChart, Pie,
+} from 'recharts';
+
+// ── Strategy color palette ──
+const STRAT_COLORS = {
+  aggressive: '#ef4444', conservative: '#3b82f6', weather: '#06b6d4', momentum: '#f59e0b',
+  contrarian: '#a855f7', ai_enhanced: '#ec4899', mc_validated: '#8b5cf6', scalper: '#10b981',
+  patient: '#6366f1', random: '#f97316', penny_pincher: '#84cc16', spray_pray: '#14b8a6',
+  nickel_slots: '#eab308', degen_lite: '#e11d48', politics_junkie: '#7c3aed', double_down: '#dc2626',
+  volatility_rider: '#0ea5e9', whale: '#1d4ed8', yolo: '#be123c', full_send: '#ea580c',
+  kelly_criterion: '#4f46e5', sharpe_sniper: '#059669', bayesian_blend: '#7c3aed', ensemble_lock: '#0891b2',
+  mean_revert: '#4338ca', info_ratio: '#b45309', anti_fomo: '#64748b', nightcrawler: '#334155',
+  heatseeker: '#dc2626', quant_core: '#2563eb',
+};
+const ETF_COLOR = '#F7931A';
+
+function getColor(strategy) {
+  if (strategy.startsWith('etf_')) return ETF_COLOR;
+  return STRAT_COLORS[strategy] || '#6b7280';
+}
+
+// ── Mini win/loss bar ──
+function MiniBar({ won, lost, total }) {
+  if (!total) return <span className="text-xs" style={{ color: 'var(--text-muted)' }}>no trades</span>;
+  const w = (won / total) * 100;
+  const l = (lost / total) * 100;
+  return (
+    <div className="flex h-1.5 rounded-full overflow-hidden w-full" style={{ background: 'var(--border)' }}>
+      {won > 0 && <div style={{ width: `${w}%`, background: 'var(--green)' }} />}
+      {lost > 0 && <div style={{ width: `${l}%`, background: 'var(--red)' }} />}
+    </div>
+  );
+}
+
+// ── Streak badge ──
+function StreakBadge({ streak }) {
+  if (streak === 0) return <span style={{ color: 'var(--text-muted)', fontSize: '0.7rem' }}>—</span>;
+  const hot = streak > 0;
+  return (
+    <span
+      className="mono font-bold text-xs"
+      style={{ color: hot ? 'var(--green)' : 'var(--red)' }}
+    >
+      {hot ? '+' : ''}{streak}
+    </span>
+  );
+}
+
+// ── Risk level badge ──
+function RiskBadge({ level }) {
+  if (!level) return null;
+  const map = {
+    low: 'bg-green',
+    medium: 'bg-orange',
+    high: 'bg-red',
+  };
+  return (
+    <span className={`badge ${map[level] || 'bg-blue'}`}>
+      {level.toUpperCase()}
+    </span>
+  );
+}
+
+// ── Trade direction helpers ──
+const sideLabel = (dir) => dir === 'BUY_YES' ? 'YES' : 'NO';
+const sideBadge = (dir) => dir === 'BUY_YES' ? 'bg-green' : 'bg-red';
+const pnlColor = (cents) => cents >= 0 ? 'var(--green)' : 'var(--red)';
+
+// ── Portfolio Trades Table (shown inside expanded card) ──
+function PortfolioTradesTable({ trades, loading }) {
+  if (loading) {
+    return (
+      <div className="flex justify-center py-4">
+        <div className="w-5 h-5 rounded-full border-2 animate-spin" style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }} />
+      </div>
+    );
+  }
+  if (!trades || trades.length === 0) {
+    return (
+      <p className="text-xs text-center py-3" style={{ color: 'var(--text-muted)' }}>
+        No trades found for this portfolio
+      </p>
+    );
+  }
+  return (
+    <div className="overflow-x-auto" style={{ maxHeight: 240, overflowY: 'auto' }}>
+      <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.7rem' }}>
+        <thead>
+          <tr style={{ borderBottom: '1px solid var(--border)' }}>
+            {['Market', 'Dir', 'Entry', 'Qty', 'Cost', 'P&L', 'Status'].map(h => (
+              <th key={h} style={{
+                padding: '4px 6px', textAlign: 'left', color: 'var(--text-muted)',
+                fontWeight: 600, fontFamily: 'var(--font-body)', fontSize: '0.65rem',
+                whiteSpace: 'nowrap',
+              }}>{h}</th>
+            ))}
+          </tr>
+        </thead>
+        <tbody>
+          {trades.map((t, i) => {
+            const pnl = parseInt(t.pnl_cents) || 0;
+            const cost = parseInt(t.cost_cents) || 0;
+            const entry = parseInt(t.entry_price_cents) || 0;
+            const contracts = parseInt(t.contracts) || 0;
+            const status = t.status || 'open';
+            return (
+              <tr key={t.id || i} style={{
+                borderBottom: '1px solid var(--border)',
+                background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)',
+              }}>
+                <td style={{
+                  padding: '4px 6px', maxWidth: 140, overflow: 'hidden',
+                  textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: 'var(--text-dim)',
+                  fontFamily: 'var(--font-body)',
+                }} title={t.market_title || t.market_id}>
+                  {(t.market_title || t.market_id || '').substring(0, 28)}{(t.market_title || t.market_id || '').length > 28 ? '...' : ''}
+                </td>
+                <td style={{ padding: '4px 6px' }}>
+                  <span className={`badge ${sideBadge(t.direction)}`} style={{ fontSize: '0.6rem', padding: '1px 5px' }}>
+                    {sideLabel(t.direction)}
+                  </span>
+                </td>
+                <td style={{ padding: '4px 6px', fontFamily: "'JetBrains Mono', monospace", color: 'var(--orange)' }}>
+                  {entry}¢
+                </td>
+                <td style={{ padding: '4px 6px', fontFamily: "'JetBrains Mono', monospace", color: 'var(--text-dim)' }}>
+                  {contracts}
+                </td>
+                <td style={{ padding: '4px 6px', fontFamily: "'JetBrains Mono', monospace", color: 'var(--text-dim)' }}>
+                  ${(cost / 100).toFixed(2)}
+                </td>
+                <td style={{
+                  padding: '4px 6px', fontFamily: "'JetBrains Mono', monospace", fontWeight: 700,
+                  color: pnlColor(pnl),
+                }}>
+                  {pnl !== 0 ? `${pnl >= 0 ? '+' : ''}$${(pnl / 100).toFixed(2)}` : '—'}
+                </td>
+                <td style={{ padding: '4px 6px' }}>
+                  <span className={`badge ${status === 'open' ? 'bg-blue' : status === 'closed' ? 'bg-green' : 'bg-orange'}`}
+                    style={{ fontSize: '0.6rem', padding: '1px 5px' }}
+                  >
+                    {status}
+                  </span>
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  );
+}
+
+// ── Portfolio Card ──
+function PortfolioCard({ p, dailyPnl, expanded, onToggle, pfTrades, pfTradesLoading }) {
+  const pnl = (p.total_returned_cents - p.total_invested_cents) / 100;
+  const totalTrades = p.trades_won + p.trades_lost + p.trades_expired;
+  const invested = p.total_invested_cents / 100;
+  const returned = p.total_returned_cents / 100;
+  const active = totalTrades > 0;
+  const color = getColor(p.strategy);
+
+  // Allocation as % of daily budget vs a synthetic max
+  const allocPct = Math.min(100, (p.daily_budget_cents / Math.max(1, 100000)) * 100);
+
+  return (
+    <div
+      className="card cursor-pointer transition-all"
+      style={
+        expanded
+          ? {
+              borderColor: 'var(--cyan)',
+              boxShadow: '0 0 20px var(--cyan-glow)',
+              opacity: 1,
+            }
+          : active
+          ? { opacity: 1 }
+          : { opacity: 0.5 }
+      }
+      onClick={onToggle}
+    >
+      {/* Header row */}
+      <div className="flex items-center justify-between mb-2">
+        <div className="flex items-center gap-2 min-w-0">
+          <div
+            className="w-2.5 h-2.5 rounded-full flex-shrink-0"
+            style={{ background: color }}
+          />
+          <span
+            className="heading font-semibold text-sm truncate"
+            style={{ color: 'var(--text)' }}
+          >
+            {p.name}
+          </span>
+          <span className="mono text-xs flex-shrink-0" style={{ color: 'var(--text-muted)' }}>
+            #{p.id}
+          </span>
+          {totalTrades > 0 && (
+            <span className="text-xs flex-shrink-0 px-1.5 py-0.5 rounded"
+              style={{ background: 'var(--surface-el)', color: 'var(--text-muted)', fontSize: '0.65rem' }}>
+              {totalTrades} trade{totalTrades !== 1 ? 's' : ''}
+            </span>
+          )}
+        </div>
+        <span
+          className="mono text-sm font-bold flex-shrink-0 ml-2"
+          style={{ color: pnl >= 0 ? 'var(--green)' : 'var(--red)' }}
+        >
+          {pnl >= 0 ? '+' : ''}${pnl.toFixed(2)}
+        </span>
+      </div>
+
+      {/* Strategy tag + streak */}
+      <div className="flex items-center gap-2 mb-3">
+        <span
+          className="mono px-1.5 py-0.5 rounded text-xs flex-shrink-0"
+          style={{
+            background: `${color}15`,
+            color,
+            border: `1px solid ${color}30`,
+          }}
+        >
+          {p.strategy}
+        </span>
+        <StreakBadge streak={p.streak} />
+        <div className="flex-1" />
+        <span
+          className="badge flex-shrink-0"
+          style={
+            active
+              ? { background: 'var(--green-dim)', color: 'var(--green)' }
+              : { background: 'var(--border)', color: 'var(--text-muted)' }
+          }
+        >
+          {active ? 'Active' : 'Dormant'}
+        </span>
+      </div>
+
+      {/* Win/loss bar */}
+      <div className="mb-3">
+        <MiniBar won={p.trades_won} lost={p.trades_lost} total={totalTrades} />
+        <div className="flex justify-between mt-1">
+          <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
+            {p.trades_won}W / {p.trades_lost}L
+          </span>
+          <span className="mono text-xs" style={{ color: 'var(--text-dim)' }}>
+            {p.win_rate || 0}%
+          </span>
+        </div>
+      </div>
+
+      {/* Allocation bar */}
+      <div className="mb-3">
+        <div className="flex justify-between text-xs mb-1" style={{ color: 'var(--text-muted)' }}>
+          <span>Daily Budget</span>
+          <span className="mono" style={{ color: 'var(--text-dim)' }}>
+            ${(p.daily_budget_cents / 100).toFixed(2)}
+          </span>
+        </div>
+        <div className="progress-bar">
+          <div
+            className="progress-fill"
+            style={{ width: `${allocPct}%`, background: `linear-gradient(90deg, ${color}, ${color}99)` }}
+          />
+        </div>
+      </div>
+
+      {/* Stats grid */}
+      <div className="grid grid-cols-2 gap-x-3 gap-y-1" style={{ fontSize: '0.7rem' }}>
+        <div className="flex justify-between">
+          <span style={{ color: 'var(--text-muted)' }}>Invested</span>
+          <span className="mono" style={{ color: 'var(--text-dim)' }}>${invested.toFixed(2)}</span>
+        </div>
+        <div className="flex justify-between">
+          <span style={{ color: 'var(--text-muted)' }}>Returned</span>
+          <span className="mono" style={{ color: 'var(--text-dim)' }}>${returned.toFixed(2)}</span>
+        </div>
+        <div className="flex justify-between">
+          <span style={{ color: 'var(--text-muted)' }}>Trades Today</span>
+          <span className="mono" style={{ color: 'var(--cyan)' }}>{p.trades_today}</span>
+        </div>
+        <div className="flex justify-between">
+          <span style={{ color: 'var(--text-muted)' }}>Total</span>
+          <span className="mono" style={{ color: 'var(--text-dim)' }}>{totalTrades}</span>
+        </div>
+      </div>
+
+      {/* Expanded sparkline */}
+      {expanded && dailyPnl && dailyPnl.length > 0 && (
+        <div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--border)' }}>
+          <p className="text-xs mb-1" style={{ color: 'var(--text-muted)' }}>Cumulative P&L</p>
+          <ResponsiveContainer width="100%" height={80}>
+            <AreaChart data={dailyPnl}>
+              <XAxis dataKey="date" hide />
+              <YAxis hide domain={['dataMin', 'dataMax']} />
+              <Tooltip
+                contentStyle={{
+                  background: 'var(--surface-el)',
+                  border: '1px solid var(--border)',
+                  borderRadius: 8,
+                  fontSize: 11,
+                  fontFamily: 'var(--font-mono)',
+                }}
+                formatter={v => [`$${(v / 100).toFixed(2)}`, 'P&L']}
+                labelFormatter={l => l}
+              />
+              <Area
+                type="monotone"
+                dataKey="cumulative"
+                stroke={color}
+                fill={color}
+                fillOpacity={0.1}
+                strokeWidth={1.5}
+                dot={false}
+              />
+            </AreaChart>
+          </ResponsiveContainer>
+          {p.description && (
+            <p className="text-xs mt-1" style={{ color: 'var(--text-muted)' }}>{p.description}</p>
+          )}
+
+          {/* Portfolio Trades Drill-Down */}
+          <div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--border)' }}>
+            <p className="text-xs mb-2 font-semibold" style={{ color: 'var(--cyan)' }}>
+              Individual Trades
+            </p>
+            <PortfolioTradesTable trades={pfTrades} loading={pfTradesLoading} />
+          </div>
+        </div>
+      )}
+
+      {/* If expanded but no dailyPnl chart, still show trades */}
+      {expanded && (!dailyPnl || dailyPnl.length === 0) && (
+        <div className="mt-3 pt-3" style={{ borderTop: '1px solid var(--border)' }}>
+          <p className="text-xs mb-2 font-semibold" style={{ color: 'var(--cyan)' }}>
+            Individual Trades
+          </p>
+          <PortfolioTradesTable trades={pfTrades} loading={pfTradesLoading} />
+        </div>
+      )}
+    </div>
+  );
+}
+
+// ── Spinner ──
+function Spinner() {
+  return (
+    <div className="flex items-center justify-center h-64">
+      <div
+        className="w-9 h-9 rounded-full border-2 animate-spin"
+        style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }}
+      />
+    </div>
+  );
+}
+
+export default function Risk() {
+  const [portfolios, setPortfolios] = useState([]);
+  const [dailyData, setDailyData] = useState({});
+  const [loading, setLoading] = useState(true);
+  const [filter, setFilter] = useState('all'); // all, active, dormant, etf, core
+  const [sort, setSort] = useState('pnl'); // pnl, winrate, invested, name, id
+  const [expanded, setExpanded] = useState(null);
+  const [pfTrades, setPfTrades] = useState({}); // { portfolioId: [...trades] }
+  const [pfTradesLoading, setPfTradesLoading] = useState(null); // portfolioId currently loading
+
+  useEffect(() => { load(); }, []);
+
+  async function load() {
+    try {
+      const [pf, hist] = await Promise.all([
+        api('/portfolios'),
+        api('/portfolio-trades?include_history=true').catch(() => ({})),
+      ]);
+      setPortfolios(pf || []);
+      if (hist?.byPortfolio) setDailyData(hist.byPortfolio);
+    } catch {}
+    setLoading(false);
+  }
+
+  // Load trades for a portfolio when expanded
+  async function loadPortfolioTrades(portfolioId) {
+    if (pfTrades[portfolioId]) return; // already loaded
+    setPfTradesLoading(portfolioId);
+    try {
+      const data = await api(`/portfolio-trades?portfolio_id=${portfolioId}`);
+      setPfTrades(prev => ({ ...prev, [portfolioId]: Array.isArray(data) ? data : [] }));
+    } catch {
+      setPfTrades(prev => ({ ...prev, [portfolioId]: [] }));
+    }
+    setPfTradesLoading(null);
+  }
+
+  function handleToggle(id) {
+    if (expanded === id) {
+      setExpanded(null);
+    } else {
+      setExpanded(id);
+      loadPortfolioTrades(id);
+    }
+  }
+
+  const filtered = useMemo(() => {
+    let list = [...portfolios];
+    if (filter === 'active') list = list.filter(p => p.trades_won + p.trades_lost > 0);
+    if (filter === 'dormant') list = list.filter(p => p.trades_won + p.trades_lost === 0);
+    if (filter === 'etf') list = list.filter(p => p.strategy.startsWith('etf_'));
+    if (filter === 'core') list = list.filter(p => !p.strategy.startsWith('etf_'));
+
+    if (sort === 'pnl')
+      list.sort((a, b) =>
+        (b.total_returned_cents - b.total_invested_cents) -
+        (a.total_returned_cents - a.total_invested_cents)
+      );
+    if (sort === 'winrate') list.sort((a, b) => (b.win_rate || 0) - (a.win_rate || 0));
+    if (sort === 'invested') list.sort((a, b) => b.total_invested_cents - a.total_invested_cents);
+    if (sort === 'name') list.sort((a, b) => a.name.localeCompare(b.name));
+    if (sort === 'id') list.sort((a, b) => a.id - b.id);
+    return list;
+  }, [portfolios, filter, sort]);
+
+  const stats = useMemo(() => {
+    const totalInvested = portfolios.reduce((s, p) => s + p.total_invested_cents, 0);
+    const totalReturned = portfolios.reduce((s, p) => s + p.total_returned_cents, 0);
+    const totalWins = portfolios.reduce((s, p) => s + p.trades_won, 0);
+    const totalLosses = portfolios.reduce((s, p) => s + p.trades_lost, 0);
+    const active = portfolios.filter(p => p.trades_won + p.trades_lost > 0).length;
+    const dormant = portfolios.length - active;
+    const bestPf = [...portfolios].sort(
+      (a, b) =>
+        (b.total_returned_cents - b.total_invested_cents) -
+        (a.total_returned_cents - a.total_invested_cents)
+    )[0];
+    const worstPf = [...portfolios].sort(
+      (a, b) =>
+        (a.total_returned_cents - a.total_invested_cents) -
+        (b.total_returned_cents - b.total_invested_cents)
+    )[0];
+    return { totalInvested, totalReturned, totalWins, totalLosses, active, dormant, bestPf, worstPf };
+  }, [portfolios]);
+
+  const barData = useMemo(() =>
+    [...portfolios]
+      .map(p => ({ name: p.name, pnl: (p.total_returned_cents - p.total_invested_cents) / 100, strategy: p.strategy }))
+      .filter(p => p.pnl !== 0)
+      .sort((a, b) => b.pnl - a.pnl)
+      .slice(0, 20),
+    [portfolios]
+  );
+
+  const pieData = useMemo(() => {
+    const cats = {};
+    portfolios.forEach(p => {
+      const cat = p.strategy.startsWith('etf_') ? 'ETF Baskets' : p.strategy;
+      if (!cats[cat]) cats[cat] = { name: cat, value: 0 };
+      cats[cat].value += p.total_invested_cents;
+    });
+    return Object.values(cats).filter(c => c.value > 0).sort((a, b) => b.value - a.value);
+  }, [portfolios]);
+
+  if (loading) return <Spinner />;
+
+  const pnl = (stats.totalReturned - stats.totalInvested) / 100;
+  const totalTrades = stats.totalWins + stats.totalLosses;
+
+  return (
+    <div className="space-y-6 fade-in">
+
+      {/* ── Page Header ── */}
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="heading text-3xl" style={{ color: 'var(--text)' }}>
+            Portfauxlios
+            <span className="badge bg-cyan ml-3 align-middle text-base">
+              {portfolios.length}
+            </span>
+          </h1>
+          <p className="section-subtitle mt-1">
+            All {portfolios.length} portfolios — performance, P&L, and strategy breakdown
+          </p>
+        </div>
+        <button onClick={load} className="btn btn-o btn-sm">Refresh</button>
+      </div>
+
+      {/* ── Summary Stats ── */}
+      <div className="grid grid-cols-3 md:grid-cols-6 gap-3">
+        {[
+          {
+            label: 'Total P&L',
+            value: `${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)}`,
+            color: pnl >= 0 ? 'var(--green)' : 'var(--red)',
+          },
+          {
+            label: 'Invested',
+            value: `$${(stats.totalInvested / 100).toFixed(2)}`,
+            color: 'var(--orange)',
+          },
+          {
+            label: 'Returned',
+            value: `$${(stats.totalReturned / 100).toFixed(2)}`,
+            color: 'var(--blue)',
+          },
+          {
+            label: 'Win Rate',
+            value: totalTrades ? `${((stats.totalWins / totalTrades) * 100).toFixed(1)}%` : '0%',
+            color: 'var(--purple)',
+          },
+          { label: 'Active', value: stats.active, color: 'var(--green)' },
+          { label: 'Dormant', value: stats.dormant, color: 'var(--text-muted)' },
+        ].map(s => (
+          <div key={s.label} className="card text-center" style={{ padding: '1rem' }}>
+            <div className="stat-v" style={{ color: s.color, fontSize: '1.4rem' }}>
+              {s.value}
+            </div>
+            <div className="stat-label mt-1">{s.label}</div>
+          </div>
+        ))}
+      </div>
+
+      {/* ── Charts Row ── */}
+      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+        {/* P&L Bar Chart */}
+        <div className="card md:col-span-2">
+          <h2 className="heading text-sm mb-3" style={{ color: 'var(--text-dim)' }}>
+            P&L by Portfolio
+          </h2>
+          {barData.length > 0 ? (
+            <ResponsiveContainer width="100%" height={220}>
+              <BarChart data={barData} margin={{ left: 10, right: 10 }}>
+                <XAxis
+                  dataKey="name"
+                  tick={{ fill: 'var(--text-muted)', fontSize: 9, fontFamily: 'var(--font-mono)' }}
+                  angle={-45}
+                  textAnchor="end"
+                  height={60}
+                />
+                <YAxis
+                  tick={{ fill: 'var(--text-muted)', fontSize: 10, fontFamily: 'var(--font-mono)' }}
+                  tickFormatter={v => `$${v}`}
+                />
+                <Tooltip
+                  contentStyle={{
+                    background: 'var(--surface-el)',
+                    border: '1px solid var(--border)',
+                    borderRadius: 8,
+                    fontSize: 11,
+                    fontFamily: 'var(--font-mono)',
+                  }}
+                  formatter={v => [`$${v.toFixed(2)}`, 'P&L']}
+                />
+                <Bar dataKey="pnl" radius={[4, 4, 0, 0]}>
+                  {barData.map((d, i) => (
+                    <Cell
+                      key={i}
+                      fill={d.pnl >= 0 ? 'var(--green)' : 'var(--red)'}
+                      fillOpacity={0.8}
+                    />
+                  ))}
+                </Bar>
+              </BarChart>
+            </ResponsiveContainer>
+          ) : (
+            <div className="text-center py-10 text-sm" style={{ color: 'var(--text-muted)' }}>
+              No P&L data yet
+            </div>
+          )}
+        </div>
+
+        {/* Allocation Pie */}
+        <div className="card">
+          <h2 className="heading text-sm mb-3" style={{ color: 'var(--text-dim)' }}>
+            Capital Allocation
+          </h2>
+          {pieData.length > 0 ? (
+            <ResponsiveContainer width="100%" height={220}>
+              <PieChart>
+                <Pie
+                  data={pieData}
+                  dataKey="value"
+                  nameKey="name"
+                  cx="50%"
+                  cy="50%"
+                  innerRadius={40}
+                  outerRadius={75}
+                  paddingAngle={2}
+                  strokeWidth={0}
+                >
+                  {pieData.map((d, i) => (
+                    <Cell
+                      key={i}
+                      fill={getColor(d.name === 'ETF Baskets' ? 'etf_' : d.name)}
+                      fillOpacity={0.85}
+                    />
+                  ))}
+                </Pie>
+                <Tooltip
+                  contentStyle={{
+                    background: 'var(--surface-el)',
+                    border: '1px solid var(--border)',
+                    borderRadius: 8,
+                    fontSize: 11,
+                    fontFamily: 'var(--font-mono)',
+                  }}
+                  formatter={v => [`$${(v / 100).toFixed(2)}`, 'Invested']}
+                />
+              </PieChart>
+            </ResponsiveContainer>
+          ) : (
+            <div className="text-center py-10 text-sm" style={{ color: 'var(--text-muted)' }}>
+              No allocations yet
+            </div>
+          )}
+        </div>
+      </div>
+
+      {/* ── Best / Worst Performers ── */}
+      <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+        {stats.bestPf && (
+          <div
+            className="card"
+            style={{ borderColor: 'rgba(0,195,137,0.25)', background: 'var(--green-dim)' }}
+          >
+            <div className="flex items-center gap-2 mb-2">
+              <span
+                className="badge"
+                style={{ background: 'var(--green-dim)', color: 'var(--green)' }}
+              >
+                Best Performer
+              </span>
+            </div>
+            <div className="flex items-center justify-between">
+              <span className="heading font-bold" style={{ color: 'var(--text)' }}>
+                {stats.bestPf.name}
+              </span>
+              <span className="mono font-bold text-green">
+                +${((stats.bestPf.total_returned_cents - stats.bestPf.total_invested_cents) / 100).toFixed(2)}
+              </span>
+            </div>
+            <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
+              {stats.bestPf.strategy} — {stats.bestPf.win_rate || 0}% win rate
+            </span>
+          </div>
+        )}
+        {stats.worstPf && (
+          <div
+            className="card"
+            style={{ borderColor: 'rgba(255,92,92,0.25)', background: 'var(--red-dim)' }}
+          >
+            <div className="flex items-center gap-2 mb-2">
+              <span
+                className="badge"
+                style={{ background: 'var(--red-dim)', color: 'var(--red)' }}
+              >
+                Worst Performer
+              </span>
+            </div>
+            <div className="flex items-center justify-between">
+              <span className="heading font-bold" style={{ color: 'var(--text)' }}>
+                {stats.worstPf.name}
+              </span>
+              <span className="mono font-bold text-red">
+                ${((stats.worstPf.total_returned_cents - stats.worstPf.total_invested_cents) / 100).toFixed(2)}
+              </span>
+            </div>
+            <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
+              {stats.worstPf.strategy} — {stats.worstPf.win_rate || 0}% win rate
+            </span>
+          </div>
+        )}
+      </div>
+
+      {/* ── Filter & Sort Controls ── */}
+      <div className="flex flex-wrap items-center justify-between gap-3">
+        <div className="flex flex-wrap gap-2">
+          {['all', 'active', 'dormant', 'etf', 'core'].map(f => (
+            <button
+              key={f}
+              onClick={() => setFilter(f)}
+              className="px-3 py-1.5 rounded-lg text-xs font-semibold transition"
+              style={
+                filter === f
+                  ? {
+                      background: 'var(--cyan-dim)',
+                      color: 'var(--cyan)',
+                      border: '1px solid rgba(0,240,255,0.3)',
+                    }
+                  : {
+                      background: 'var(--surface-el)',
+                      color: 'var(--text-muted)',
+                      border: '1px solid var(--border)',
+                    }
+              }
+            >
+              {f === 'all'
+                ? `All (${portfolios.length})`
+                : f === 'active'
+                ? `Active (${stats.active})`
+                : f === 'dormant'
+                ? `Dormant (${stats.dormant})`
+                : f === 'etf'
+                ? 'ETF Baskets'
+                : 'Core Strategies'}
+            </button>
+          ))}
+        </div>
+        <div className="flex items-center gap-2">
+          <span className="text-xs" style={{ color: 'var(--text-muted)' }}>Sort:</span>
+          {['pnl', 'winrate', 'invested', 'name', 'id'].map(s => (
+            <button
+              key={s}
+              onClick={() => setSort(s)}
+              className="px-2 py-1 rounded text-xs font-medium transition"
+              style={
+                sort === s
+                  ? { color: 'var(--cyan)', background: 'var(--cyan-dim)' }
+                  : { color: 'var(--text-muted)' }
+              }
+            >
+              {s === 'pnl' ? 'P&L' : s === 'winrate' ? 'Win%' : s.charAt(0).toUpperCase() + s.slice(1)}
+            </button>
+          ))}
+        </div>
+      </div>
+
+      {/* ── Portfolio Card Grid ── */}
+      {filtered.length === 0 ? (
+        <div
+          className="text-center py-16 text-sm"
+          style={{ color: 'var(--text-muted)' }}
+        >
+          No portfolios match this filter
+        </div>
+      ) : (
+        <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
+          {filtered.map(p => (
+            <PortfolioCard
+              key={p.id}
+              p={p}
+              dailyPnl={dailyData[p.id]?.daily || []}
+              expanded={expanded === p.id}
+              onToggle={() => handleToggle(p.id)}
+              pfTrades={pfTrades[p.id] || null}
+              pfTradesLoading={pfTradesLoading === p.id}
+            />
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Settings.jsx b/kalshi-dash/src/pages/Settings.jsx
new file mode 100644
index 0000000..24a1155
--- /dev/null
+++ b/kalshi-dash/src/pages/Settings.jsx
@@ -0,0 +1,602 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+
+// ── Interval presets ──
+const INTERVAL_PRESETS = [
+  { label: '30s', ms: 30000 },
+  { label: '1m', ms: 60000 },
+  { label: '2m', ms: 120000 },
+  { label: '5m', ms: 300000 },
+  { label: '8m', ms: 480000 },
+  { label: '10m', ms: 600000 },
+  { label: '15m', ms: 900000 },
+  { label: '30m', ms: 1800000 },
+  { label: '60m', ms: 3600000 },
+];
+
+function msToLabel(ms) {
+  if (ms >= 3600000) return `${ms / 3600000}h`;
+  if (ms >= 60000) return `${ms / 60000}m`;
+  return `${ms / 1000}s`;
+}
+
+// ── Gear icon ──
+function GearIcon() {
+  return (
+    <svg
+      width="22"
+      height="22"
+      viewBox="0 0 24 24"
+      fill="none"
+      stroke="currentColor"
+      strokeWidth="2"
+      strokeLinecap="round"
+      strokeLinejoin="round"
+    >
+      <circle cx="12" cy="12" r="3" />
+      <path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" />
+    </svg>
+  );
+}
+
+// ── Section panel ──
+function SettingsPanel({ title, children }) {
+  return (
+    <div className="card space-y-4">
+      <h2
+        className="heading font-semibold text-base pb-3"
+        style={{
+          color: 'var(--text)',
+          borderBottom: '1px solid var(--border)',
+        }}
+      >
+        {title}
+      </h2>
+      {children}
+    </div>
+  );
+}
+
+// ── Label + description row ──
+function FieldRow({ label, description, children }) {
+  return (
+    <div className="flex items-start justify-between gap-4">
+      <div className="flex-1 min-w-0">
+        <p className="text-sm font-medium" style={{ color: 'var(--text)' }}>{label}</p>
+        {description && (
+          <p className="text-xs mt-0.5" style={{ color: 'var(--text-muted)' }}>{description}</p>
+        )}
+      </div>
+      <div className="flex-shrink-0">{children}</div>
+    </div>
+  );
+}
+
+// ── Toggle switch ──
+function Toggle({ active, onChange }) {
+  return (
+    <div
+      className={`toggle ${active ? 'active' : ''}`}
+      onClick={() => onChange(!active)}
+      role="switch"
+      aria-checked={active}
+      tabIndex={0}
+      onKeyDown={e => (e.key === ' ' || e.key === 'Enter') && onChange(!active)}
+    />
+  );
+}
+
+// ── Interval control ──
+function IntervalControl({ label, value, onChange, description }) {
+  return (
+    <div className="space-y-2">
+      <FieldRow label={label} description={description}>
+        <span
+          className="mono font-bold text-lg"
+          style={{ color: 'var(--cyan)' }}
+        >
+          {msToLabel(value)}
+        </span>
+      </FieldRow>
+      <div className="flex flex-wrap gap-1.5">
+        {INTERVAL_PRESETS.map(p => (
+          <button
+            key={p.ms}
+            onClick={() => onChange(p.ms)}
+            className="px-2.5 py-1 rounded text-xs font-medium transition"
+            style={
+              value === p.ms
+                ? {
+                    background: 'var(--cyan-dim)',
+                    color: 'var(--cyan)',
+                    border: '1px solid rgba(0,240,255,0.4)',
+                  }
+                : {
+                    background: 'var(--surface-el)',
+                    color: 'var(--text-muted)',
+                    border: '1px solid var(--border)',
+                  }
+            }
+          >
+            {p.label}
+          </button>
+        ))}
+      </div>
+    </div>
+  );
+}
+
+// ── Inline feedback message ──
+function Msg({ msg }) {
+  if (!msg) return null;
+  return (
+    <p
+      className="text-xs px-3 py-2 rounded-lg"
+      style={
+        msg.type === 'ok'
+          ? { background: 'var(--green-dim)', color: 'var(--green)' }
+          : { background: 'var(--red-dim)', color: 'var(--red)' }
+      }
+    >
+      {msg.text}
+    </p>
+  );
+}
+
+// ── Spinner ──
+function Spinner() {
+  return (
+    <div className="flex items-center justify-center h-64">
+      <div
+        className="w-9 h-9 rounded-full border-2 animate-spin"
+        style={{ borderColor: 'var(--border)', borderTopColor: 'var(--cyan)' }}
+      />
+    </div>
+  );
+}
+
+export default function Settings() {
+  const [status, setStatus] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [apiKey, setApiKey] = useState('');
+  const [privateKey, setPrivateKey] = useState('');
+  const [env, setEnv] = useState('demo');
+  const [saving, setSaving] = useState(false);
+  const [msg, setMsg] = useState(null);
+
+  // Scan config state
+  const [scanCfg, setScanCfg] = useState(null);
+  const [scanMsg, setScanMsg] = useState(null);
+  const [scanSaving, setScanSaving] = useState(false);
+
+  useEffect(() => {
+    loadStatus();
+    loadScanConfig();
+    const iv = setInterval(loadScanConfig, 30000);
+    return () => clearInterval(iv);
+  }, []);
+
+  async function loadStatus() {
+    try {
+      const r = await post('/kalshi', { action: 'status' });
+      setStatus(r);
+      if (r.env) setEnv(r.env);
+    } catch {}
+    setLoading(false);
+  }
+
+  async function loadScanConfig() {
+    try {
+      const r = await api('/scan-config');
+      setScanCfg(r);
+    } catch {}
+  }
+
+  async function updateScanField(field, value) {
+    setScanSaving(true);
+    setScanMsg(null);
+    try {
+      const r = await api('/scan-config', {
+        method: 'POST',
+        body: JSON.stringify({ [field]: value }),
+      });
+      if (r.success) {
+        setScanCfg(r);
+        setScanMsg({ type: 'ok', text: `Updated ${field.replace(/_/g, ' ')}` });
+      } else {
+        setScanMsg({ type: 'err', text: r.error || 'Failed' });
+      }
+    } catch {
+      setScanMsg({ type: 'err', text: 'Connection error' });
+    }
+    setScanSaving(false);
+    setTimeout(() => setScanMsg(null), 3000);
+  }
+
+  async function saveKeys() {
+    setSaving(true);
+    setMsg(null);
+    try {
+      const r = await post('/kalshi', {
+        action: 'save_keys',
+        api_key: apiKey,
+        private_key: privateKey,
+        env,
+      });
+      if (r.success) {
+        setMsg({ type: 'ok', text: 'Keys saved and connected' });
+        loadStatus();
+        setApiKey('');
+        setPrivateKey('');
+      } else {
+        setMsg({ type: 'err', text: r.error || 'Failed' });
+      }
+    } catch {
+      setMsg({ type: 'err', text: 'Connection error' });
+    }
+    setSaving(false);
+  }
+
+  async function disconnect() {
+    try {
+      await post('/kalshi', { action: 'disconnect' });
+      loadStatus();
+    } catch {}
+  }
+
+  async function testConnection() {
+    setMsg(null);
+    try {
+      const r = await post('/kalshi', { action: 'test_connection' });
+      setMsg({
+        type: r.success ? 'ok' : 'err',
+        text: r.success
+          ? `Connected. Exchange: ${r.exchange_status}`
+          : 'Connection failed',
+      });
+    } catch {
+      setMsg({ type: 'err', text: 'Connection error' });
+    }
+  }
+
+  if (loading) return <Spinner />;
+
+  return (
+    <div className="space-y-6 fade-in">
+
+      {/* ── Page Header ── */}
+      <div className="flex items-center gap-3">
+        <div
+          className="flex items-center justify-center w-10 h-10 rounded-xl"
+          style={{ background: 'var(--cyan-dim)', color: 'var(--cyan)' }}
+        >
+          <GearIcon />
+        </div>
+        <div>
+          <h1 className="heading text-3xl" style={{ color: 'var(--text)' }}>Settings</h1>
+          <p className="section-subtitle mt-0.5">Configure scan pipeline, API keys, and risk parameters</p>
+        </div>
+      </div>
+
+      {/* ══════════════════════════════
+          SCAN & PIPELINE CONFIG
+          ══════════════════════════════ */}
+      {scanCfg && (
+        <SettingsPanel title="Scan & Pipeline Configuration">
+          <div className="flex items-center justify-between">
+            <p className="text-xs" style={{ color: 'var(--text-muted)' }}>
+              Adjust intervals, thresholds, and risk parameters in real-time
+            </p>
+            {scanSaving && (
+              <span className="text-xs animate-pulse" style={{ color: 'var(--orange)' }}>
+                Saving...
+              </span>
+            )}
+          </div>
+
+          <Msg msg={scanMsg} />
+
+          {/* Cached markets indicator */}
+          <div
+            className="flex items-center gap-3 px-4 py-3 rounded-xl"
+            style={{
+              background: 'var(--surface-el)',
+              border: '1px solid var(--border)',
+            }}
+          >
+            <span className="stat-label">Cached Markets</span>
+            <span className="mono font-bold text-xl" style={{ color: 'var(--cyan)' }}>
+              {scanCfg.cached_markets || 0}
+            </span>
+            <div
+              className="w-2 h-2 rounded-full pulse-cyan ml-auto"
+              style={{ background: 'var(--cyan)' }}
+            />
+          </div>
+
+          {/* Interval controls */}
+          <div
+            className="space-y-5 pt-2 pb-1"
+            style={{ borderTop: '1px solid var(--border)' }}
+          >
+            <h3 className="text-sm font-semibold pt-3" style={{ color: 'var(--text-dim)' }}>
+              Pipeline Intervals
+            </h3>
+
+            <IntervalControl
+              label="Market Scan"
+              description="How often to fetch and analyze all active Kalshi markets"
+              value={scanCfg.scan_interval_ms}
+              onChange={v => updateScanField('scan_interval_ms', v)}
+            />
+
+            <IntervalControl
+              label="Signal Pipeline"
+              description="News sentiment + probability + Monte Carlo signal generation"
+              value={scanCfg.signal_pipeline_ms}
+              onChange={v => updateScanField('signal_pipeline_ms', v)}
+            />
+
+            <IntervalControl
+              label="Intelligence Sweep"
+              description="Reddit, news feeds, Bluesky, Mastodon, Lemmy + 20 more sources"
+              value={scanCfg.reddit_interval_ms}
+              onChange={v => updateScanField('reddit_interval_ms', v)}
+            />
+
+            <IntervalControl
+              label="Trade Resolution"
+              description="Take-profit / stop-loss check on open portfolio positions"
+              value={scanCfg.resolve_interval_ms}
+              onChange={v => updateScanField('resolve_interval_ms', v)}
+            />
+          </div>
+
+          {/* Risk Parameters */}
+          <div className="space-y-5" style={{ borderTop: '1px solid var(--border)', paddingTop: '1rem' }}>
+            <h3 className="text-sm font-semibold" style={{ color: 'var(--text-dim)' }}>
+              Risk Parameters
+            </h3>
+
+            {/* Min Edge Threshold */}
+            <div className="space-y-2">
+              <FieldRow
+                label="Min Edge Threshold"
+                description="Minimum statistical edge required to approve a signal"
+              >
+                <span className="mono font-bold text-lg" style={{ color: 'var(--cyan)' }}>
+                  {(scanCfg.min_edge_threshold * 100).toFixed(1)}%
+                </span>
+              </FieldRow>
+              <div className="flex flex-wrap gap-1.5">
+                {[0.01, 0.02, 0.03, 0.05, 0.08, 0.10, 0.15, 0.20].map(v => (
+                  <button
+                    key={v}
+                    onClick={() => updateScanField('min_edge_threshold', v)}
+                    className="px-2.5 py-1 rounded text-xs font-medium transition"
+                    style={
+                      Math.abs(scanCfg.min_edge_threshold - v) < 0.001
+                        ? {
+                            background: 'var(--cyan-dim)',
+                            color: 'var(--cyan)',
+                            border: '1px solid rgba(0,240,255,0.4)',
+                          }
+                        : {
+                            background: 'var(--surface-el)',
+                            color: 'var(--text-muted)',
+                            border: '1px solid var(--border)',
+                          }
+                    }
+                  >
+                    {(v * 100).toFixed(0)}%
+                  </button>
+                ))}
+              </div>
+            </div>
+
+            {/* Min Liquidity */}
+            <div className="space-y-2">
+              <FieldRow
+                label="Min Liquidity (24h Vol)"
+                description="Minimum 24-hour trading volume to consider a market"
+              >
+                <span className="mono font-bold text-lg" style={{ color: 'var(--cyan)' }}>
+                  {scanCfg.min_liquidity}
+                </span>
+              </FieldRow>
+              <div className="flex flex-wrap gap-1.5">
+                {[0, 10, 25, 50, 100, 250, 500, 1000].map(v => (
+                  <button
+                    key={v}
+                    onClick={() => updateScanField('min_liquidity', v)}
+                    className="px-2.5 py-1 rounded text-xs font-medium transition"
+                    style={
+                      scanCfg.min_liquidity === v
+                        ? {
+                            background: 'var(--cyan-dim)',
+                            color: 'var(--cyan)',
+                            border: '1px solid rgba(0,240,255,0.4)',
+                          }
+                        : {
+                            background: 'var(--surface-el)',
+                            color: 'var(--text-muted)',
+                            border: '1px solid var(--border)',
+                          }
+                    }
+                  >
+                    {v === 0 ? 'None' : v}
+                  </button>
+                ))}
+              </div>
+            </div>
+          </div>
+        </SettingsPanel>
+      )}
+
+      {/* ══════════════════════════════
+          API CONFIGURATION
+          ══════════════════════════════ */}
+      <SettingsPanel title="API Configuration">
+        {/* Connection status indicator */}
+        <div className="flex items-center gap-3 mb-2">
+          <div
+            className={`w-2.5 h-2.5 rounded-full ${status?.connected ? 'pulse-cyan' : ''}`}
+            style={{ background: status?.connected ? 'var(--cyan)' : 'var(--text-muted)' }}
+          />
+          <span
+            className="text-sm font-semibold"
+            style={{ color: status?.connected ? 'var(--cyan)' : 'var(--text-muted)' }}
+          >
+            {status?.connected ? 'Connected' : 'Not Connected'}
+          </span>
+        </div>
+
+        {status?.connected ? (
+          <div className="space-y-3">
+            <div
+              className="rounded-xl px-4 py-3 space-y-2"
+              style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+            >
+              <div className="flex justify-between text-sm">
+                <span style={{ color: 'var(--text-muted)' }}>API Key</span>
+                <span className="mono" style={{ color: 'var(--text)' }}>
+                  {status.api_key_masked || '•••••'}
+                </span>
+              </div>
+              <div className="flex justify-between text-sm">
+                <span style={{ color: 'var(--text-muted)' }}>Environment</span>
+                <span
+                  className="mono font-bold"
+                  style={{ color: status.env === 'prod' ? 'var(--green)' : 'var(--orange)' }}
+                >
+                  {(status.env || 'demo').toUpperCase()}
+                </span>
+              </div>
+            </div>
+            <div className="flex gap-2">
+              <button onClick={testConnection} className="btn btn-o btn-sm">
+                Test Connection
+              </button>
+              <button onClick={disconnect} className="btn btn-d btn-sm">
+                Disconnect
+              </button>
+            </div>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            {/* Environment toggle */}
+            <div className="flex gap-2">
+              <button
+                onClick={() => setEnv('demo')}
+                className="flex-1 py-2 rounded-xl text-sm font-semibold transition"
+                style={
+                  env === 'demo'
+                    ? {
+                        background: 'var(--orange-dim)',
+                        color: 'var(--orange)',
+                        border: '1px solid rgba(247,147,26,0.4)',
+                      }
+                    : {
+                        background: 'var(--surface-el)',
+                        color: 'var(--text-muted)',
+                        border: '1px solid var(--border)',
+                      }
+                }
+              >
+                Demo
+              </button>
+              <button
+                onClick={() => setEnv('prod')}
+                className="flex-1 py-2 rounded-xl text-sm font-semibold transition"
+                style={
+                  env === 'prod'
+                    ? {
+                        background: 'var(--green-dim)',
+                        color: 'var(--green)',
+                        border: '1px solid rgba(0,195,137,0.4)',
+                      }
+                    : {
+                        background: 'var(--surface-el)',
+                        color: 'var(--text-muted)',
+                        border: '1px solid var(--border)',
+                      }
+                }
+              >
+                Production
+              </button>
+            </div>
+
+            <div>
+              <label className="stat-label block mb-1.5">API Key ID</label>
+              <input
+                type="text"
+                value={apiKey}
+                onChange={e => setApiKey(e.target.value)}
+                placeholder="Your API key"
+                className="input mono"
+              />
+            </div>
+            <div>
+              <label className="stat-label block mb-1.5">Private Key (PEM)</label>
+              <textarea
+                value={privateKey}
+                onChange={e => setPrivateKey(e.target.value)}
+                placeholder="-----BEGIN RSA PRIVATE KEY-----"
+                rows={4}
+                className="input mono text-xs"
+                style={{ resize: 'vertical' }}
+              />
+            </div>
+            <button
+              onClick={saveKeys}
+              disabled={saving || !apiKey}
+              className="btn btn-p w-full"
+              style={saving || !apiKey ? { opacity: 0.5, cursor: 'not-allowed' } : {}}
+            >
+              {saving ? 'Saving...' : 'Save & Connect'}
+            </button>
+          </div>
+        )}
+
+        <Msg msg={msg} />
+      </SettingsPanel>
+
+      {/* ══════════════════════════════
+          API REFERENCE
+          ══════════════════════════════ */}
+      <SettingsPanel title="Ken API Reference">
+        <div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-xs" style={{ color: 'var(--text-muted)' }}>
+          <div className="space-y-2">
+            <p className="stat-label mb-2">Environments</p>
+            <div className="space-y-1">
+              <div className="flex gap-2">
+                <span>Demo:</span>
+                <span className="mono" style={{ color: 'var(--orange)' }}>demo-api.kalshi.co</span>
+              </div>
+              <div className="flex gap-2">
+                <span>Prod:</span>
+                <span className="mono" style={{ color: 'var(--green)' }}>api.elections.kalshi.com</span>
+              </div>
+            </div>
+          </div>
+          <div className="space-y-2">
+            <p className="stat-label mb-2">Authentication</p>
+            <div className="space-y-1">
+              <div className="flex gap-2">
+                <span>Method:</span>
+                <span className="mono" style={{ color: 'var(--text-dim)' }}>RSA-PSS + SHA-256</span>
+              </div>
+              <div>
+                <span>Headers: </span>
+                <span className="mono" style={{ color: 'var(--text-dim)' }}>
+                  KALSHI-ACCESS-KEY, TIMESTAMP, SIGNATURE
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </SettingsPanel>
+
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Signals.jsx b/kalshi-dash/src/pages/Signals.jsx
new file mode 100644
index 0000000..fdb5b13
--- /dev/null
+++ b/kalshi-dash/src/pages/Signals.jsx
@@ -0,0 +1,482 @@
+import React, { useState, useEffect } from 'react';
+import { api } from '../api';
+
+const DIR_BG = { BUY_YES: 'bg-green', BUY_NO: 'bg-red' };
+
+/* Extract human-readable title from reasoning: 'BUY_YES on "Will X happen?" — edge...' */
+function extractTitle(reasoning) {
+  if (!reasoning) return null;
+  const m = reasoning.match(/on\s+"([^"]+)"/);
+  return m ? m[1] : null;
+}
+
+/* ─── Confidence meter ─── */
+function ConfBar({ value }) {
+  const pct = Math.max(0, Math.min(100, value * 100));
+  const fillColor = value > 0.7 ? 'var(--green)' : value > 0.5 ? 'var(--orange)' : 'var(--red)';
+  return (
+    <div className="flex items-center gap-2">
+      <div className="progress-bar flex-1" style={{ minWidth: 64 }}>
+        <div
+          className="progress-fill"
+          style={{
+            width: `${pct}%`,
+            background: `linear-gradient(90deg, ${fillColor}, ${fillColor}cc)`,
+          }}
+        />
+      </div>
+      <span className="mono text-xs" style={{ color: 'var(--text-dim)' }}>
+        {pct.toFixed(0)}%
+      </span>
+    </div>
+  );
+}
+
+/* ─── Strategy signal expandable row ─── */
+function SignalRow({ s }) {
+  const [expanded, setExpanded] = useState(false);
+  const title = extractTitle(s.reasoning) || s.market_id;
+  const edge = parseFloat(s.expected_edge) || 0;
+  const conf = parseFloat(s.confidence) || 0;
+
+  return (
+    <>
+      <tr
+        className="cursor-pointer transition"
+        onClick={() => setExpanded(!expanded)}
+        style={{ background: expanded ? 'rgba(0,240,255,0.04)' : undefined }}
+      >
+        {/* Time */}
+        <td className="whitespace-nowrap">
+          <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
+            {new Date(s.created_at).toLocaleTimeString()}
+          </span>
+        </td>
+
+        {/* Market */}
+        <td className="max-w-[280px]">
+          <a
+            href={s.kalshi_url || `https://kalshi.com/search?query=${encodeURIComponent((s.market_id || '').split('-')[0])}`}
+            target="_blank"
+            rel="noopener noreferrer"
+            className="text-sm truncate block hover:underline transition heading"
+            style={{ color: 'var(--cyan)' }}
+            onClick={ev => ev.stopPropagation()}
+          >
+            {title}
+          </a>
+          <span className="mono text-[10px]" style={{ color: 'var(--text-muted)' }}>
+            {s.market_id}
+          </span>
+        </td>
+
+        {/* Direction badge */}
+        <td>
+          <span className={`badge ${DIR_BG[s.direction] || 'bg-yellow'}`}>
+            {s.direction}
+          </span>
+        </td>
+
+        {/* Edge — large cyan mono */}
+        <td>
+          <span
+            className="mono font-bold text-base"
+            style={{ color: edge > 0 ? 'var(--cyan)' : 'var(--red)' }}
+          >
+            {edge > 0 ? '+' : ''}{(edge * 100).toFixed(1)}%
+          </span>
+        </td>
+
+        {/* Size */}
+        <td>
+          <span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
+            {s.size_recommendation || '—'}
+          </span>
+        </td>
+
+        {/* Confidence meter */}
+        <td style={{ minWidth: 140 }}>
+          <ConfBar value={conf} />
+        </td>
+
+        {/* Risk */}
+        <td>
+          <span className={`badge ${s.risk_approved ? 'bg-green' : 'bg-red'}`}>
+            {s.risk_approved ? 'APPROVED' : 'BLOCKED'}
+          </span>
+        </td>
+
+        {/* Expand chevron */}
+        <td>
+          <span style={{ color: 'var(--text-muted)' }}>{expanded ? '▾' : '▸'}</span>
+        </td>
+      </tr>
+
+      {/* Expanded reasoning */}
+      {expanded && (
+        <tr style={{ background: 'rgba(0,240,255,0.02)' }}>
+          <td colSpan={8} className="px-4 py-3">
+            <p className="text-xs leading-relaxed fade-in" style={{ color: 'var(--text-dim)' }}>
+              {s.reasoning}
+            </p>
+            {s.sources && (
+              <div className="mt-2 flex flex-wrap gap-1">
+                {(typeof s.sources === 'string'
+                  ? s.sources.split(',')
+                  : Array.isArray(s.sources) ? s.sources : []
+                ).map((src, i) => (
+                  <span key={i} className="badge bg-blue text-[10px]">{src.trim()}</span>
+                ))}
+              </div>
+            )}
+          </td>
+        </tr>
+      )}
+    </>
+  );
+}
+
+/* ─── Probability shift row ─── */
+function ShiftRow({ s }) {
+  const edge = parseFloat(s.edge) || 0;
+  const sentImpact = parseFloat(s.sentiment_impact) || 0;
+  const title = s.market_title || s.market_id;
+
+  return (
+    <tr>
+      <td className="whitespace-nowrap">
+        <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
+          {new Date(s.created_at).toLocaleTimeString()}
+        </span>
+      </td>
+
+      <td className="max-w-[250px]">
+        <a
+          href={s.kalshi_url || `https://kalshi.com/search?query=${encodeURIComponent((s.market_id || '').split('-')[0])}`}
+          target="_blank"
+          rel="noopener noreferrer"
+          className="text-sm truncate block hover:underline transition heading"
+          style={{ color: 'var(--cyan)' }}
+        >
+          {title}
+        </a>
+        <span className="mono text-[10px]" style={{ color: 'var(--text-muted)' }}>
+          {s.market_id}
+        </span>
+      </td>
+
+      <td>
+        <span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
+          {(parseFloat(s.prior_probability) * 100).toFixed(1)}%
+        </span>
+      </td>
+
+      <td>
+        <span className="mono text-sm font-semibold" style={{ color: sentImpact > 0 ? 'var(--green)' : 'var(--red)' }}>
+          {sentImpact > 0 ? '+' : ''}{(sentImpact * 100).toFixed(2)}%
+        </span>
+      </td>
+
+      <td>
+        <span className="mono text-sm" style={{ color: 'var(--orange)' }}>
+          {(parseFloat(s.adjusted_probability) * 100).toFixed(1)}%
+        </span>
+      </td>
+
+      <td>
+        <span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
+          {(parseFloat(s.market_probability) * 100).toFixed(1)}%
+        </span>
+      </td>
+
+      <td>
+        <span
+          className="mono text-sm font-bold"
+          style={{ color: edge > 0 ? 'var(--green)' : edge < 0 ? 'var(--red)' : 'var(--text-muted)' }}
+        >
+          {edge > 0 ? '+' : ''}{(edge * 100).toFixed(1)}%
+        </span>
+      </td>
+
+      <td>
+        <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
+          {s.num_articles}
+        </span>
+      </td>
+    </tr>
+  );
+}
+
+/* ─── Source card ─── */
+function SourceCard({ s }) {
+  const sent = parseFloat(s.avg_sent) || 0;
+  return (
+    <div
+      className="card flex items-center justify-between"
+      style={{ padding: '0.75rem 1rem' }}
+    >
+      <span className="text-sm font-medium heading">{s.source}</span>
+      <div className="flex items-center gap-3">
+        <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>{s.cnt} items</span>
+        <span
+          className="mono text-xs font-bold"
+          style={{ color: sent > 0.05 ? 'var(--green)' : sent < -0.05 ? 'var(--red)' : 'var(--text-muted)' }}
+        >
+          {sent > 0 ? '+' : ''}{(sent * 100).toFixed(0)}%
+        </span>
+      </div>
+    </div>
+  );
+}
+
+/* ─── Stat card ─── */
+function StatCard({ label, value, color }) {
+  return (
+    <div className="card">
+      <p className="stat-label mb-2">{label}</p>
+      <p className="stat-v" style={{ color }}>{value}</p>
+    </div>
+  );
+}
+
+/* ─── Tab button ─── */
+function TabBtn({ active, onClick, children }) {
+  return (
+    <button
+      onClick={onClick}
+      className="btn btn-sm transition"
+      style={{
+        background: active ? 'var(--cyan-dim)' : 'transparent',
+        color: active ? 'var(--cyan)' : 'var(--text-muted)',
+        border: active ? '1px solid rgba(0,240,255,0.3)' : '1px solid transparent',
+        borderRadius: 'var(--radius-md)',
+      }}
+    >
+      {children}
+    </button>
+  );
+}
+
+/* ─── Main Signals page ─── */
+export default function Signals() {
+  const [data, setData] = useState(null);
+  const [dash, setDash] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [tab, setTab] = useState('signals');
+
+  useEffect(() => {
+    load();
+    const i = setInterval(load, 30000);
+    return () => clearInterval(i);
+  }, []);
+
+  async function load() {
+    try {
+      const [s, d] = await Promise.all([api('/signals'), api('/signals/dashboard')]);
+      setData(s);
+      setDash(d);
+    } catch {}
+    setLoading(false);
+  }
+
+  if (loading) {
+    return (
+      <div className="flex items-center justify-center h-64">
+        <div
+          className="animate-spin w-8 h-8 border-2 border-t-transparent rounded-full pulse-cyan"
+          style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+        />
+      </div>
+    );
+  }
+
+  const stats = dash?.stats || {};
+  const signals = data?.signals || [];
+  const shifts = data?.probabilityShifts || [];
+  const sources = dash?.sourceBreakdown || [];
+
+  return (
+    <div className="space-y-6 fade-in">
+
+      {/* ── Page Header ── */}
+      <div className="flex items-center justify-between">
+        <div>
+          <div className="flex items-center gap-3">
+            {/* Lightning bolt icon */}
+            <span className="text-2xl" style={{ color: 'var(--cyan)' }}>⚡</span>
+            <h1 className="heading text-3xl font-bold" style={{ color: 'var(--text)' }}>
+              Signals
+            </h1>
+            {/* Live dot */}
+            <span className="flex items-center gap-1.5">
+              <span
+                className="inline-block w-2 h-2 rounded-full pulse-cyan"
+                style={{ background: 'var(--cyan)' }}
+              />
+              <span className="text-xs" style={{ color: 'var(--cyan)' }}>LIVE</span>
+            </span>
+          </div>
+          <p className="section-subtitle mt-1">
+            News → Sentiment → Probability → Signals → Risk &nbsp;·&nbsp; {data?.pipeline?.cachedMarkets || 0} markets tracked
+          </p>
+        </div>
+        <button onClick={load} className="btn btn-o btn-sm">↻ Refresh</button>
+      </div>
+
+      {/* ── Stats Grid ── */}
+      <div className="grid grid-cols-6 gap-3">
+        <StatCard label="Signals 24h"    value={stats.total_signals || 0}                                              color="var(--orange)" />
+        <StatCard label="Approved"        value={stats.approved_signals || 0}                                           color="var(--green)" />
+        <StatCard label="BUY YES"         value={stats.buy_yes || 0}                                                    color="var(--green)" />
+        <StatCard label="BUY NO"          value={stats.buy_no || 0}                                                     color="var(--red)" />
+        <StatCard label="Avg Edge"        value={stats.avg_edge ? `${(stats.avg_edge * 100).toFixed(1)}%` : '—'}        color="var(--cyan)" />
+        <StatCard label="Avg Confidence"  value={stats.avg_confidence ? `${(stats.avg_confidence * 100).toFixed(0)}%` : '—'} color="var(--blue)" />
+      </div>
+
+      {/* ── Tab Bar ── */}
+      <div
+        className="flex gap-2 pb-3"
+        style={{ borderBottom: '1px solid var(--border)' }}
+      >
+        <TabBtn active={tab === 'signals'} onClick={() => setTab('signals')}>
+          Strategy Signals ({signals.length})
+        </TabBtn>
+        <TabBtn active={tab === 'shifts'} onClick={() => setTab('shifts')}>
+          Probability Shifts ({shifts.length})
+        </TabBtn>
+        <TabBtn active={tab === 'sources'} onClick={() => setTab('sources')}>
+          Source Breakdown ({sources.length})
+        </TabBtn>
+      </div>
+
+      {/* ── Strategy Signals Table ── */}
+      {tab === 'signals' && (
+        <div className="card overflow-x-auto fade-in" style={{ padding: 0 }}>
+          <table>
+            <thead>
+              <tr>
+                <th>Time</th>
+                <th>Market</th>
+                <th>Direction</th>
+                <th>Edge</th>
+                <th>Size</th>
+                <th>Confidence</th>
+                <th>Risk</th>
+                <th></th>
+              </tr>
+            </thead>
+            <tbody>
+              {signals.length === 0 ? (
+                <tr>
+                  <td colSpan={8} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
+                    <div className="flex flex-col items-center gap-2">
+                      <span className="text-2xl">⚡</span>
+                      <span className="text-sm">No signals generated yet</span>
+                    </div>
+                  </td>
+                </tr>
+              ) : (
+                signals.map(s => <SignalRow key={s.id} s={s} />)
+              )}
+            </tbody>
+          </table>
+        </div>
+      )}
+
+      {/* ── Probability Shifts Table ── */}
+      {tab === 'shifts' && (
+        <div className="card overflow-x-auto fade-in" style={{ padding: 0 }}>
+          <table>
+            <thead>
+              <tr>
+                <th>Time</th>
+                <th>Market</th>
+                <th>Prior</th>
+                <th>Impact</th>
+                <th>Adjusted</th>
+                <th>Market Price</th>
+                <th>Edge</th>
+                <th>Articles</th>
+              </tr>
+            </thead>
+            <tbody>
+              {shifts.length === 0 ? (
+                <tr>
+                  <td colSpan={8} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
+                    <div className="flex flex-col items-center gap-2">
+                      <span className="text-2xl">📊</span>
+                      <span className="text-sm">No probability shifts yet</span>
+                    </div>
+                  </td>
+                </tr>
+              ) : (
+                shifts.map((s, i) => <ShiftRow key={i} s={s} />)
+              )}
+            </tbody>
+          </table>
+        </div>
+      )}
+
+      {/* ── Source Breakdown ── */}
+      {tab === 'sources' && (
+        <div className="grid grid-cols-2 gap-4 fade-in">
+          {/* Active sources */}
+          <div>
+            <p className="section-title mb-3" style={{ fontSize: '1rem' }}>
+              Active Sources
+              <span className="badge bg-cyan ml-2">{sources.length}</span>
+            </p>
+            <div className="space-y-2">
+              {sources.length === 0 ? (
+                <p className="text-sm" style={{ color: 'var(--text-muted)' }}>No recent source data</p>
+              ) : (
+                sources.map((s, i) => <SourceCard key={i} s={s} />)
+              )}
+            </div>
+          </div>
+
+          {/* Pipeline config */}
+          <div>
+            <p className="section-title mb-3" style={{ fontSize: '1rem' }}>Pipeline Config</p>
+            <div className="card space-y-2">
+              {[
+                { label: 'Market Scan',         value: 'Every 15 min' },
+                { label: 'Signal Pipeline',     value: 'Every 5 min' },
+                { label: 'Intelligence Sweep',  value: 'Every 30 min' },
+                {
+                  label: 'Min Edge Threshold',
+                  value: `${((data?.pipeline?.riskLimits?.minEdgeThreshold || 0.05) * 100).toFixed(0)}%`,
+                  highlight: true,
+                },
+                {
+                  label: 'Min Liquidity',
+                  value: `${data?.pipeline?.riskLimits?.minLiquidity || 100} vol`,
+                  highlight: true,
+                },
+                {
+                  label: 'Cached Markets',
+                  value: data?.pipeline?.cachedMarkets || 0,
+                  highlight: true,
+                },
+              ].map(row => (
+                <div
+                  key={row.label}
+                  className="flex justify-between items-center p-2 rounded"
+                  style={{ background: 'var(--surface-el)' }}
+                >
+                  <span className="text-sm" style={{ color: 'var(--text-muted)' }}>{row.label}</span>
+                  <span
+                    className="mono text-sm font-semibold"
+                    style={{ color: row.highlight ? 'var(--cyan)' : 'var(--text)' }}
+                  >
+                    {row.value}
+                  </span>
+                </div>
+              ))}
+            </div>
+          </div>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Trading.jsx b/kalshi-dash/src/pages/Trading.jsx
new file mode 100644
index 0000000..f7e948a
--- /dev/null
+++ b/kalshi-dash/src/pages/Trading.jsx
@@ -0,0 +1,394 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+
+/* ─── Stat card ─── */
+function StatCard({ label, value, color }) {
+  return (
+    <div className="card">
+      <p className="stat-label mb-2">{label}</p>
+      <p className="stat-v" style={{ color }}>{value}</p>
+    </div>
+  );
+}
+
+/* ─── Config row ─── */
+function ConfigRow({ label, value, highlight }) {
+  return (
+    <div
+      className="flex justify-between items-center px-3 py-2 rounded"
+      style={{ background: 'var(--surface-el)' }}
+    >
+      <span className="text-sm" style={{ color: 'var(--text-muted)' }}>{label}</span>
+      <span
+        className="mono text-sm font-semibold"
+        style={{ color: highlight || 'var(--text)' }}
+      >
+        {value}
+      </span>
+    </div>
+  );
+}
+
+/* ─── Env toggle button ─── */
+function EnvBtn({ label, active, color, onClick, disabled }) {
+  return (
+    <button
+      onClick={onClick}
+      disabled={disabled}
+      className="flex-1 py-2 rounded-lg text-sm font-bold transition"
+      style={{
+        background: active ? `${color}22` : 'var(--surface-el)',
+        color: active ? color : 'var(--text-muted)',
+        border: `1px solid ${active ? color + '55' : 'var(--border)'}`,
+      }}
+    >
+      {label}
+    </button>
+  );
+}
+
+/* ─── Main Trading page ─── */
+export default function Trading() {
+  const [mode, setMode] = useState(null);
+  const [history, setHistory] = useState(null);
+  const [system, setSystem] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [actionLoading, setActionLoading] = useState('');
+
+  useEffect(() => {
+    load();
+    const i = setInterval(load, 15000);
+    return () => clearInterval(i);
+  }, []);
+
+  async function load() {
+    try {
+      const [m, h, s] = await Promise.all([
+        post('/trading/mode', { action: 'get' }),
+        api('/trading/history'),
+        api('/system'),
+      ]);
+      setMode(m);
+      setHistory(h);
+      setSystem(s);
+    } catch {}
+    setLoading(false);
+  }
+
+  async function doAction(action, body = {}) {
+    setActionLoading(action);
+    try {
+      await post('/trading/mode', { action, ...body });
+      await load();
+    } catch {}
+    setActionLoading('');
+  }
+
+  if (loading) {
+    return (
+      <div className="flex items-center justify-center h-64">
+        <div
+          className="animate-spin w-8 h-8 border-2 border-t-transparent rounded-full pulse-cyan"
+          style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+        />
+      </div>
+    );
+  }
+
+  const tc = mode?.trade_config || {};
+  const ts = mode?.trader_state || {};
+  const summary = history?.summary || {};
+  const trades = history?.trades || [];
+  const mem = system?.memory || {};
+
+  const totalPnl = parseInt(summary.total_pnl || 0);
+  const pnlColor = totalPnl > 0 ? 'var(--green)' : totalPnl < 0 ? 'var(--red)' : 'var(--text-muted)';
+
+  return (
+    <div className="space-y-6 fade-in">
+
+      {/* ── Page Header ── */}
+      <div className="flex items-center justify-between">
+        <div>
+          <div className="flex items-center gap-3">
+            {/* Dollar icon */}
+            <span className="text-2xl" style={{ color: 'var(--green)' }}>$</span>
+            <h1 className="heading text-3xl font-bold" style={{ color: 'var(--text)' }}>
+              Trading
+            </h1>
+            {/* Environment badge */}
+            <span className={`badge ${mode?.env === 'prod' ? 'bg-green' : 'bg-orange'}`}>
+              {(mode?.env || 'demo').toUpperCase()}
+            </span>
+            {mode?.safe_mode && (
+              <span className="badge bg-orange">SAFE MODE</span>
+            )}
+            {mode?.auto_trader_enabled && (
+              <span className="badge bg-green">AUTO ON</span>
+            )}
+          </div>
+          <p className="section-subtitle mt-1">Mode control, execution, and P&amp;L</p>
+        </div>
+        <button onClick={load} className="btn btn-o btn-sm">↻ Refresh</button>
+      </div>
+
+      {/* ── Trading Controls ── */}
+      <div className="grid grid-cols-3 gap-4">
+
+        {/* Environment */}
+        <div className="card-glow">
+          <p className="stat-label mb-3">Environment</p>
+          <div className="flex gap-2 mb-3">
+            <EnvBtn
+              label="DEMO"
+              active={mode?.env === 'demo'}
+              color="var(--orange)"
+              onClick={() => doAction('switch_env', { env: 'demo' })}
+              disabled={actionLoading === 'switch_env'}
+            />
+            <EnvBtn
+              label="PROD"
+              active={mode?.env === 'prod'}
+              color="var(--green)"
+              onClick={() => doAction('switch_env', { env: 'prod' })}
+              disabled={actionLoading === 'switch_env'}
+            />
+          </div>
+          <p className="text-xs" style={{ color: 'var(--text-muted)' }}>
+            API keys: {mode?.kalshi_connected ? (
+              <span style={{ color: 'var(--green)' }}>Configured</span>
+            ) : (
+              <span style={{ color: 'var(--red)' }}>Not set</span>
+            )}
+          </p>
+        </div>
+
+        {/* Safe mode */}
+        <div className="card-glow">
+          <p className="stat-label mb-3">Safe Mode</p>
+          <button
+            onClick={() => doAction('toggle_safe_mode')}
+            disabled={actionLoading === 'toggle_safe_mode'}
+            className="btn w-full btn-lg font-bold transition"
+            style={{
+              background: mode?.safe_mode ? 'rgba(247,147,26,0.15)' : 'var(--red-dim)',
+              color: mode?.safe_mode ? 'var(--orange)' : 'var(--red)',
+              border: `1px solid ${mode?.safe_mode ? 'rgba(247,147,26,0.4)' : 'rgba(255,92,92,0.4)'}`,
+            }}
+          >
+            {actionLoading === 'toggle_safe_mode' ? '...' : (
+              mode?.safe_mode ? 'SAFE MODE — ON' : 'SAFE MODE — OFF'
+            )}
+          </button>
+          <p className="text-xs mt-2" style={{ color: 'var(--text-muted)' }}>
+            {mode?.safe_mode ? 'No real orders will be placed' : (
+              <span style={{ color: 'var(--red)' }}>Real orders will be executed!</span>
+            )}
+          </p>
+        </div>
+
+        {/* Auto trader */}
+        <div className="card-glow">
+          <p className="stat-label mb-3">Auto Trader</p>
+          <button
+            onClick={() => doAction('toggle_auto_trader')}
+            disabled={actionLoading === 'toggle_auto_trader'}
+            className="btn w-full btn-lg font-bold transition"
+            style={{
+              background: mode?.auto_trader_enabled ? 'var(--green-dim)' : 'var(--surface-el)',
+              color: mode?.auto_trader_enabled ? 'var(--green)' : 'var(--text-muted)',
+              border: `1px solid ${mode?.auto_trader_enabled ? 'rgba(0,195,137,0.4)' : 'var(--border)'}`,
+            }}
+          >
+            {actionLoading === 'toggle_auto_trader' ? '...' : (
+              mode?.auto_trader_enabled ? 'AUTO TRADER: ON' : 'AUTO TRADER: OFF'
+            )}
+          </button>
+          <p className="text-xs mt-2" style={{ color: 'var(--text-muted)' }}>
+            Min confidence: <span className="mono">{tc.min_confidence}%</span>
+            &nbsp;·&nbsp;
+            Budget: <span className="mono">${(tc.budget_cents / 100).toFixed(0)}</span>
+          </p>
+        </div>
+      </div>
+
+      {/* ── Portfolio Stats ── */}
+      <div className="grid grid-cols-6 gap-3">
+        <StatCard label="Total Trades" value={summary.total_trades || 0}                                                                         color="var(--orange)" />
+        <StatCard label="Open"         value={summary.open_trades || 0}                                                                          color="var(--blue)" />
+        <StatCard label="Wins"         value={summary.winning_trades || 0}                                                                       color="var(--green)" />
+        <StatCard label="Losses"       value={summary.losing_trades || 0}                                                                        color="var(--red)" />
+        <StatCard label="Total P&L"    value={summary.total_pnl ? `$${(totalPnl / 100).toFixed(2)}` : '$0.00'}                                  color={pnlColor} />
+        <StatCard label="Exposure"     value={`$${(parseInt(summary.open_exposure || 0) / 100).toFixed(2)}`}                                     color="var(--purple)" />
+      </div>
+
+      {/* ── Config + System ── */}
+      <div className="grid grid-cols-2 gap-4">
+
+        {/* Trade Config */}
+        <div className="card">
+          <p className="section-title mb-3" style={{ fontSize: '1rem' }}>
+            <span style={{ color: 'var(--cyan)' }}>⚙</span> Trade Config
+          </p>
+          <div className="space-y-2">
+            <ConfigRow label="Budget"          value={`$${(tc.budget_cents / 100).toFixed(0)}`}            highlight="var(--cyan)" />
+            <ConfigRow label="Max per trade"   value={`$${(tc.max_position_cents / 100).toFixed(0)}`} />
+            <ConfigRow label="Max positions"   value={tc.max_open_positions} />
+            <ConfigRow label="Daily loss limit" value={`$${(tc.daily_loss_limit_cents / 100).toFixed(0)}`} highlight="var(--red)" />
+            <ConfigRow label="Min confidence"  value={`${tc.min_confidence}%`}                             highlight="var(--orange)" />
+            <ConfigRow label="Take profit"     value={`${tc.take_profit_pct}%`}                            highlight="var(--green)" />
+            <ConfigRow label="Stop loss"       value={`${tc.stop_loss_pct}%`}                              highlight="var(--red)" />
+          </div>
+        </div>
+
+        {/* System Health */}
+        <div className="card">
+          <p className="section-title mb-3" style={{ fontSize: '1rem' }}>
+            <span style={{ color: 'var(--cyan)' }}>♥</span> System Health
+          </p>
+          <div className="space-y-2">
+            <ConfigRow label="Memory (RSS)"     value={`${mem.rss_mb || '?'} MB`} />
+            <ConfigRow label="Heap Used"        value={`${mem.heapUsed_mb || '?'} MB`} />
+            <ConfigRow label="Heap Total"       value={`${mem.heapTotal_mb || '?'} MB`} />
+            <ConfigRow label="Uptime"           value={`${system?.uptime_hours || '?'}h`}                   highlight="var(--blue)" />
+            <ConfigRow label="Cached Markets"   value={system?.cachedMarkets || 0}                          highlight="var(--cyan)" />
+            <ConfigRow label="PID"              value={system?.pid || '?'} />
+            <ConfigRow
+              label="Available Budget"
+              value={`$${((ts.available || 0) / 100).toFixed(2)}`}
+              highlight="var(--green)"
+            />
+          </div>
+        </div>
+      </div>
+
+      {/* ── Trade History ── */}
+      <div className="card overflow-x-auto" style={{ padding: 0 }}>
+        {/* Table header */}
+        <div
+          className="flex items-center justify-between px-5 py-4"
+          style={{ borderBottom: '1px solid var(--border)' }}
+        >
+          <p className="section-title" style={{ fontSize: '1rem' }}>
+            <span style={{ color: 'var(--orange)' }}>◈</span> Trade History
+          </p>
+          {trades.length > 0 && (
+            <span className="badge bg-orange">{trades.length} trades</span>
+          )}
+        </div>
+
+        <table>
+          <thead>
+            <tr>
+              <th>Time</th>
+              <th>Market</th>
+              <th>Side</th>
+              <th>Qty</th>
+              <th>Entry</th>
+              <th>Exit</th>
+              <th>P&amp;L</th>
+              <th>Type</th>
+              <th>Status</th>
+            </tr>
+          </thead>
+          <tbody>
+            {trades.length === 0 ? (
+              <tr>
+                <td colSpan={9} className="text-center py-12" style={{ color: 'var(--text-muted)' }}>
+                  <div className="flex flex-col items-center gap-2">
+                    <span className="text-2xl" style={{ color: 'var(--text-muted)' }}>$</span>
+                    <span className="text-sm">No trades yet — signals are generating, waiting for execution</span>
+                  </div>
+                </td>
+              </tr>
+            ) : (
+              trades.map((t, i) => {
+                const pnl = parseInt(t.pnl_cents) || 0;
+                const pnlColor = pnl > 0 ? 'var(--green)' : pnl < 0 ? 'var(--red)' : 'var(--text-muted)';
+                const statusClass =
+                  t.status === 'open'    ? 'bg-blue'   :
+                  t.status === 'profit'  ? 'bg-green'  :
+                  t.status === 'stopped' ? 'bg-red'    : 'bg-orange';
+
+                return (
+                  <tr key={i}>
+                    {/* Time */}
+                    <td className="whitespace-nowrap">
+                      <span className="mono text-xs" style={{ color: 'var(--text-muted)' }}>
+                        {new Date(t.created_at).toLocaleDateString()}{' '}
+                        {new Date(t.created_at).toLocaleTimeString()}
+                      </span>
+                    </td>
+
+                    {/* Market */}
+                    <td className="max-w-[200px]">
+                      <a
+                        href={`https://kalshi.com/search?query=${encodeURIComponent((t.ticker || '').split('-')[0])}`}
+                        target="_blank"
+                        rel="noopener noreferrer"
+                        className="text-sm truncate block hover:underline transition heading"
+                        style={{ color: 'var(--cyan)' }}
+                      >
+                        {t.title || t.ticker}
+                      </a>
+                    </td>
+
+                    {/* Side */}
+                    <td>
+                      <span className={`badge ${t.side === 'yes' ? 'bg-green' : 'bg-red'}`}>
+                        {(t.side || '').toUpperCase()}
+                      </span>
+                    </td>
+
+                    {/* Qty */}
+                    <td>
+                      <span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
+                        {t.count}
+                      </span>
+                    </td>
+
+                    {/* Entry price */}
+                    <td>
+                      <span className="mono text-sm" style={{ color: 'var(--text-dim)' }}>
+                        {t.price_cents}c
+                      </span>
+                    </td>
+
+                    {/* Exit price */}
+                    <td>
+                      <span className="mono text-sm" style={{ color: t.exit_price ? 'var(--text-dim)' : 'var(--text-muted)' }}>
+                        {t.exit_price ? `${t.exit_price}c` : '—'}
+                      </span>
+                    </td>
+
+                    {/* P&L */}
+                    <td>
+                      <span className="mono text-sm font-bold" style={{ color: pnlColor }}>
+                        {pnl !== 0
+                          ? `${pnl > 0 ? '+' : ''}$${(pnl / 100).toFixed(2)}`
+                          : '—'
+                        }
+                      </span>
+                    </td>
+
+                    {/* Signal type */}
+                    <td>
+                      <span className="text-xs" style={{ color: 'var(--text-muted)' }}>
+                        {t.signal_type}
+                      </span>
+                    </td>
+
+                    {/* Status */}
+                    <td>
+                      <span className={`badge ${statusClass}`}>{t.status}</span>
+                    </td>
+                  </tr>
+                );
+              })
+            )}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/pages/Weather.jsx b/kalshi-dash/src/pages/Weather.jsx
new file mode 100644
index 0000000..22cd156
--- /dev/null
+++ b/kalshi-dash/src/pages/Weather.jsx
@@ -0,0 +1,171 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../api';
+
+export default function Weather() {
+  const [data, setData] = useState(null);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => { load(); }, []);
+
+  async function load() {
+    setLoading(true);
+    try {
+      const r = await post('/weather', { action: 'get_data' });
+      setData(r);
+    } catch {}
+    setLoading(false);
+  }
+
+  async function fetchAll() {
+    setLoading(true);
+    try {
+      const r = await post('/weather', { action: 'fetch_all' });
+      setData(r);
+    } catch {}
+    setLoading(false);
+  }
+
+  if (loading) return (
+    <div className="flex items-center justify-center h-64">
+      <div
+        className="w-8 h-8 rounded-full border-2 border-t-transparent animate-spin"
+        style={{ borderColor: 'var(--cyan)', borderTopColor: 'transparent' }}
+      />
+    </div>
+  );
+
+  const metricCards = [
+    { label: 'Stations',     key: 'stations',     icon: '\u{1F4CD}', color: 'var(--cyan)' },
+    { label: 'Forecasts',    key: 'forecasts',    icon: '\u{1F52E}', color: 'var(--blue)' },
+    { label: 'Observations', key: 'observations', icon: '\u{1F4CA}', color: 'var(--green)' },
+    { label: 'Metrics',      key: 'metrics',      icon: '\u{1F4CF}', color: 'var(--purple)' },
+  ];
+
+  return (
+    <div className="space-y-6">
+      {/* Page Header */}
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="heading text-2xl font-bold flex items-center gap-2">
+            <span style={{ color: 'var(--cyan)' }}>{'\u2601'}</span>
+            <span className="gradient-cyan">Weather Markets</span>
+          </h1>
+          <p className="section-subtitle mt-1">NWS settlement data for Ken weather contracts</p>
+        </div>
+        <button onClick={fetchAll} className="btn btn-p">
+          {'\u21BB'} Fetch Latest
+        </button>
+      </div>
+
+      {/* Metric Summary Cards */}
+      <div className="grid grid-cols-4 gap-4">
+        {metricCards.map(({ label, key, icon, color }) => (
+          <div key={label} className="card">
+            <div className="flex items-center gap-2 mb-2">
+              <span className="text-lg">{icon}</span>
+              <p className="stat-label">{label}</p>
+            </div>
+            <p className="stat-v" style={{ color }}>{data?.[key] ?? '—'}</p>
+          </div>
+        ))}
+      </div>
+
+      {/* Settlement Rule Reference */}
+      <div className="card">
+        <h2 className="section-title mb-4">Settlement Rule Reference</h2>
+        <div className="grid grid-cols-2 gap-6">
+          <div
+            className="rounded-lg p-4"
+            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+          >
+            <p className="stat-label mb-3">Temperature Contracts</p>
+            <ul className="space-y-2">
+              {[
+                'Source: NWS Daily Climate Report (final)',
+                'Station: Market-specified (e.g. KORD, KLGA)',
+                'Values: Tmax/Tmin in degrees F',
+                'Missing data: treated as 0',
+              ].map((item, i) => (
+                <li key={i} className="flex items-start gap-2 text-xs" style={{ color: 'var(--text-dim)' }}>
+                  <span style={{ color: 'var(--cyan)', flexShrink: 0 }}>&#8227;</span>
+                  {item}
+                </li>
+              ))}
+            </ul>
+          </div>
+          <div
+            className="rounded-lg p-4"
+            style={{ background: 'var(--surface-el)', border: '1px solid var(--border)' }}
+          >
+            <p className="stat-label mb-3">Precipitation Contracts</p>
+            <ul className="space-y-2">
+              {[
+                'Source: NWS Daily Climate Report (first complete)',
+                'Trace: counted as 0.00 inches',
+                'Missing: counted as 0.00 inches',
+                'Revisions: do NOT change settlement',
+              ].map((item, i) => (
+                <li key={i} className="flex items-start gap-2 text-xs" style={{ color: 'var(--text-dim)' }}>
+                  <span style={{ color: 'var(--blue)', flexShrink: 0 }}>&#8227;</span>
+                  {item}
+                </li>
+              ))}
+            </ul>
+          </div>
+        </div>
+      </div>
+
+      {/* Recent Observations Table */}
+      {(data?.outcomes || []).length > 0 && (
+        <div className="card overflow-x-auto">
+          <h2 className="section-title mb-4">Recent Observations</h2>
+          <table>
+            <thead>
+              <tr>
+                <th>Station</th>
+                <th>Metric</th>
+                <th>Value</th>
+                <th>Source</th>
+                <th>Date</th>
+              </tr>
+            </thead>
+            <tbody>
+              {data.outcomes.map((o, i) => (
+                <tr key={i} className="fade-in">
+                  <td className="mono text-sm" style={{ color: 'var(--cyan)' }}>{o.station_id}</td>
+                  <td>
+                    <span className="badge bg-blue">{o.metric}</span>
+                  </td>
+                  <td className="mono font-bold" style={{ color: 'var(--text)' }}>
+                    {o.value}{o.units}
+                  </td>
+                  <td className="text-xs" style={{ color: 'var(--text-muted)' }}>{o.source}</td>
+                  <td className="text-xs mono" style={{ color: 'var(--text-muted)' }}>{o.period_end}</td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      )}
+
+      {/* Empty state */}
+      {(data?.outcomes || []).length === 0 && (
+        <div
+          className="card text-center py-12"
+          style={{ border: '1px dashed var(--border)' }}
+        >
+          <p className="text-4xl mb-3">{'\u26C5'}</p>
+          <p className="heading text-sm font-semibold mb-1" style={{ color: 'var(--text-dim)' }}>
+            No observations loaded
+          </p>
+          <p className="text-xs mb-4" style={{ color: 'var(--text-muted)' }}>
+            Click "Fetch Latest" to pull current NWS settlement data
+          </p>
+          <button onClick={fetchAll} className="btn btn-p btn-sm">
+            Fetch Latest
+          </button>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/SetupWizard.jsx b/kalshi-dash/src/setup/SetupWizard.jsx
new file mode 100644
index 0000000..2df4fda
--- /dev/null
+++ b/kalshi-dash/src/setup/SetupWizard.jsx
@@ -0,0 +1,119 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { checkSession, post, api } from '../api';
+import Welcome from './steps/Welcome';
+import ConnectKalshi from './steps/ConnectKalshi';
+import TestConnection from './steps/TestConnection';
+import BrowseMarkets from './steps/BrowseMarkets';
+import FetchWeather from './steps/FetchWeather';
+import RunPredictions from './steps/RunPredictions';
+import RiskProfile from './steps/RiskProfile';
+import GoLive from './steps/GoLive';
+
+const STEPS = [
+  { label: 'Shalom', icon: '1' },
+  { label: 'Keys', icon: '2' },
+  { label: 'Test It', icon: '3' },
+  { label: 'Snoop', icon: '4' },
+  { label: 'Weather', icon: '5' },
+  { label: 'Predict', icon: '6' },
+  { label: 'Limits', icon: '7' },
+  { label: 'Mazel Tov', icon: '8' },
+];
+
+const COMPS = [Welcome, ConnectKalshi, TestConnection, BrowseMarkets, FetchWeather, RunPredictions, RiskProfile, GoLive];
+
+export default function SetupWizard() {
+  const nav = useNavigate();
+  const [authed, setAuthed] = useState(false);
+  const [step, setStep] = useState(0);
+  const [data, setData] = useState({
+    keys: null,
+    connectionOk: false,
+    marketsLoaded: 0,
+    weatherFetched: false,
+    predictionsRun: false,
+    riskProfile: null,
+  });
+
+  useEffect(() => {
+    checkSession().then(ok => {
+      if (!ok) { nav('/login'); return; }
+      setAuthed(true);
+      api('/setup').then(r => {
+        if (r.setup_complete) { nav('/'); return; }
+        if (r.setup_progress) {
+          setStep(r.setup_progress.step || 0);
+          setData(d => ({ ...d, ...r.setup_progress.data }));
+        }
+      }).catch(() => {});
+    });
+  }, []);
+
+  useEffect(() => {
+    if (authed && step > 0) {
+      post('/setup', { action: 'save_progress', progress: { step, data } }).catch(() => {});
+    }
+  }, [step]);
+
+  function next() { if (step < STEPS.length - 1) setStep(s => s + 1); }
+  function back() { if (step > 0) setStep(s => s - 1); }
+  function goTo(i) { if (i <= step) setStep(i); }
+  function update(partial) { setData(d => ({ ...d, ...partial })); }
+  async function complete() {
+    await post('/setup', { action: 'complete', progress: { step: STEPS.length - 1, data } });
+    nav('/');
+  }
+
+  if (!authed) return <div className="flex items-center justify-center min-h-screen"><div className="animate-spin w-8 h-8 border-2 border-amber-500 border-t-transparent rounded-full" /></div>;
+
+  const StepComponent = COMPS[step];
+
+  return (
+    <div className="min-h-screen flex flex-col" style={{ background: 'var(--bg)' }}>
+      {/* Header */}
+      <div className="flex items-center gap-3 px-6 py-4 border-b border-gray-800">
+        <div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0" style={{ background: 'linear-gradient(135deg,#f59e0b,#f97316)' }}>
+          <span className="text-black font-bold text-sm">K</span>
+        </div>
+        <span className="font-bold text-sm kalshi-gradient">KEN SETUP — SIT, WE'LL TALK</span>
+      </div>
+
+      {/* Stepper */}
+      <div className="px-6 py-5">
+        <div className="max-w-3xl mx-auto flex items-center">
+          {STEPS.map((s, i) => (
+            <React.Fragment key={i}>
+              <button onClick={() => goTo(i)} disabled={i > step} className="flex flex-col items-center gap-1.5 group" style={{ minWidth: 56 }}>
+                <div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all ${
+                  i < step ? 'bg-green-500 text-white' :
+                  i === step ? 'bg-amber-500 text-black' :
+                  'bg-gray-800 text-gray-500'
+                } ${i <= step ? 'cursor-pointer' : 'cursor-default'}`}
+                  style={i === step ? { animation: 'pg 2s infinite' } : {}}
+                >
+                  {i < step ? (
+                    <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
+                  ) : s.icon}
+                </div>
+                <span className={`text-[10px] font-medium ${
+                  i < step ? 'text-green-400' : i === step ? 'text-amber-400' : 'text-gray-600'
+                }`}>{s.label}</span>
+              </button>
+              {i < STEPS.length - 1 && (
+                <div className={`h-0.5 flex-1 transition-colors ${i < step ? 'bg-green-500' : 'bg-gray-800'}`} />
+              )}
+            </React.Fragment>
+          ))}
+        </div>
+      </div>
+
+      {/* Content */}
+      <div className="flex-1 flex items-start justify-center px-6 pb-10">
+        <div className="w-full max-w-2xl" key={step}>
+          <StepComponent data={data} update={update} next={next} back={back} complete={complete} step={step} />
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/steps/BrowseMarkets.jsx b/kalshi-dash/src/setup/steps/BrowseMarkets.jsx
new file mode 100644
index 0000000..a2352be
--- /dev/null
+++ b/kalshi-dash/src/setup/steps/BrowseMarkets.jsx
@@ -0,0 +1,94 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../../api';
+
+export default function BrowseMarkets({ data, update, next, back }) {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  useEffect(() => { loadMarkets(); }, []);
+
+  async function loadMarkets() {
+    setLoading(true); setError(null);
+    try {
+      const r = await post('/kalshi', { action: 'get_events', params: { status: 'open', limit: 50 } });
+      setResult(r);
+      const count = r.events?.length || 0;
+      update({ marketsLoaded: count });
+    } catch { setError('Failed to load markets'); }
+    setLoading(false);
+  }
+
+  const weatherEvents = (result?.events || []).filter(e =>
+    (e.category || '').toLowerCase().includes('climate') ||
+    (e.category || '').toLowerCase().includes('weather') ||
+    (e.title || '').toLowerCase().includes('temperature') ||
+    (e.title || '').toLowerCase().includes('rain') ||
+    (e.title || '').toLowerCase().includes('snow') ||
+    (e.title || '').toLowerCase().includes('precipitation')
+  );
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Let's See What They're Selling</h2>
+        <p className="text-gray-400 text-sm">I'm snooping through the markets. You're welcome.</p>
+      </div>
+
+      <div className="card">
+        {loading ? (
+          <div className="flex flex-col items-center justify-center py-12 gap-4">
+            <div className="animate-spin w-10 h-10 border-3 border-amber-500 border-t-transparent rounded-full" />
+            <p className="text-sm text-gray-400">Schlepping markets from Ken... patience is a virtue</p>
+          </div>
+        ) : error ? (
+          <div className="text-center py-8">
+            <p className="text-red-400 text-sm mb-3">{error}</p>
+            <button onClick={loadMarkets} className="btn btn-s text-sm">Retry</button>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            <div className="grid grid-cols-3 gap-4">
+              <div className="text-center">
+                <p className="stat-v text-amber-400">{result?.events?.length || 0}</p>
+                <p className="text-xs text-gray-500 mt-1">Things to Bet On</p>
+              </div>
+              <div className="text-center">
+                <p className="stat-v text-blue-400">{weatherEvents.length}</p>
+                <p className="text-xs text-gray-500 mt-1">Weather Bets (Our Thing)</p>
+              </div>
+              <div className="text-center">
+                <p className="stat-v text-green-400">{result?.markets?.length || '—'}</p>
+                <p className="text-xs text-gray-500 mt-1">Individual Contracts</p>
+              </div>
+            </div>
+
+            {weatherEvents.length > 0 && (
+              <div className="border-t border-gray-800 pt-3">
+                <p className="text-xs text-gray-500 mb-2 uppercase font-medium">Weather Bets I Found For You</p>
+                <div className="space-y-1.5 max-h-48 overflow-y-auto">
+                  {weatherEvents.slice(0, 12).map((e, i) => (
+                    <div key={i} className="flex items-center gap-2 text-xs">
+                      <span className="w-1.5 h-1.5 rounded-full bg-amber-500 flex-shrink-0" />
+                      <span className="text-gray-300 truncate">{e.title}</span>
+                      <span className="text-gray-600 ml-auto font-mono text-[10px]">{e.event_ticker}</span>
+                    </div>
+                  ))}
+                </div>
+              </div>
+            )}
+
+            {weatherEvents.length === 0 && (
+              <p className="text-center text-gray-500 text-sm py-4">No weather bets right now. What, the weather took a day off? Other markets are there at least.</p>
+            )}
+          </div>
+        )}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={next} disabled={loading} className="btn btn-p">{loading ? 'Loading...' : 'Continue'}</button>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/steps/ConnectKalshi.jsx b/kalshi-dash/src/setup/steps/ConnectKalshi.jsx
new file mode 100644
index 0000000..02bd052
--- /dev/null
+++ b/kalshi-dash/src/setup/steps/ConnectKalshi.jsx
@@ -0,0 +1,95 @@
+import React, { useState } from 'react';
+import { post } from '../../api';
+
+export default function ConnectKalshi({ data, update, next, back }) {
+  const [apiKey, setApiKey] = useState(data.keys?.api_key || '');
+  const [privateKey, setPrivateKey] = useState(data.keys?.private_key || '');
+  const [env, setEnv] = useState(data.keys?.env || 'demo');
+  const [saving, setSaving] = useState(false);
+  const [error, setError] = useState(null);
+
+  async function save() {
+    setSaving(true); setError(null);
+    try {
+      const r = await post('/kalshi', {
+        action: 'save_keys',
+        api_key: apiKey,
+        private_key: privateKey,
+        env,
+      });
+      if (r.success) {
+        update({ keys: { api_key: apiKey, private_key: '***', env } });
+        next();
+      } else setError(r.error || 'Failed to save');
+    } catch { setError('Connection error'); }
+    setSaving(false);
+  }
+
+  function skip() {
+    update({ keys: null, skippedConnect: true });
+    next();
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Give Me the Keys</h2>
+        <p className="text-gray-400 text-sm">Your API credentials, darling. I don't have all day.</p>
+      </div>
+
+      <div className="card space-y-4">
+        {/* Environment Toggle */}
+        <div>
+          <label className="block text-xs text-gray-400 mb-1.5 font-medium">Which playground are we using?</label>
+          <div className="flex gap-2">
+            <button onClick={() => setEnv('demo')} className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${env === 'demo' ? 'bg-yellow-500/15 text-yellow-400 border border-yellow-500/30' : 'bg-gray-800/50 text-gray-500 border border-gray-700'}`}>
+              Demo (Smart)
+            </button>
+            <button onClick={() => setEnv('prod')} className={`flex-1 py-2 rounded-lg text-sm font-medium transition ${env === 'prod' ? 'bg-green-500/15 text-green-400 border border-green-500/30' : 'bg-gray-800/50 text-gray-500 border border-gray-700'}`}>
+              Production (Brave)
+            </button>
+          </div>
+        </div>
+
+        <div>
+          <label className="block text-xs text-gray-400 mb-1.5 font-medium">API Key ID (the one they gave you)</label>
+          <input type="text" value={apiKey} onChange={e => setApiKey(e.target.value)}
+            placeholder="Paste it here, bubeleh"
+            className="w-full px-3 py-2 bg-gray-800/50 border border-gray-700 rounded-lg text-sm focus:border-amber-500 focus:outline-none font-mono" />
+        </div>
+
+        <div>
+          <label className="block text-xs text-gray-400 mb-1.5 font-medium">Private Key (the whole megillah, PEM format)</label>
+          <textarea value={privateKey} onChange={e => setPrivateKey(e.target.value)}
+            placeholder="-----BEGIN RSA PRIVATE KEY-----&#10;...&#10;-----END RSA PRIVATE KEY-----"
+            rows={6}
+            className="w-full px-3 py-2 bg-gray-800/50 border border-gray-700 rounded-lg text-sm focus:border-amber-500 focus:outline-none font-mono text-xs leading-relaxed" />
+          <p className="text-[10px] text-gray-600 mt-1">Don't worry, I keep it locked up tighter than your grandmother's jewelry box</p>
+        </div>
+
+        {error && <p className="text-red-400 text-xs">{error}</p>}
+      </div>
+
+      <div className="card bg-gray-800/30">
+        <h3 className="text-sm font-medium mb-2">What, you don't have keys yet? Oy vey.</h3>
+        <ol className="text-xs text-gray-400 space-y-1.5 list-decimal list-inside">
+          <li>Go log into <span className="text-amber-400 font-mono">ken.com</span> like a mensch</li>
+          <li>Find Settings → API Keys (it's not that hard, look around)</li>
+          <li>Click "Create API Key" and <strong>download that private key file immediately</strong></li>
+          <li>Copy the Key ID and paste the PEM contents up there. Simple.</li>
+        </ol>
+        <p className="text-[10px] text-red-400 mt-2">Listen to me — that private key file? You can only download it ONCE. Lose it and you start over. Don't say I didn't warn you.</p>
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <div className="flex items-center gap-3">
+          <button onClick={skip} className="text-sm text-gray-500 hover:text-gray-300">Skip (play pretend)</button>
+          <button onClick={save} disabled={saving || !apiKey || !privateKey} className="btn btn-p">
+            {saving ? 'Saving, patience...' : 'Save & Continue'}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/steps/FetchWeather.jsx b/kalshi-dash/src/setup/steps/FetchWeather.jsx
new file mode 100644
index 0000000..112cbd6
--- /dev/null
+++ b/kalshi-dash/src/setup/steps/FetchWeather.jsx
@@ -0,0 +1,79 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../../api';
+
+export default function FetchWeather({ data, update, next, back }) {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  useEffect(() => { runFetch(); }, []);
+
+  async function runFetch() {
+    setLoading(true); setError(null);
+    try {
+      const r = await post('/weather', { action: 'fetch_all' });
+      setResult(r);
+      update({ weatherFetched: true });
+    } catch { setError('Failed to fetch weather data'); }
+    setLoading(false);
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Checking the Weather, Darling</h2>
+        <p className="text-gray-400 text-sm">I'm schlepping in the government weather reports. In my day we just looked outside.</p>
+      </div>
+
+      <div className="card">
+        {loading ? (
+          <div className="flex flex-col items-center justify-center py-12 gap-4">
+            <div className="animate-spin w-10 h-10 border-3 border-blue-500 border-t-transparent rounded-full" />
+            <p className="text-sm text-gray-400">Fetching from the National Weather Service... they take their sweet time</p>
+          </div>
+        ) : error ? (
+          <div className="text-center py-8">
+            <p className="text-red-400 text-sm mb-3">{error}</p>
+            <button onClick={runFetch} className="btn btn-s text-sm">Retry</button>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            <div className="flex items-center justify-center gap-3 py-4">
+              <div className="w-12 h-12 rounded-full bg-green-500/10 flex items-center justify-center">
+                <svg className="w-6 h-6 text-green-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
+              </div>
+              <div>
+                <p className="font-semibold text-green-400">Got the Weather, Finally</p>
+                <p className="text-xs text-gray-400">All the fancy government data, right here in your tuchas seat</p>
+              </div>
+            </div>
+            <div className="grid grid-cols-3 gap-4">
+              <div className="text-center p-3 bg-gray-800/30 rounded-lg">
+                <p className="stat-v text-blue-400">{result?.stations || '—'}</p>
+                <p className="text-xs text-gray-500 mt-1">Stations</p>
+              </div>
+              <div className="text-center p-3 bg-gray-800/30 rounded-lg">
+                <p className="stat-v text-amber-400">{result?.data_points || '—'}</p>
+                <p className="text-xs text-gray-500 mt-1">Data Points</p>
+              </div>
+              <div className="text-center p-3 bg-gray-800/30 rounded-lg">
+                <p className="stat-v text-green-400">{result?.metrics?.length || '—'}</p>
+                <p className="text-xs text-gray-500 mt-1">Metrics</p>
+              </div>
+            </div>
+          </div>
+        )}
+      </div>
+
+      <div className="card bg-gray-800/30">
+        <p className="text-xs text-gray-500 mb-1 font-medium">How They Settle (Pay Attention)</p>
+        <p className="text-xs text-gray-400">Ken settles on the <span className="text-amber-400">final NWS Daily Climate Report</span>. A trace of rain? That's zero. Missing data? Also zero. And listen — the first report is the last word. They don't go back and change it, even if the weatherman was wrong. Such is life.</p>
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={next} disabled={loading} className="btn btn-p">{loading ? 'Fetching...' : 'Continue'}</button>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/steps/GoLive.jsx b/kalshi-dash/src/setup/steps/GoLive.jsx
new file mode 100644
index 0000000..58efea4
--- /dev/null
+++ b/kalshi-dash/src/setup/steps/GoLive.jsx
@@ -0,0 +1,106 @@
+import React, { useState } from 'react';
+
+const CHECKS = [
+  { key: 'keys', label: 'Gave me the API keys (good)', check: d => !!d.keys },
+  { key: 'conn', label: 'Connection actually works (miracle)', check: d => !!d.connectionOk },
+  { key: 'markets', label: 'Found some markets to bet on', check: d => d.marketsLoaded > 0 },
+  { key: 'weather', label: 'Got the weather data (finally)', check: d => !!d.weatherFetched },
+  { key: 'preds', label: 'The machine made its predictions', check: d => !!d.predictionsRun },
+  { key: 'risk', label: 'Set some limits (smart)', check: d => !!d.riskProfile },
+];
+
+export default function GoLive({ data, complete, back }) {
+  const [safeMode, setSafeMode] = useState(true);
+  const [launching, setLaunching] = useState(false);
+
+  const done = CHECKS.filter(c => c.check(data)).length;
+  const skipped = data.skippedConnect;
+
+  async function launch() {
+    setLaunching(true);
+    await complete();
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Alright, Mazel Tov</h2>
+        <p className="text-gray-400 text-sm">Let's see if you actually did everything right. I have my doubts.</p>
+      </div>
+
+      <div className="card">
+        <h3 className="font-semibold text-sm mb-3">Your Report Card</h3>
+        <div className="space-y-2">
+          {CHECKS.map(c => {
+            const ok = c.check(data);
+            const skip = c.key === 'keys' && skipped;
+            return (
+              <div key={c.key} className="flex items-center gap-3">
+                {ok ? (
+                  <div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0">
+                    <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
+                  </div>
+                ) : skip ? (
+                  <div className="w-5 h-5 rounded-full bg-yellow-500 flex items-center justify-center flex-shrink-0"><span className="text-white text-[10px] font-bold">—</span></div>
+                ) : (
+                  <div className="w-5 h-5 rounded-full bg-gray-700 flex-shrink-0" />
+                )}
+                <span className={`text-sm ${ok ? 'text-gray-200' : skip ? 'text-yellow-400' : 'text-gray-500'}`}>
+                  {c.label}{skip ? ' (skipped)' : ''}
+                </span>
+              </div>
+            );
+          })}
+        </div>
+        <p className="text-xs text-gray-500 mt-3 pt-3 border-t border-gray-800">{done}/{CHECKS.length} steps completed</p>
+      </div>
+
+      <div className="grid grid-cols-3 gap-4">
+        <div className="card text-center">
+          <p className="stat-v text-amber-400">{data.marketsLoaded || 0}</p>
+          <p className="text-xs text-gray-500 mt-1">Markets</p>
+        </div>
+        <div className="card text-center">
+          <p className="stat-v text-blue-400">{data.riskProfile || '—'}</p>
+          <p className="text-xs text-gray-500 mt-1">Risk Profile</p>
+        </div>
+        <div className="card text-center">
+          <p className="stat-v text-green-400">{data.keys ? (data.keys.env || 'demo').toUpperCase() : 'PAPER'}</p>
+          <p className="text-xs text-gray-500 mt-1">Mode</p>
+        </div>
+      </div>
+
+      {/* Safe Mode Toggle */}
+      <div className="card">
+        <div className="flex items-center justify-between">
+          <div>
+            <h3 className="font-semibold text-sm">Safe Mode (I Recommend It)</h3>
+            <p className="text-xs text-gray-500 mt-0.5">
+              {safeMode ? 'Paper trading — playing pretend, like a sensible person' : 'Live orders! With real money! Oy gevalt.'}
+            </p>
+          </div>
+          <div className="flex items-center gap-3">
+            <span className={`text-xs font-medium ${safeMode ? 'text-yellow-400' : 'text-gray-500'}`}>Safe</span>
+            <button onClick={() => setSafeMode(!safeMode)} disabled={!data.keys}
+              className={`w-11 h-6 rounded-full transition-colors relative ${safeMode ? 'bg-yellow-500/30' : 'bg-green-500/30'} ${!data.keys ? 'opacity-50 cursor-not-allowed' : ''}`}>
+              <div className={`w-4 h-4 rounded-full absolute top-1 transition-all ${safeMode ? 'left-1 bg-yellow-400' : 'left-6 bg-green-400'}`} />
+            </button>
+            <span className={`text-xs font-medium ${!safeMode ? 'text-green-400' : 'text-gray-500'}`}>Live</span>
+          </div>
+        </div>
+        {!safeMode && (
+          <div className="mt-3 p-2 bg-red-500/10 border border-red-500/20 rounded-lg">
+            <p className="text-xs text-red-400">Live mode means real money, bubeleh. Real. Money. Don't say I didn't warn you. Make sure you've got funds and your limits are set, or so help me.</p>
+          </div>
+        )}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={launch} disabled={launching} className="btn btn-p text-lg px-8 py-3 rounded-xl font-bold">
+          {launching ? 'Starting up, hold your horses...' : 'Go Make Some Gelt Already'}
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/steps/RiskProfile.jsx b/kalshi-dash/src/setup/steps/RiskProfile.jsx
new file mode 100644
index 0000000..a3d23a4
--- /dev/null
+++ b/kalshi-dash/src/setup/steps/RiskProfile.jsx
@@ -0,0 +1,68 @@
+import React, { useState } from 'react';
+import { post } from '../../api';
+
+const PROFILES = [
+  { name: 'Conservative', maxContracts: 5, maxDollars: 25, dailyLoss: 10, color: 'blue',
+    desc: 'Like your bubbe taught you — small bets, tight grip on the purse strings.' },
+  { name: 'Moderate', maxContracts: 20, maxDollars: 100, dailyLoss: 50, color: 'amber',
+    desc: 'Not too much, not too little. Like Goldilocks, but with money.', recommended: true },
+  { name: 'Aggressive', maxContracts: 50, maxDollars: 500, dailyLoss: 200, color: 'red',
+    desc: 'What are you, a big shot? Don\'t come crying to me when it goes sideways.' },
+];
+
+export default function RiskProfile({ data, update, next, back }) {
+  const [selected, setSelected] = useState(data.riskProfile || 'Moderate');
+  const [saving, setSaving] = useState(false);
+
+  async function save() {
+    setSaving(true);
+    const p = PROFILES.find(x => x.name === selected);
+    try {
+      await post('/risk', {
+        action: 'update_config',
+        max_contracts_per_market: p.maxContracts,
+        max_dollars_at_risk: p.maxDollars,
+        daily_loss_cap: p.dailyLoss,
+        risk_profile: p.name,
+      });
+      update({ riskProfile: selected });
+      next();
+    } catch {}
+    setSaving(false);
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">How Much Are You Willing to Lose?</h2>
+        <p className="text-gray-400 text-sm">Pick one. And no, "all of it" is not an option.</p>
+      </div>
+
+      <div className="grid grid-cols-3 gap-4">
+        {PROFILES.map(p => {
+          const colors = { blue: { ring: '#3b82f6', text: 'text-blue-400' }, amber: { ring: '#f59e0b', text: 'text-amber-400' }, red: { ring: '#ef4444', text: 'text-red-400' } };
+          const c = colors[p.color];
+          return (
+            <button key={p.name} onClick={() => setSelected(p.name)}
+              className="card text-left transition-all relative"
+              style={selected === p.name ? { boxShadow: `0 0 0 2px ${c.ring}` } : {}}>
+              {p.recommended && <span className="absolute -top-2 right-3 text-[9px] font-bold uppercase px-2 py-0.5 rounded-full bg-amber-500 text-black">Recommended</span>}
+              <h3 className={`font-bold text-lg mb-1 ${c.text}`}>{p.name}</h3>
+              <p className="text-xs text-gray-500 mb-3">{p.desc}</p>
+              <div className="space-y-1.5 text-xs">
+                <div className="flex justify-between"><span className="text-gray-500">Max Contracts</span><span className="font-mono text-gray-300">{p.maxContracts}</span></div>
+                <div className="flex justify-between"><span className="text-gray-500">Max $ at Risk</span><span className="font-mono text-gray-300">${p.maxDollars}</span></div>
+                <div className="flex justify-between"><span className="text-gray-500">Daily Loss Cap</span><span className="font-mono text-gray-300">${p.dailyLoss}</span></div>
+              </div>
+            </button>
+          );
+        })}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={save} disabled={saving} className="btn btn-p">{saving ? 'Saving...' : 'Save & Continue'}</button>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/steps/RunPredictions.jsx b/kalshi-dash/src/setup/steps/RunPredictions.jsx
new file mode 100644
index 0000000..2c5edbd
--- /dev/null
+++ b/kalshi-dash/src/setup/steps/RunPredictions.jsx
@@ -0,0 +1,87 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../../api';
+
+export default function RunPredictions({ data, update, next, back }) {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  useEffect(() => { run(); }, []);
+
+  async function run() {
+    setLoading(true); setError(null);
+    try {
+      const r = await post('/kalshi', { action: 'run_predictions' });
+      setResult(r);
+      update({ predictionsRun: true });
+    } catch { setError('Failed to generate predictions'); }
+    setLoading(false);
+  }
+
+  const predictions = result?.predictions || [];
+  const buySignals = predictions.filter(p => (p.edge_bps || 0) > 0);
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">The Machine Is Thinking</h2>
+        <p className="text-gray-400 text-sm">Running the numbers so you don't have to use that head of yours</p>
+      </div>
+
+      <div className="card">
+        {loading ? (
+          <div className="flex flex-col items-center justify-center py-12 gap-4">
+            <div className="animate-spin w-10 h-10 border-3 border-green-500 border-t-transparent rounded-full" />
+            <p className="text-sm text-gray-400">Crunching numbers... in my day we did this with an abacus</p>
+          </div>
+        ) : error ? (
+          <div className="text-center py-8">
+            <p className="text-red-400 text-sm mb-3">{error}</p>
+            <button onClick={run} className="btn btn-s text-sm">Retry</button>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            <div className="grid grid-cols-3 gap-4">
+              <div className="text-center">
+                <p className="stat-v text-amber-400">{predictions.length}</p>
+                <p className="text-xs text-gray-500 mt-1">Opinions</p>
+              </div>
+              <div className="text-center">
+                <p className="stat-v text-green-400">{buySignals.length}</p>
+                <p className="text-xs text-gray-500 mt-1">Worth a Look</p>
+              </div>
+              <div className="text-center">
+                <p className="stat-v text-blue-400">{result?.avg_edge_bps || 0}bps</p>
+                <p className="text-xs text-gray-500 mt-1">Your Edge (God Willing)</p>
+              </div>
+            </div>
+
+            {predictions.length > 0 && (
+              <div className="border-t border-gray-800 pt-3">
+                <table>
+                  <thead><tr><th>Contract</th><th>Side</th><th>Price</th><th>Edge</th><th>Signal</th></tr></thead>
+                  <tbody>
+                    {predictions.slice(0, 6).map((p, i) => (
+                      <tr key={i}>
+                        <td className="max-w-[180px]"><span className="truncate block text-xs">{p.ticker || p.market}</span></td>
+                        <td><span className={`badge ${p.side === 'yes' ? 'bg-green' : 'bg-red'}`}>{(p.side || 'yes').toUpperCase()}</span></td>
+                        <td className="font-mono text-sm">{p.price_cents || '—'}c</td>
+                        <td className={`font-mono text-sm ${(p.edge_bps || 0) > 0 ? 'text-green-400' : 'text-gray-500'}`}>{p.edge_bps || 0}bps</td>
+                        <td><span className={`badge ${(p.edge_bps || 0) > 0 ? 'bg-green' : 'bg-yellow'}`}>{(p.edge_bps || 0) > 0 ? 'BUY' : 'HOLD'}</span></td>
+                      </tr>
+                    ))}
+                  </tbody>
+                </table>
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={next} disabled={loading} className="btn btn-p">{loading ? 'Processing...' : 'Continue'}</button>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/steps/TestConnection.jsx b/kalshi-dash/src/setup/steps/TestConnection.jsx
new file mode 100644
index 0000000..66faf36
--- /dev/null
+++ b/kalshi-dash/src/setup/steps/TestConnection.jsx
@@ -0,0 +1,91 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../../api';
+
+export default function TestConnection({ data, update, next, back }) {
+  const [exchangeTest, setExchangeTest] = useState(null);
+  const [authTest, setAuthTest] = useState(null);
+  const [exLoading, setExLoading] = useState(true);
+  const [authLoading, setAuthLoading] = useState(false);
+
+  useEffect(() => { runExchangeTest(); }, []);
+
+  async function runExchangeTest() {
+    setExLoading(true);
+    try {
+      const r = await post('/kalshi', { action: 'test_connection' });
+      setExchangeTest(r);
+      if (r.success && data.keys && !data.skippedConnect) runAuthTest();
+    } catch { setExchangeTest({ error: true }); }
+    setExLoading(false);
+  }
+
+  async function runAuthTest() {
+    setAuthLoading(true);
+    try {
+      const r = await post('/kalshi', { action: 'get_balance' });
+      setAuthTest(r);
+      if (r.success) update({ connectionOk: true });
+    } catch { setAuthTest({ error: true }); }
+    setAuthLoading(false);
+  }
+
+  const skipped = data.skippedConnect;
+
+  const TestCard = ({ title, loading, result, skippedMsg }) => (
+    <div className="card">
+      <div className="flex items-center gap-2 mb-3">
+        {skippedMsg ? (
+          <div className="w-5 h-5 rounded-full bg-yellow-500 flex items-center justify-center"><span className="text-white text-xs font-bold">—</span></div>
+        ) : loading ? (
+          <div className="animate-spin w-5 h-5 border-2 border-amber-500 border-t-transparent rounded-full" />
+        ) : result?.success ? (
+          <div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center">
+            <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
+          </div>
+        ) : result?.error ? (
+          <div className="w-5 h-5 rounded-full bg-red-500 flex items-center justify-center">
+            <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
+          </div>
+        ) : <div className="w-5 h-5 rounded-full bg-gray-600" />}
+        <h3 className="font-semibold text-sm">{title}</h3>
+      </div>
+      {skippedMsg ? (
+        <p className="text-xs text-yellow-400">{skippedMsg}</p>
+      ) : result?.success ? (
+        <div className="space-y-1 text-xs text-gray-400">
+          {result.exchange_status && <p>Exchange: <span className="text-green-400 font-mono">{result.exchange_status}</span></p>}
+          {result.balance !== undefined && <p>Balance: <span className="text-green-400 font-mono">${(result.balance / 100).toFixed(2)}</span></p>}
+          {result.env && <p>Env: <span className="text-amber-400 font-mono">{result.env.toUpperCase()}</span></p>}
+        </div>
+      ) : result?.error ? (
+        <p className="text-xs text-red-400">{typeof result.error === 'string' ? result.error : 'Connection failed'}</p>
+      ) : null}
+    </div>
+  );
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Let's See If This Thing Works</h2>
+        <p className="text-gray-400 text-sm">I'm checking your wiring. Hold your horses.</p>
+      </div>
+
+      <div className="grid grid-cols-2 gap-4">
+        <TestCard title="Is Ken Even Open?" loading={exLoading} result={exchangeTest} />
+        <TestCard title="Do They Know Who You Are?" loading={authLoading} result={authTest}
+          skippedMsg={skipped ? 'You skipped this — playing pretend, remember?' : (!data.keys ? 'No keys, no entry. What did you expect?' : null)} />
+      </div>
+
+      {!exLoading && !authLoading && (
+        <div className="flex justify-center">
+          <button onClick={runExchangeTest} className="text-xs text-gray-500 hover:text-gray-300">Try again, maybe this time it'll listen</button>
+        </div>
+      )}
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={next} className="btn btn-p">Continue</button>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/setup/steps/Welcome.jsx b/kalshi-dash/src/setup/steps/Welcome.jsx
new file mode 100644
index 0000000..9146190
--- /dev/null
+++ b/kalshi-dash/src/setup/steps/Welcome.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+
+const FEATURES = [
+  { title: 'Event Contracts', desc: 'Betting on weather like a person who never learned to just look outside. CFTC-regulated, at least.', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
+  { title: 'Weather Intelligence', desc: 'We schlepp forecast data from the government so you don\'t have to read the almanac yourself.', icon: 'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z' },
+  { title: 'Automated Execution', desc: 'The machine places your bets so your hands stay clean. Safe mode included, because I worry.', icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z' },
+];
+
+const PIPELINE = [
+  'Hook up to Ken (demo first, like a sensible person)',
+  'Snoop around the weather contracts',
+  'Schlep in the NWS settlement data',
+  'Let the computer do the math you were too lazy to learn',
+  'Set some limits so you don\'t lose your shirt',
+  'Place the bets (on paper first, God willing)',
+];
+
+export default function Welcome({ next }) {
+  return (
+    <div className="space-y-8">
+      <div className="text-center">
+        <h1 className="text-4xl font-bold kalshi-gradient mb-3">Oy, Look Who Finally Showed Up</h1>
+        <p className="text-gray-400 text-lg">So you want to bet on the weather? Your mother would be so proud.</p>
+      </div>
+
+      <div className="grid grid-cols-3 gap-4">
+        {FEATURES.map(f => (
+          <div key={f.title} className="card text-center">
+            <div className="w-10 h-10 rounded-lg bg-amber-500/10 flex items-center justify-center mx-auto mb-3">
+              <svg className="w-5 h-5 text-amber-400" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" d={f.icon} />
+              </svg>
+            </div>
+            <h3 className="font-semibold text-sm mb-1">{f.title}</h3>
+            <p className="text-xs text-gray-500 leading-relaxed">{f.desc}</p>
+          </div>
+        ))}
+      </div>
+
+      <div className="card">
+        <h3 className="font-semibold text-sm mb-3">Listen, this is how it works</h3>
+        <div className="grid grid-cols-2 gap-2">
+          {PIPELINE.map((p, i) => (
+            <div key={i} className="flex items-center gap-3 py-1.5">
+              <span className="w-6 h-6 rounded-full bg-amber-500/15 text-amber-400 flex items-center justify-center text-xs font-bold flex-shrink-0">{i + 1}</span>
+              <span className="text-sm text-gray-300">{p}</span>
+            </div>
+          ))}
+        </div>
+      </div>
+
+      <div className="card bg-green-500/5 border-green-500/20">
+        <div className="flex items-start gap-3">
+          <svg className="w-5 h-5 text-green-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
+          </svg>
+          <div>
+            <p className="text-sm font-medium text-green-400">At least it's legal, thank God</p>
+            <p className="text-xs text-gray-400 mt-1">Ken is a real CFTC-regulated exchange, not some offshore mishegoss. You'll need an account with API access. Go make one already, I'll wait.</p>
+          </div>
+        </div>
+      </div>
+
+      <div className="flex justify-center">
+        <button onClick={next} className="btn btn-p px-8 py-3 text-base font-bold rounded-xl">Nu, Let's Go Already</button>
+      </div>
+    </div>
+  );
+}
diff --git a/kalshi-dash/src/utils/chartTheme.js b/kalshi-dash/src/utils/chartTheme.js
new file mode 100644
index 0000000..f5d0653
--- /dev/null
+++ b/kalshi-dash/src/utils/chartTheme.js
@@ -0,0 +1,76 @@
+/* ─── Shared Recharts Theme Config ─── */
+
+export const CHART_COLORS = {
+  cyan:   '#00F0FF',
+  orange: '#F7931A',
+  green:  '#00C389',
+  red:    '#FF5C5C',
+  blue:   '#3B82F6',
+  purple: '#A855F7',
+  muted:  '#6B7280',
+  dim:    '#A8A8A8',
+  border: '#2A2A35',
+  surface: '#141419',
+  surfaceEl: '#1C1C24',
+  text:   '#E0E0E0',
+};
+
+/* Ordered palette for cycling through categories / series */
+export const COLOR_CYCLE = [
+  '#00F0FF', // cyan
+  '#F7931A', // orange
+  '#00C389', // green
+  '#3B82F6', // blue
+  '#A855F7', // purple
+  '#FF5C5C', // red
+];
+
+/* Standard tooltip style for all charts */
+export const TOOLTIP_STYLE = {
+  contentStyle: {
+    background: '#1C1C24',
+    border: '1px solid #2A2A35',
+    borderRadius: 8,
+    color: '#E0E0E0',
+    fontFamily: "'Inter', sans-serif",
+    fontSize: 12,
+  },
+  itemStyle: {
+    color: '#E0E0E0',
+    fontFamily: "'JetBrains Mono', monospace",
+  },
+  labelStyle: {
+    color: '#6B7280',
+    fontFamily: "'Inter', sans-serif",
+    fontWeight: 600,
+    marginBottom: 4,
+  },
+};
+
+/* Standard axis tick style */
+export const AXIS_TICK = {
+  fill: '#6B7280',
+  fontSize: 11,
+  fontFamily: "'Inter', sans-serif",
+};
+
+/* Standard grid style */
+export const GRID_STYLE = {
+  stroke: '#2A2A35',
+  strokeOpacity: 0.6,
+  strokeDasharray: '3 3',
+};
+
+/* Format cents to dollar string */
+export function fmtDollars(cents) {
+  const v = cents / 100;
+  if (Math.abs(v) >= 1000) return `$${(v / 1000).toFixed(1)}k`;
+  return `$${v.toFixed(2)}`;
+}
+
+/* Format a date string "2025-01-15" -> "Jan 15" */
+export function fmtDateShort(dateStr) {
+  if (!dateStr) return '';
+  const d = new Date(dateStr + 'T00:00:00');
+  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
+}
diff --git a/kalshi-dash/tailwind.config.js b/kalshi-dash/tailwind.config.js
new file mode 100644
index 0000000..bac94e1
--- /dev/null
+++ b/kalshi-dash/tailwind.config.js
@@ -0,0 +1,5 @@
+export default {
+  content: ['./index.html', './src/**/*.{js,jsx}'],
+  theme: { extend: {} },
+  plugins: [],
+};
diff --git a/kalshi-dash/vite.config.js b/kalshi-dash/vite.config.js
new file mode 100644
index 0000000..48c4635
--- /dev/null
+++ b/kalshi-dash/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+  plugins: [react()],
+  server: { proxy: { '/api': 'http://localhost:7810' } },
+});
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..3ebb967
--- /dev/null
+++ b/next.config.js
@@ -0,0 +1,6 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  reactStrictMode: false,
+  eslint: { ignoreDuringBuilds: true },
+};
+module.exports = nextConfig;
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..ba4d2df
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2237 @@
+{
+  "name": "bertha-weather-betting",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "bertha-weather-betting",
+      "version": "1.0.0",
+      "dependencies": {
+        "cookie": "^1.0.0",
+        "jsonwebtoken": "^9.0.2",
+        "next": "^14.2.0",
+        "node-cron": "^3.0.3",
+        "pg": "^8.13.0",
+        "react": "^18.3.0",
+        "react-dom": "^18.3.0",
+        "recharts": "^2.15.0"
+      },
+      "devDependencies": {
+        "autoprefixer": "^10.4.20",
+        "postcss": "^8.4.49",
+        "tailwindcss": "^3.4.17"
+      }
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+      "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
+      "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "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": "14.2.35",
+      "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
+      "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
+      "license": "MIT"
+    },
+    "node_modules/@next/swc-darwin-arm64": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
+      "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-darwin-x64": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
+      "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-gnu": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
+      "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-arm64-musl": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
+      "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-gnu": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
+      "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-linux-x64-musl": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
+      "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-arm64-msvc": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
+      "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-ia32-msvc": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
+      "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
+      "cpu": [
+        "ia32"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@next/swc-win32-x64-msvc": {
+      "version": "14.2.33",
+      "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
+      "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
+      "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/@swc/counter": {
+      "version": "0.1.3",
+      "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
+      "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
+      "license": "Apache-2.0"
+    },
+    "node_modules/@swc/helpers": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz",
+      "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@swc/counter": "^0.1.3",
+        "tslib": "^2.4.0"
+      }
+    },
+    "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-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-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-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-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-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-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/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.4.24",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
+      "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
+      "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.1",
+        "caniuse-lite": "^1.0.30001766",
+        "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.9.19",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
+      "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.js"
+      }
+    },
+    "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.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+      "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+      "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.9.0",
+        "caniuse-lite": "^1.0.30001759",
+        "electron-to-chromium": "^1.5.263",
+        "node-releases": "^2.0.27",
+        "update-browserslist-db": "^1.2.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
+    "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.30001769",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz",
+      "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==",
+      "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/client-only": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
+      "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
+      "license": "MIT"
+    },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/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/cookie": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+      "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/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==",
+      "license": "MIT"
+    },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+      "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+      "license": "ISC",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "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-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-format": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+      "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+      "license": "ISC",
+      "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-path": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+      "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+      "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+      "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+      "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+      "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+      "license": "ISC",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "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/decimal.js-light": {
+      "version": "2.5.1",
+      "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+      "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+      "license": "MIT"
+    },
+    "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/dom-helpers": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+      "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.286",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
+      "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "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/eventemitter3": {
+      "version": "4.0.7",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+      "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+      "license": "MIT"
+    },
+    "node_modules/fast-equals": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
+      "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "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/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/graceful-fs": {
+      "version": "4.2.11",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+      "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+      "license": "ISC"
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+      "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "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/js-tokens": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+      "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+      "license": "MIT"
+    },
+    "node_modules/jsonwebtoken": {
+      "version": "9.0.3",
+      "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
+      "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
+      "license": "MIT",
+      "dependencies": {
+        "jws": "^4.0.1",
+        "lodash.includes": "^4.3.0",
+        "lodash.isboolean": "^3.0.3",
+        "lodash.isinteger": "^4.0.4",
+        "lodash.isnumber": "^3.0.3",
+        "lodash.isplainobject": "^4.0.6",
+        "lodash.isstring": "^4.0.1",
+        "lodash.once": "^4.0.0",
+        "ms": "^2.1.1",
+        "semver": "^7.5.4"
+      },
+      "engines": {
+        "node": ">=12",
+        "npm": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "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/lodash": {
+      "version": "4.17.23",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
+      "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.includes": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+      "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isboolean": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+      "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isinteger": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+      "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isnumber": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+      "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isplainobject": {
+      "version": "4.0.6",
+      "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+      "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.isstring": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+      "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.once": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+      "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+      "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "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/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "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": "14.2.35",
+      "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
+      "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
+      "license": "MIT",
+      "dependencies": {
+        "@next/env": "14.2.35",
+        "@swc/helpers": "0.5.5",
+        "busboy": "1.6.0",
+        "caniuse-lite": "^1.0.30001579",
+        "graceful-fs": "^4.2.11",
+        "postcss": "8.4.31",
+        "styled-jsx": "5.1.1"
+      },
+      "bin": {
+        "next": "dist/bin/next"
+      },
+      "engines": {
+        "node": ">=18.17.0"
+      },
+      "optionalDependencies": {
+        "@next/swc-darwin-arm64": "14.2.33",
+        "@next/swc-darwin-x64": "14.2.33",
+        "@next/swc-linux-arm64-gnu": "14.2.33",
+        "@next/swc-linux-arm64-musl": "14.2.33",
+        "@next/swc-linux-x64-gnu": "14.2.33",
+        "@next/swc-linux-x64-musl": "14.2.33",
+        "@next/swc-win32-arm64-msvc": "14.2.33",
+        "@next/swc-win32-ia32-msvc": "14.2.33",
+        "@next/swc-win32-x64-msvc": "14.2.33"
+      },
+      "peerDependencies": {
+        "@opentelemetry/api": "^1.1.0",
+        "@playwright/test": "^1.41.2",
+        "react": "^18.2.0",
+        "react-dom": "^18.2.0",
+        "sass": "^1.3.0"
+      },
+      "peerDependenciesMeta": {
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@playwright/test": {
+          "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-cron": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
+      "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
+      "license": "ISC",
+      "dependencies": {
+        "uuid": "8.3.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.27",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+      "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+      "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==",
+      "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.18.0",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
+      "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.11.0",
+        "pg-pool": "^3.11.0",
+        "pg-protocol": "^1.11.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.11.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz",
+      "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==",
+      "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.11.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz",
+      "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz",
+      "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==",
+      "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.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "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.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "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/prop-types": {
+      "version": "15.8.1",
+      "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+      "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
+    "node_modules/prop-types/node_modules/react-is": {
+      "version": "16.13.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+      "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+      "license": "MIT"
+    },
+    "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": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+      "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+      "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.2"
+      },
+      "peerDependencies": {
+        "react": "^18.3.1"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "18.3.1",
+      "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
+      "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+      "license": "MIT"
+    },
+    "node_modules/react-smooth": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
+      "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-equals": "^5.0.1",
+        "prop-types": "^15.8.1",
+        "react-transition-group": "^4.4.5"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+      "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
+    "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/recharts": {
+      "version": "2.15.4",
+      "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz",
+      "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==",
+      "license": "MIT",
+      "dependencies": {
+        "clsx": "^2.0.0",
+        "eventemitter3": "^4.0.1",
+        "lodash": "^4.17.21",
+        "react-is": "^18.3.1",
+        "react-smooth": "^4.0.4",
+        "recharts-scale": "^0.4.4",
+        "tiny-invariant": "^1.3.1",
+        "victory-vendor": "^36.6.8"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/recharts-scale": {
+      "version": "0.4.5",
+      "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz",
+      "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==",
+      "license": "MIT",
+      "dependencies": {
+        "decimal.js-light": "^2.4.1"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.11",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+      "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "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/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "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/scheduler": {
+      "version": "0.23.2",
+      "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+      "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "node_modules/semver": {
+      "version": "7.7.4",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+      "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/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/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/styled-jsx": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
+      "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==",
+      "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"
+      },
+      "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/tiny-invariant": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+      "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+      "license": "MIT"
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/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.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "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/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/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/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/victory-vendor": {
+      "version": "36.9.2",
+      "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+      "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
+      "license": "MIT AND ISC",
+      "dependencies": {
+        "@types/d3-array": "^3.0.3",
+        "@types/d3-ease": "^3.0.0",
+        "@types/d3-interpolate": "^3.0.1",
+        "@types/d3-scale": "^4.0.2",
+        "@types/d3-shape": "^3.1.0",
+        "@types/d3-time": "^3.0.0",
+        "@types/d3-timer": "^3.0.0",
+        "d3-array": "^3.1.6",
+        "d3-ease": "^3.0.1",
+        "d3-interpolate": "^3.0.1",
+        "d3-scale": "^4.0.2",
+        "d3-shape": "^3.1.0",
+        "d3-time": "^3.0.0",
+        "d3-timer": "^3.0.1"
+      }
+    },
+    "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"
+      }
+    }
+  }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..a59bcc7
--- /dev/null
+++ b/package.json
@@ -0,0 +1,26 @@
+{
+  "name": "bertha-weather-betting",
+  "version": "1.0.0",
+  "description": "Weather forecast micro-betting system for Polymarket",
+  "private": true,
+  "scripts": {
+    "dev": "next dev -p 7800",
+    "build": "next build",
+    "start": "next start -p 7800"
+  },
+  "dependencies": {
+    "next": "^14.2.0",
+    "react": "^18.3.0",
+    "react-dom": "^18.3.0",
+    "pg": "^8.13.0",
+    "jsonwebtoken": "^9.0.2",
+    "cookie": "^1.0.0",
+    "recharts": "^2.15.0",
+    "node-cron": "^3.0.3"
+  },
+  "devDependencies": {
+    "autoprefixer": "^10.4.20",
+    "postcss": "^8.4.49",
+    "tailwindcss": "^3.4.17"
+  }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..12a703d
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {},
+  },
+};
diff --git a/react-dash/index.html b/react-dash/index.html
new file mode 100644
index 0000000..31bc407
--- /dev/null
+++ b/react-dash/index.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Poly - Weather Micro-Betting Dashboard</title>
+  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
+</head>
+<body>
+  <div id="root"></div>
+  <script type="module" src="/src/main.jsx"></script>
+</body>
+</html>
diff --git a/react-dash/package-lock.json b/react-dash/package-lock.json
new file mode 100644
index 0000000..3d4809f
--- /dev/null
+++ b/react-dash/package-lock.json
@@ -0,0 +1,5100 @@
+{
+  "name": "bertha-react-dash",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "bertha-react-dash",
+      "version": "1.0.0",
+      "dependencies": {
+        "@polymarket/clob-client": "^5.2.1",
+        "ethers": "^5.8.0",
+        "google-auth-library": "^10.5.0",
+        "pg": "^8.18.0",
+        "react": "^18.3.0",
+        "react-dom": "^18.3.0",
+        "react-router-dom": "^6.28.0",
+        "recharts": "^2.15.0"
+      },
+      "devDependencies": {
+        "@vitejs/plugin-react": "^4.3.0",
+        "autoprefixer": "^10.4.20",
+        "postcss": "^8.4.49",
+        "tailwindcss": "^3.4.17",
+        "vite": "^5.4.0"
+      }
+    },
+    "node_modules/@adraffy/ens-normalize": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz",
+      "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==",
+      "license": "MIT"
+    },
+    "node_modules/@alloc/quick-lru": {
+      "version": "5.2.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@babel/code-frame": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+      "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/compat-data": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+      "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/core": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+      "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-compilation-targets": "^7.28.6",
+        "@babel/helper-module-transforms": "^7.28.6",
+        "@babel/helpers": "^7.28.6",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/traverse": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/remapping": "^2.3.5",
+        "convert-source-map": "^2.0.0",
+        "debug": "^4.1.0",
+        "gensync": "^1.0.0-beta.2",
+        "json5": "^2.2.3",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/babel"
+      }
+    },
+    "node_modules/@babel/core/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/generator": {
+      "version": "7.29.1",
+      "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+      "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.29.0",
+        "@babel/types": "^7.29.0",
+        "@jridgewell/gen-mapping": "^0.3.12",
+        "@jridgewell/trace-mapping": "^0.3.28",
+        "jsesc": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+      "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/compat-data": "^7.28.6",
+        "@babel/helper-validator-option": "^7.27.1",
+        "browserslist": "^4.24.0",
+        "lru-cache": "^5.1.1",
+        "semver": "^6.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "node_modules/@babel/helper-globals": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+      "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-imports": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+      "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/traverse": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-module-transforms": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+      "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-module-imports": "^7.28.6",
+        "@babel/helper-validator-identifier": "^7.28.5",
+        "@babel/traverse": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0"
+      }
+    },
+    "node_modules/@babel/helper-plugin-utils": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+      "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+      "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.28.5",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+      "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-option": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+      "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helpers": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+      "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/parser": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+      "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.29.0"
+      },
+      "bin": {
+        "parser": "bin/babel-parser.js"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-self": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+      "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/plugin-transform-react-jsx-source": {
+      "version": "7.27.1",
+      "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+      "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-plugin-utils": "^7.27.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      },
+      "peerDependencies": {
+        "@babel/core": "^7.0.0-0"
+      }
+    },
+    "node_modules/@babel/runtime": {
+      "version": "7.28.6",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/template": {
+      "version": "7.28.6",
+      "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+      "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.28.6",
+        "@babel/parser": "^7.28.6",
+        "@babel/types": "^7.28.6"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/traverse": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+      "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.29.0",
+        "@babel/generator": "^7.29.0",
+        "@babel/helper-globals": "^7.28.0",
+        "@babel/parser": "^7.29.0",
+        "@babel/template": "^7.28.6",
+        "@babel/types": "^7.29.0",
+        "debug": "^4.3.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/types": {
+      "version": "7.29.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+      "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.27.1",
+        "@babel/helper-validator-identifier": "^7.28.5"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+      "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ethersproject/abi": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.8.0.tgz",
+      "integrity": "sha512-b9YS/43ObplgyV6SlyQsG53/vkSal0MNA1fskSC4mbnCMi8R+NkcH8K9FPYNESf6jUefBUniE4SOKms0E/KK1Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/abstract-provider": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz",
+      "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/networks": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/web": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/abstract-signer": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz",
+      "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/address": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz",
+      "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/base64": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz",
+      "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/basex": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz",
+      "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/bignumber": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz",
+      "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "bn.js": "^5.2.1"
+      }
+    },
+    "node_modules/@ethersproject/bytes": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz",
+      "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/constants": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz",
+      "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/contracts": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.8.0.tgz",
+      "integrity": "sha512-0eFjGz9GtuAi6MZwhb4uvUM216F38xiuR0yYCjKJpNfSEy4HUM8hvqqBj9Jmm0IUz8l0xKEhWwLIhPgxNY0yvQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abi": "^5.8.0",
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/hash": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz",
+      "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/hdnode": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz",
+      "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/basex": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/pbkdf2": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/wordlists": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/json-wallets": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz",
+      "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hdnode": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/pbkdf2": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "aes-js": "3.0.0",
+        "scrypt-js": "3.0.1"
+      }
+    },
+    "node_modules/@ethersproject/keccak256": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz",
+      "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "js-sha3": "0.8.0"
+      }
+    },
+    "node_modules/@ethersproject/logger": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz",
+      "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/@ethersproject/networks": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz",
+      "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/pbkdf2": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz",
+      "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/properties": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz",
+      "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/providers": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz",
+      "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/basex": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/networks": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/web": "^5.8.0",
+        "bech32": "1.1.4",
+        "ws": "8.18.0"
+      }
+    },
+    "node_modules/@ethersproject/random": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz",
+      "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/rlp": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz",
+      "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/sha2": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz",
+      "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "hash.js": "1.1.7"
+      }
+    },
+    "node_modules/@ethersproject/signing-key": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz",
+      "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "bn.js": "^5.2.1",
+        "elliptic": "6.6.1",
+        "hash.js": "1.1.7"
+      }
+    },
+    "node_modules/@ethersproject/solidity": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/solidity/-/solidity-5.8.0.tgz",
+      "integrity": "sha512-4CxFeCgmIWamOHwYN9d+QWGxye9qQLilpgTU0XhYs1OahkclF+ewO+3V1U0mvpiuQxm5EHHmv8f7ClVII8EHsA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/sha2": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/strings": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz",
+      "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/transactions": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz",
+      "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/rlp": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/units": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/units/-/units-5.8.0.tgz",
+      "integrity": "sha512-lxq0CAnc5kMGIiWW4Mr041VT8IhNM+Pn5T3haO74XZWFulk7wH1Gv64HqE96hT4a7iiNMdOCFEBgaxWuk8ETKQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/constants": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/wallet": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz",
+      "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abstract-provider": "^5.8.0",
+        "@ethersproject/abstract-signer": "^5.8.0",
+        "@ethersproject/address": "^5.8.0",
+        "@ethersproject/bignumber": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/hdnode": "^5.8.0",
+        "@ethersproject/json-wallets": "^5.8.0",
+        "@ethersproject/keccak256": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/random": "^5.8.0",
+        "@ethersproject/signing-key": "^5.8.0",
+        "@ethersproject/transactions": "^5.8.0",
+        "@ethersproject/wordlists": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/web": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz",
+      "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/base64": "^5.8.0",
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@ethersproject/wordlists": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz",
+      "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/bytes": "^5.8.0",
+        "@ethersproject/hash": "^5.8.0",
+        "@ethersproject/logger": "^5.8.0",
+        "@ethersproject/properties": "^5.8.0",
+        "@ethersproject/strings": "^5.8.0"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@jridgewell/gen-mapping": {
+      "version": "0.3.13",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.0",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/remapping": {
+      "version": "2.3.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+      "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/gen-mapping": "^0.3.5",
+        "@jridgewell/trace-mapping": "^0.3.24"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.31",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.1.0",
+        "@jridgewell/sourcemap-codec": "^1.4.14"
+      }
+    },
+    "node_modules/@noble/ciphers": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
+      "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/curves": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
+      "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "1.8.0"
+      },
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/hashes": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+      "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "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",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/@polymarket/builder-signing-sdk": {
+      "version": "0.0.8",
+      "resolved": "https://registry.npmjs.org/@polymarket/builder-signing-sdk/-/builder-signing-sdk-0.0.8.tgz",
+      "integrity": "sha512-rZLCFxEdYahl5FiJmhe22RDXysS1ibFJlWz4NT0s3itJRYq3XJzXXHXEZkAQplU+nIS1IlbbKjA4zDQaeCyYtg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^18.7.18",
+        "axios": "^1.12.2",
+        "tslib": "^2.8.1"
+      }
+    },
+    "node_modules/@polymarket/clob-client": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/@polymarket/clob-client/-/clob-client-5.2.1.tgz",
+      "integrity": "sha512-ZAYzCiPbAATcocUg2Uh+ZuktpQcyMZXyRWvV4w0mCXQy46qHYCgLoQBWEWDFEcQpkteqf9cEngOcr31tjMuuBg==",
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/providers": "^5.7.2",
+        "@ethersproject/units": "^5.7.0",
+        "@ethersproject/wallet": "^5.7.0",
+        "@polymarket/builder-signing-sdk": "^0.0.8",
+        "@polymarket/order-utils": "^3.0.1",
+        "axios": "^1.0.0",
+        "browser-or-node": "^2.1.1",
+        "ethers": "^5.7.1",
+        "tslib": "^2.4.0"
+      },
+      "engines": {
+        "node": ">=20.10"
+      }
+    },
+    "node_modules/@polymarket/order-utils": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/@polymarket/order-utils/-/order-utils-3.0.1.tgz",
+      "integrity": "sha512-XVcVladfGtC/VmboMkcszqYs82rvath/0XFWqzIFfq8O4atVOU8ykPOGJ2ZfodBPcXETzL+2u1rcepLMLKu9AQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/providers": "^5.7.2",
+        "@ethersproject/wallet": "^5.7.0",
+        "ethers": "^5.7.1",
+        "tslib": "^2.4.0",
+        "viem": "^2.31.4"
+      },
+      "engines": {
+        "node": ">=20.10",
+        "yarn": ">=1"
+      }
+    },
+    "node_modules/@remix-run/router": {
+      "version": "1.23.2",
+      "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+      "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@rolldown/pluginutils": {
+      "version": "1.0.0-beta.27",
+      "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+      "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+      "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+      "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+      "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+      "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+      "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+      "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+      "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+      "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+      "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+      "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+      "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loong64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+      "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+      "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+      "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+      "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+      "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+      "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+      "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+      "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openbsd-x64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+      "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+      "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+      "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+      "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-gnu": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+      "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+      "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@scure/base": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
+      "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip32": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
+      "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "~1.9.0",
+        "@noble/hashes": "~1.8.0",
+        "@scure/base": "~1.2.5"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip39": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
+      "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "~1.8.0",
+        "@scure/base": "~1.2.5"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@types/babel__core": {
+      "version": "7.20.5",
+      "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+      "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.20.7",
+        "@babel/types": "^7.20.7",
+        "@types/babel__generator": "*",
+        "@types/babel__template": "*",
+        "@types/babel__traverse": "*"
+      }
+    },
+    "node_modules/@types/babel__generator": {
+      "version": "7.27.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+      "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__template": {
+      "version": "7.4.4",
+      "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+      "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/parser": "^7.1.0",
+        "@babel/types": "^7.0.0"
+      }
+    },
+    "node_modules/@types/babel__traverse": {
+      "version": "7.28.0",
+      "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+      "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.28.2"
+      }
+    },
+    "node_modules/@types/d3-array": {
+      "version": "3.2.2",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-color": {
+      "version": "3.1.3",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-ease": {
+      "version": "3.0.2",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-interpolate": {
+      "version": "3.0.4",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-color": "*"
+      }
+    },
+    "node_modules/@types/d3-path": {
+      "version": "3.1.1",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-scale": {
+      "version": "4.0.9",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-time": "*"
+      }
+    },
+    "node_modules/@types/d3-shape": {
+      "version": "3.1.8",
+      "license": "MIT",
+      "dependencies": {
+        "@types/d3-path": "*"
+      }
+    },
+    "node_modules/@types/d3-time": {
+      "version": "3.0.4",
+      "license": "MIT"
+    },
+    "node_modules/@types/d3-timer": {
+      "version": "3.0.2",
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "18.19.130",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+      "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@vitejs/plugin-react": {
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+      "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/core": "^7.28.0",
+        "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+        "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+        "@rolldown/pluginutils": "1.0.0-beta.27",
+        "@types/babel__core": "^7.20.5",
+        "react-refresh": "^0.17.0"
+      },
+      "engines": {
+        "node": "^14.18.0 || >=16.0.0"
+      },
+      "peerDependencies": {
+        "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+      }
+    },
+    "node_modules/abitype": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz",
+      "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/wevm"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4",
+        "zod": "^3.22.0 || ^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "zod": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/aes-js": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz",
+      "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==",
+      "license": "MIT"
+    },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/any-promise": {
+      "version": "1.3.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/anymatch": {
+      "version": "3.1.3",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "normalize-path": "^3.0.0",
+        "picomatch": "^2.0.4"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/arg": {
+      "version": "5.0.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/autoprefixer": {
+      "version": "10.4.24",
+      "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.1",
+        "caniuse-lite": "^1.0.30001766",
+        "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/axios": {
+      "version": "1.13.5",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
+      "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
+      "license": "MIT",
+      "dependencies": {
+        "follow-redirects": "^1.15.11",
+        "form-data": "^4.0.5",
+        "proxy-from-env": "^1.1.0"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "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/baseline-browser-mapping": {
+      "version": "2.9.19",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "baseline-browser-mapping": "dist/cli.js"
+      }
+    },
+    "node_modules/bech32": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz",
+      "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==",
+      "license": "MIT"
+    },
+    "node_modules/bignumber.js": {
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
+      "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/binary-extensions": {
+      "version": "2.3.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/bn.js": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz",
+      "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==",
+      "license": "MIT"
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+      "license": "MIT"
+    },
+    "node_modules/browser-or-node": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz",
+      "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==",
+      "license": "MIT"
+    },
+    "node_modules/browserslist": {
+      "version": "4.28.1",
+      "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.9.0",
+        "caniuse-lite": "^1.0.30001759",
+        "electron-to-chromium": "^1.5.263",
+        "node-releases": "^2.0.27",
+        "update-browserslist-db": "^1.2.0"
+      },
+      "bin": {
+        "browserslist": "cli.js"
+      },
+      "engines": {
+        "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+      }
+    },
+    "node_modules/buffer-equal-constant-time": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+      "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/camelcase-css": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/caniuse-lite": {
+      "version": "1.0.30001769",
+      "dev": true,
+      "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",
+      "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",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/clsx": {
+      "version": "2.1.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "license": "MIT"
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/commander": {
+      "version": "4.1.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/cssesc": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "cssesc": "bin/cssesc"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/csstype": {
+      "version": "3.2.3",
+      "license": "MIT"
+    },
+    "node_modules/d3-array": {
+      "version": "3.2.4",
+      "license": "ISC",
+      "dependencies": {
+        "internmap": "1 - 2"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-color": {
+      "version": "3.1.0",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-ease": {
+      "version": "3.0.1",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-format": {
+      "version": "3.1.2",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-interpolate": {
+      "version": "3.0.1",
+      "license": "ISC",
+      "dependencies": {
+        "d3-color": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-path": {
+      "version": "3.1.0",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-scale": {
+      "version": "4.0.2",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2.10.0 - 3",
+        "d3-format": "1 - 3",
+        "d3-interpolate": "1.2.0 - 3",
+        "d3-time": "2.1.1 - 3",
+        "d3-time-format": "2 - 4"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-shape": {
+      "version": "3.2.0",
+      "license": "ISC",
+      "dependencies": {
+        "d3-path": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time": {
+      "version": "3.1.0",
+      "license": "ISC",
+      "dependencies": {
+        "d3-array": "2 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-time-format": {
+      "version": "4.1.0",
+      "license": "ISC",
+      "dependencies": {
+        "d3-time": "1 - 3"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/d3-timer": {
+      "version": "3.0.1",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/data-uri-to-buffer": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
+      "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decimal.js-light": {
+      "version": "2.5.1",
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/didyoumean": {
+      "version": "1.2.2",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "node_modules/dlv": {
+      "version": "1.1.3",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/dom-helpers": {
+      "version": "5.2.1",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/runtime": "^7.8.7",
+        "csstype": "^3.0.2"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "license": "MIT"
+    },
+    "node_modules/ecdsa-sig-formatter": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+      "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/electron-to-chromium": {
+      "version": "1.5.286",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/elliptic": {
+      "version": "6.6.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
+      "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
+      "license": "MIT",
+      "dependencies": {
+        "bn.js": "^4.11.9",
+        "brorand": "^1.1.0",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.1",
+        "inherits": "^2.0.4",
+        "minimalistic-assert": "^1.0.1",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/elliptic/node_modules/bn.js": {
+      "version": "4.12.2",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz",
+      "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==",
+      "license": "MIT"
+    },
+    "node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "license": "MIT"
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+      "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/escalade": {
+      "version": "3.2.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/ethers": {
+      "version": "5.8.0",
+      "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.8.0.tgz",
+      "integrity": "sha512-DUq+7fHrCg1aPDFCHx6UIPb3nmt2XMpM7Y/g2gLhsl3lIBqeAfOJIl1qEvRf2uq3BiKxmh6Fh5pfp2ieyek7Kg==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2"
+        },
+        {
+          "type": "individual",
+          "url": "https://www.buymeacoffee.com/ricmoo"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@ethersproject/abi": "5.8.0",
+        "@ethersproject/abstract-provider": "5.8.0",
+        "@ethersproject/abstract-signer": "5.8.0",
+        "@ethersproject/address": "5.8.0",
+        "@ethersproject/base64": "5.8.0",
+        "@ethersproject/basex": "5.8.0",
+        "@ethersproject/bignumber": "5.8.0",
+        "@ethersproject/bytes": "5.8.0",
+        "@ethersproject/constants": "5.8.0",
+        "@ethersproject/contracts": "5.8.0",
+        "@ethersproject/hash": "5.8.0",
+        "@ethersproject/hdnode": "5.8.0",
+        "@ethersproject/json-wallets": "5.8.0",
+        "@ethersproject/keccak256": "5.8.0",
+        "@ethersproject/logger": "5.8.0",
+        "@ethersproject/networks": "5.8.0",
+        "@ethersproject/pbkdf2": "5.8.0",
+        "@ethersproject/properties": "5.8.0",
+        "@ethersproject/providers": "5.8.0",
+        "@ethersproject/random": "5.8.0",
+        "@ethersproject/rlp": "5.8.0",
+        "@ethersproject/sha2": "5.8.0",
+        "@ethersproject/signing-key": "5.8.0",
+        "@ethersproject/solidity": "5.8.0",
+        "@ethersproject/strings": "5.8.0",
+        "@ethersproject/transactions": "5.8.0",
+        "@ethersproject/units": "5.8.0",
+        "@ethersproject/wallet": "5.8.0",
+        "@ethersproject/web": "5.8.0",
+        "@ethersproject/wordlists": "5.8.0"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "4.0.7",
+      "license": "MIT"
+    },
+    "node_modules/extend": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+      "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+      "license": "MIT"
+    },
+    "node_modules/fast-equals": {
+      "version": "5.4.0",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "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",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fastq": {
+      "version": "1.20.1",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/fetch-blob": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
+      "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "paypal",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "node-domexception": "^1.0.0",
+        "web-streams-polyfill": "^3.0.3"
+      },
+      "engines": {
+        "node": "^12.20 || >= 14.13"
+      }
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/follow-redirects": {
+      "version": "1.15.11",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+      "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+      "funding": [
+        {
+          "type": "individual",
+          "url": "https://github.com/sponsors/RubenVerborgh"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      },
+      "peerDependenciesMeta": {
+        "debug": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/formdata-polyfill": {
+      "version": "4.0.10",
+      "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
+      "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
+      "license": "MIT",
+      "dependencies": {
+        "fetch-blob": "^3.1.2"
+      },
+      "engines": {
+        "node": ">=12.20.0"
+      }
+    },
+    "node_modules/fraction.js": {
+      "version": "5.3.4",
+      "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",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gaxios": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.3.tgz",
+      "integrity": "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "extend": "^3.0.2",
+        "https-proxy-agent": "^7.0.1",
+        "node-fetch": "^3.3.2",
+        "rimraf": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/gcp-metadata": {
+      "version": "8.1.2",
+      "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
+      "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "google-logging-utils": "^1.0.0",
+        "json-bigint": "^1.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/gensync": {
+      "version": "1.0.0-beta.2",
+      "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+      "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/glob": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+      "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/google-auth-library": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.5.0.tgz",
+      "integrity": "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "base64-js": "^1.3.0",
+        "ecdsa-sig-formatter": "^1.0.11",
+        "gaxios": "^7.0.0",
+        "gcp-metadata": "^8.0.0",
+        "google-logging-utils": "^1.0.0",
+        "gtoken": "^8.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/google-logging-utils": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
+      "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=14"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/gtoken": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz",
+      "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==",
+      "license": "MIT",
+      "dependencies": {
+        "gaxios": "^7.0.0",
+        "jws": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+      "license": "MIT",
+      "dependencies": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/internmap": {
+      "version": "2.0.3",
+      "license": "ISC",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/is-binary-path": {
+      "version": "2.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "binary-extensions": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-core-module": {
+      "version": "2.16.1",
+      "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",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "license": "ISC"
+    },
+    "node_modules/isows": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
+      "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "peerDependencies": {
+        "ws": "*"
+      }
+    },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/jiti": {
+      "version": "1.21.7",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jiti": "bin/jiti.js"
+      }
+    },
+    "node_modules/js-sha3": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+      "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==",
+      "license": "MIT"
+    },
+    "node_modules/js-tokens": {
+      "version": "4.0.0",
+      "license": "MIT"
+    },
+    "node_modules/jsesc": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+      "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "jsesc": "bin/jsesc"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/json-bigint": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
+      "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bignumber.js": "^9.0.0"
+      }
+    },
+    "node_modules/json5": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+      "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "json5": "lib/cli.js"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/jwa": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
+      "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer-equal-constant-time": "^1.0.1",
+        "ecdsa-sig-formatter": "1.0.11",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/jws": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
+      "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
+      "license": "MIT",
+      "dependencies": {
+        "jwa": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "node_modules/lilconfig": {
+      "version": "3.1.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antonk52"
+      }
+    },
+    "node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/lodash": {
+      "version": "4.17.23",
+      "license": "MIT"
+    },
+    "node_modules/loose-envify": {
+      "version": "1.4.0",
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^3.0.0 || ^4.0.0"
+      },
+      "bin": {
+        "loose-envify": "cli.js"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+      "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^3.0.2"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+      "license": "ISC"
+    },
+    "node_modules/minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+      "license": "MIT"
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "license": "MIT"
+    },
+    "node_modules/mz": {
+      "version": "2.7.0",
+      "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",
+      "dev": true,
+      "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/node-domexception": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+      "deprecated": "Use your platform's native DOMException instead",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "github",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.5.0"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
+      "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
+      "license": "MIT",
+      "dependencies": {
+        "data-uri-to-buffer": "^4.0.0",
+        "fetch-blob": "^3.1.4",
+        "formdata-polyfill": "^4.0.10"
+      },
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/node-fetch"
+      }
+    },
+    "node_modules/node-releases": {
+      "version": "2.0.27",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/normalize-path": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-hash": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/ox": {
+      "version": "0.12.1",
+      "resolved": "https://registry.npmjs.org/ox/-/ox-0.12.1.tgz",
+      "integrity": "sha512-uU0llpthaaw4UJoXlseCyBHmQ3bLrQmz9rRLIAUHqv46uHuae9SE+ukYBRIPVCnlEnHKuWjDUcDFHWx9gbGNoA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@adraffy/ens-normalize": "^1.11.0",
+        "@noble/ciphers": "^1.3.0",
+        "@noble/curves": "1.9.1",
+        "@noble/hashes": "^1.8.0",
+        "@scure/bip32": "^1.7.0",
+        "@scure/bip39": "^1.6.0",
+        "abitype": "^1.2.3",
+        "eventemitter3": "5.0.1"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ox/node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+      "license": "MIT"
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-parse": {
+      "version": "1.0.7",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "license": "ISC"
+    },
+    "node_modules/pg": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
+      "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.11.0",
+        "pg-pool": "^3.11.0",
+        "pg-protocol": "^1.11.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.11.0",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz",
+      "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==",
+      "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.11.0",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz",
+      "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz",
+      "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==",
+      "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",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pify": {
+      "version": "2.3.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/pirates": {
+      "version": "4.0.7",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "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",
+      "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",
+      "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",
+      "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",
+      "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",
+      "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",
+      "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/prop-types": {
+      "version": "15.8.1",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.4.0",
+        "object-assign": "^4.1.1",
+        "react-is": "^16.13.1"
+      }
+    },
+    "node_modules/prop-types/node_modules/react-is": {
+      "version": "16.13.1",
+      "license": "MIT"
+    },
+    "node_modules/proxy-from-env": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+      "license": "MIT"
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "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": "18.3.1",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-dom": {
+      "version": "18.3.1",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0",
+        "scheduler": "^0.23.2"
+      },
+      "peerDependencies": {
+        "react": "^18.3.1"
+      }
+    },
+    "node_modules/react-is": {
+      "version": "18.3.1",
+      "license": "MIT"
+    },
+    "node_modules/react-refresh": {
+      "version": "0.17.0",
+      "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+      "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/react-router": {
+      "version": "6.30.3",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+      "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+      "license": "MIT",
+      "dependencies": {
+        "@remix-run/router": "1.23.2"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8"
+      }
+    },
+    "node_modules/react-router-dom": {
+      "version": "6.30.3",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+      "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+      "license": "MIT",
+      "dependencies": {
+        "@remix-run/router": "1.23.2",
+        "react-router": "6.30.3"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      },
+      "peerDependencies": {
+        "react": ">=16.8",
+        "react-dom": ">=16.8"
+      }
+    },
+    "node_modules/react-smooth": {
+      "version": "4.0.4",
+      "license": "MIT",
+      "dependencies": {
+        "fast-equals": "^5.0.1",
+        "prop-types": "^15.8.1",
+        "react-transition-group": "^4.4.5"
+      },
+      "peerDependencies": {
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/react-transition-group": {
+      "version": "4.4.5",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@babel/runtime": "^7.5.5",
+        "dom-helpers": "^5.0.1",
+        "loose-envify": "^1.4.0",
+        "prop-types": "^15.6.2"
+      },
+      "peerDependencies": {
+        "react": ">=16.6.0",
+        "react-dom": ">=16.6.0"
+      }
+    },
+    "node_modules/read-cache": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pify": "^2.3.0"
+      }
+    },
+    "node_modules/readdirp": {
+      "version": "3.6.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "picomatch": "^2.2.1"
+      },
+      "engines": {
+        "node": ">=8.10.0"
+      }
+    },
+    "node_modules/recharts": {
+      "version": "2.15.4",
+      "license": "MIT",
+      "dependencies": {
+        "clsx": "^2.0.0",
+        "eventemitter3": "^4.0.1",
+        "lodash": "^4.17.21",
+        "react-is": "^18.3.1",
+        "react-smooth": "^4.0.4",
+        "recharts-scale": "^0.4.4",
+        "tiny-invariant": "^1.3.1",
+        "victory-vendor": "^36.6.8"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "peerDependencies": {
+        "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+        "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+      }
+    },
+    "node_modules/recharts-scale": {
+      "version": "0.4.5",
+      "license": "MIT",
+      "dependencies": {
+        "decimal.js-light": "^2.4.1"
+      }
+    },
+    "node_modules/resolve": {
+      "version": "1.22.11",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "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/reusify": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "5.0.10",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
+      "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^10.3.7"
+      },
+      "bin": {
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.57.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+      "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.57.1",
+        "@rollup/rollup-android-arm64": "4.57.1",
+        "@rollup/rollup-darwin-arm64": "4.57.1",
+        "@rollup/rollup-darwin-x64": "4.57.1",
+        "@rollup/rollup-freebsd-arm64": "4.57.1",
+        "@rollup/rollup-freebsd-x64": "4.57.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+        "@rollup/rollup-linux-arm64-musl": "4.57.1",
+        "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+        "@rollup/rollup-linux-loong64-musl": "4.57.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+        "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+        "@rollup/rollup-linux-x64-gnu": "4.57.1",
+        "@rollup/rollup-linux-x64-musl": "4.57.1",
+        "@rollup/rollup-openbsd-x64": "4.57.1",
+        "@rollup/rollup-openharmony-arm64": "4.57.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+        "@rollup/rollup-win32-x64-gnu": "4.57.1",
+        "@rollup/rollup-win32-x64-msvc": "4.57.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "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/scheduler": {
+      "version": "0.23.2",
+      "license": "MIT",
+      "dependencies": {
+        "loose-envify": "^1.1.0"
+      }
+    },
+    "node_modules/scrypt-js": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz",
+      "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==",
+      "license": "MIT"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "dev": true,
+      "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/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/string-width-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/sucrase": {
+      "version": "3.35.1",
+      "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",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/tailwindcss": {
+      "version": "3.4.19",
+      "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",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "node_modules/thenify-all": {
+      "version": "1.6.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "thenify": ">= 3.1.0 < 4"
+      },
+      "engines": {
+        "node": ">=0.8"
+      }
+    },
+    "node_modules/tiny-invariant": {
+      "version": "1.3.3",
+      "license": "MIT"
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyglobby/node_modules/fdir": {
+      "version": "6.5.0",
+      "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.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/ts-interface-checker": {
+      "version": "0.1.13",
+      "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/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "license": "MIT"
+    },
+    "node_modules/update-browserslist-db": {
+      "version": "1.2.3",
+      "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/util-deprecate": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/victory-vendor": {
+      "version": "36.9.2",
+      "license": "MIT AND ISC",
+      "dependencies": {
+        "@types/d3-array": "^3.0.3",
+        "@types/d3-ease": "^3.0.0",
+        "@types/d3-interpolate": "^3.0.1",
+        "@types/d3-scale": "^4.0.2",
+        "@types/d3-shape": "^3.1.0",
+        "@types/d3-time": "^3.0.0",
+        "@types/d3-timer": "^3.0.0",
+        "d3-array": "^3.1.6",
+        "d3-ease": "^3.0.1",
+        "d3-interpolate": "^3.0.1",
+        "d3-scale": "^4.0.2",
+        "d3-shape": "^3.1.0",
+        "d3-time": "^3.0.0",
+        "d3-timer": "^3.0.1"
+      }
+    },
+    "node_modules/viem": {
+      "version": "2.45.3",
+      "resolved": "https://registry.npmjs.org/viem/-/viem-2.45.3.tgz",
+      "integrity": "sha512-axOD7rIbGiDHHA1MHKmpqqTz3CMCw7YpE/FVypddQMXL5i364VkNZh9JeEJH17NO484LaZUOMueo35IXyL76Mw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "1.9.1",
+        "@noble/hashes": "1.8.0",
+        "@scure/bip32": "1.7.0",
+        "@scure/bip39": "1.6.0",
+        "abitype": "1.2.3",
+        "isows": "1.0.7",
+        "ox": "0.12.1",
+        "ws": "8.18.3"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/viem/node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite": {
+      "version": "5.4.21",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+      "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/web-streams-polyfill": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+      "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ws": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+      "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+      "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+      "dev": true,
+      "license": "ISC"
+    }
+  }
+}
diff --git a/react-dash/package.json b/react-dash/package.json
new file mode 100644
index 0000000..e14dbf9
--- /dev/null
+++ b/react-dash/package.json
@@ -0,0 +1,28 @@
+{
+  "name": "bertha-react-dash",
+  "version": "1.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite --port 7809 --host",
+    "build": "vite build",
+    "preview": "vite preview --port 7809 --host"
+  },
+  "dependencies": {
+    "@polymarket/clob-client": "^5.2.1",
+    "ethers": "^5.8.0",
+    "google-auth-library": "^10.5.0",
+    "pg": "^8.18.0",
+    "react": "^18.3.0",
+    "react-dom": "^18.3.0",
+    "react-router-dom": "^6.28.0",
+    "recharts": "^2.15.0"
+  },
+  "devDependencies": {
+    "@vitejs/plugin-react": "^4.3.0",
+    "autoprefixer": "^10.4.20",
+    "postcss": "^8.4.49",
+    "tailwindcss": "^3.4.17",
+    "vite": "^5.4.0"
+  }
+}
diff --git a/react-dash/postcss.config.js b/react-dash/postcss.config.js
new file mode 100644
index 0000000..4444a47
--- /dev/null
+++ b/react-dash/postcss.config.js
@@ -0,0 +1,3 @@
+export default {
+  plugins: { tailwindcss: {}, autoprefixer: {} }
+};
diff --git a/react-dash/server.js b/react-dash/server.js
new file mode 100644
index 0000000..63874e3
--- /dev/null
+++ b/react-dash/server.js
@@ -0,0 +1,698 @@
+import http from 'http';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import pg from 'pg';
+import crypto from 'crypto';
+import { OAuth2Client } from 'google-auth-library';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const PORT = 7809;
+const DIST = path.join(__dirname, 'dist');
+
+function requiredEnv(name) {
+  const value = process.env[name];
+  if (!value) {
+    throw new Error(`Missing required environment variable: ${name}`);
+  }
+  return value;
+}
+
+// ── Unified DW Auth Config ──
+const AUTH_USER = process.env.ADMIN_USER || 'admin';
+const AUTH_PASS = requiredEnv('ADMIN_PASSWORD');
+const SESSION_COOKIE = 'bertha_session';
+const SESSION_MAX_AGE = 30 * 24 * 60 * 60; // 30 days in seconds
+const WHITELISTED_IPS = ['127.0.0.1', '::1', '45.61.58.125', 'localhost'];
+
+// ── Google OAuth Config ──
+const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
+const ALLOWED_EMAILS = (process.env.ALLOWED_EMAILS || '').split(',').filter(Boolean);
+const googleClient = GOOGLE_CLIENT_ID ? new OAuth2Client(GOOGLE_CLIENT_ID) : null;
+
+const pool = new pg.Pool({
+  connectionString: requiredEnv('DATABASE_URL'),
+  max: 10,
+  idleTimeoutMillis: 30000,
+});
+
+const MIME = {
+  '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css',
+  '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml',
+  '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff',
+};
+
+async function q(text, params) {
+  return pool.query(text, params);
+}
+
+function json(res, data, status = 200, extraHeaders = {}) {
+  res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', ...extraHeaders });
+  res.end(JSON.stringify(data));
+}
+
+async function readBody(req) {
+  return new Promise(resolve => {
+    let d = '';
+    req.on('data', c => d += c);
+    req.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({}); } });
+  });
+}
+
+// ── Unified Session Helpers ──
+function parseCookies(req) {
+  const obj = {};
+  (req.headers.cookie || '').split(';').forEach(c => {
+    const [k, ...v] = c.trim().split('=');
+    if (k) obj[k] = v.join('=');
+  });
+  return obj;
+}
+
+function getSession(req) {
+  const cookies = parseCookies(req);
+  const token = cookies[SESSION_COOKIE];
+  if (!token) return null;
+  try {
+    const data = JSON.parse(Buffer.from(decodeURIComponent(token), 'base64').toString());
+    if (!data.authenticated) return null;
+    const age = Date.now() - data.loginTime;
+    if (age > SESSION_MAX_AGE * 1000) return null;
+    return data;
+  } catch { return null; }
+}
+
+function getClientIP(req) {
+  return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress || '';
+}
+
+function isWhitelistedIP(req) {
+  const ip = getClientIP(req);
+  return WHITELISTED_IPS.some(w => ip.includes(w));
+}
+
+function hasBasicAuth(req) {
+  const auth = req.headers.authorization;
+  if (!auth || !auth.startsWith('Basic ')) return false;
+  const [user, pass] = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':');
+  return user === AUTH_USER && pass === AUTH_PASS;
+}
+
+function isAuthenticated(req) {
+  if (isWhitelistedIP(req)) return true;
+  if (hasBasicAuth(req)) return true;
+  if (getSession(req)) return true;
+  return false;
+}
+
+function setSessionCookie(res, sessionToken) {
+  const cookie = `${SESSION_COOKIE}=${encodeURIComponent(sessionToken)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${SESSION_MAX_AGE}`;
+  return cookie;
+}
+
+function clearSessionCookie() {
+  return `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`;
+}
+
+// API route handlers
+const routes = {
+  'GET /api/health': async (req, res) => {
+    try {
+      const dbCheck = await q('SELECT 1');
+      const risk = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const r = risk.rows[0] || {};
+      const slack = process.env.SLACK_BOT_TOKEN ? true : false;
+      json(res, {
+        status: 'ok',
+        db_connected: !!dbCheck,
+        paper_mode: r.config?.paper_mode ?? true,
+        kill_switch: r.kill_switch_active ?? false,
+        slack_configured: slack,
+        setup_complete: r.config?.setup_complete || false,
+      });
+    } catch (err) {
+      json(res, { status: 'error', error: err.message }, 500);
+    }
+  },
+
+  // ── Unified Auth Endpoints ──
+  'POST /api/auth/login': async (req, res) => {
+    const body = await readBody(req);
+    if (body.username === AUTH_USER && body.password === AUTH_PASS) {
+      const sessionData = { authenticated: true, username: AUTH_USER, loginTime: Date.now() };
+      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
+      json(res, { success: true, message: 'Login successful', username: AUTH_USER }, 200, {
+        'Set-Cookie': setSessionCookie(res, token),
+      });
+    } else {
+      json(res, { error: 'Invalid credentials' }, 401);
+    }
+  },
+
+  'POST /api/auth/logout': async (req, res) => {
+    json(res, { success: true, message: 'Logged out successfully' }, 200, {
+      'Set-Cookie': clearSessionCookie(),
+    });
+  },
+
+  'POST /api/auth/google': async (req, res) => {
+    if (!googleClient) return json(res, { error: 'Google OAuth not configured' }, 400);
+    const body = await readBody(req);
+    if (!body.credential) return json(res, { error: 'Missing credential' }, 400);
+    try {
+      const ticket = await googleClient.verifyIdToken({ idToken: body.credential, audience: GOOGLE_CLIENT_ID });
+      const payload = ticket.getPayload();
+      const email = payload.email;
+      if (!payload.email_verified) return json(res, { error: 'Email not verified' }, 401);
+      // Check allowed emails (if list configured, enforce it; otherwise allow any Google user)
+      if (ALLOWED_EMAILS.length > 0 && !ALLOWED_EMAILS.includes(email)) {
+        return json(res, { error: 'Email not authorized' }, 403);
+      }
+      const sessionData = { authenticated: true, username: payload.name || email, email, picture: payload.picture, provider: 'google', loginTime: Date.now() };
+      const token = Buffer.from(JSON.stringify(sessionData)).toString('base64');
+      json(res, { success: true, username: payload.name || email, email, picture: payload.picture }, 200, {
+        'Set-Cookie': setSessionCookie(res, token),
+      });
+    } catch (err) {
+      json(res, { error: 'Invalid Google token', details: err.message }, 401);
+    }
+  },
+
+  'GET /api/auth/google-config': async (req, res) => {
+    json(res, { client_id: GOOGLE_CLIENT_ID || null, configured: !!GOOGLE_CLIENT_ID });
+  },
+
+  'GET /api/auth/session': async (req, res) => {
+    if (isWhitelistedIP(req)) {
+      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'whitelisted_ip' });
+    }
+    if (hasBasicAuth(req)) {
+      return json(res, { authenticated: true, username: AUTH_USER, bypassReason: 'basic_auth' });
+    }
+    const session = getSession(req);
+    if (session) {
+      return json(res, { authenticated: true, username: session.username, email: session.email, picture: session.picture, provider: session.provider, loginTime: session.loginTime });
+    }
+    json(res, { authenticated: false }, 401);
+  },
+
+  // ── Setup Wizard ──
+  'GET /api/setup': async (req, res) => {
+    try {
+      const r = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const cfg = r.rows[0]?.config || {};
+      json(res, { setup_complete: cfg.setup_complete || false, setup_progress: cfg.setup_progress || null });
+    } catch (err) { json(res, { error: err.message }, 500); }
+  },
+
+  'POST /api/setup': async (req, res) => {
+    const body = await readBody(req);
+    try {
+      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const row = current.rows[0] || {};
+      if (body.action === 'save_progress') {
+        const newConfig = { ...row.config, setup_progress: body.progress };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+        json(res, { success: true });
+      } else if (body.action === 'complete') {
+        const newConfig = { ...row.config, setup_complete: true, setup_completed_at: new Date().toISOString(), setup_progress: body.progress || row.config?.setup_progress };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+        json(res, { success: true });
+      } else {
+        json(res, { error: 'Unknown action' }, 400);
+      }
+    } catch (err) { json(res, { error: err.message }, 500); }
+  },
+
+  'GET /api/dashboard': async (req, res) => {
+    try {
+      const [markets, weatherMarkets, forecasts, predictions, trades, alerts, risk] = await Promise.all([
+        q('SELECT COUNT(*) as cnt FROM markets'),
+        q("SELECT COUNT(*) as cnt FROM markets WHERE category = 'weather' OR parsed_event IS NOT NULL"),
+        q('SELECT COUNT(*) as cnt FROM forecast_runs'),
+        q('SELECT * FROM predictions ORDER BY asof_ts DESC LIMIT 10'),
+        q('SELECT t.*, o.side, o.outcome FROM trades t LEFT JOIN orders o ON t.order_id = o.order_id ORDER BY t.match_time DESC NULLS LAST LIMIT 10'),
+        q('SELECT * FROM alerts_log ORDER BY ts DESC LIMIT 5'),
+        q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1'),
+      ]);
+      const r = risk.rows[0] || {};
+      json(res, {
+        market_count: parseInt(markets.rows[0]?.cnt || 0),
+        weather_market_count: parseInt(weatherMarkets.rows[0]?.cnt || 0),
+        forecast_count: parseInt(forecasts.rows[0]?.cnt || 0),
+        recent_predictions: predictions.rows,
+        recent_trades: trades.rows,
+        recent_alerts: alerts.rows,
+        risk: {
+          bankroll: parseFloat(r.bankroll || 50),
+          daily_pnl: parseFloat(r.daily_pnl || 0),
+          open_exposure: parseFloat(r.open_exposure || 0),
+          max_drawdown_pct: parseFloat(r.max_drawdown_pct || 0),
+          paper_mode: r.config?.paper_mode ?? true,
+          kill_switch_active: r.kill_switch_active || false,
+          config: r.config || {},
+        },
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'GET /api/markets': async (req, res) => {
+    try {
+      const r = await q('SELECT * FROM markets ORDER BY created_at DESC LIMIT 100');
+      const tokens = await q('SELECT * FROM tokens ORDER BY condition_id');
+      json(res, { markets: r.rows, tokens: tokens.rows });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'POST /api/markets': async (req, res) => {
+    const body = await readBody(req);
+    if (body.action === 'test_api') {
+      try {
+        const r = await fetch('https://gamma-api.polymarket.com/markets?limit=5&active=true');
+        const data = await r.json();
+        json(res, { success: true, market_count: data.length });
+      } catch (err) {
+        json(res, { error: err.message }, 500);
+      }
+    } else if (body.action === 'ingest') {
+      // Forward to Next.js for complex ingest logic
+      try {
+        const r = await fetch('http://127.0.0.1:7800/api/markets', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify(body),
+        });
+        json(res, await r.json());
+      } catch (err) {
+        json(res, { error: 'Backend unavailable: ' + err.message }, 502);
+      }
+    } else {
+      json(res, { error: 'Unknown action' }, 400);
+    }
+  },
+
+  'GET /api/weather': async (req, res) => {
+    try {
+      const r = await q('SELECT * FROM forecast_runs ORDER BY created_at DESC LIMIT 200');
+      json(res, { forecasts: r.rows });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'POST /api/weather': async (req, res) => {
+    const body = await readBody(req);
+    // Forward to Next.js for NWS API calls
+    try {
+      const r = await fetch('http://127.0.0.1:7800/api/weather', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(body),
+      });
+      json(res, await r.json());
+    } catch (err) {
+      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
+    }
+  },
+
+  'GET /api/models': async (req, res) => {
+    try {
+      const [models, predictions] = await Promise.all([
+        q('SELECT * FROM model_versions ORDER BY created_at DESC'),
+        q('SELECT p.*, m.question FROM predictions p LEFT JOIN markets m ON p.condition_id = m.condition_id ORDER BY p.asof_ts DESC LIMIT 50'),
+      ]);
+      json(res, { models: models.rows, predictions: predictions.rows });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'POST /api/models': async (req, res) => {
+    const body = await readBody(req);
+    try {
+      const r = await fetch('http://127.0.0.1:7800/api/models', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(body),
+      });
+      json(res, await r.json());
+    } catch (err) {
+      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
+    }
+  },
+
+  'GET /api/risk': async (req, res) => {
+    try {
+      const r = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const row = r.rows[0] || {};
+      json(res, {
+        state: {
+          bankroll: parseFloat(row.bankroll || 50),
+          daily_pnl: parseFloat(row.daily_pnl || 0),
+          open_exposure: parseFloat(row.open_exposure || 0),
+          max_drawdown_pct: parseFloat(row.max_drawdown_pct || 0),
+          kill_switch_active: row.kill_switch_active || false,
+          kill_reason: row.kill_reason,
+          config: row.config || {},
+        },
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'POST /api/risk': async (req, res) => {
+    const body = await readBody(req);
+    try {
+      const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const row = current.rows[0] || {};
+
+      if (body.action === 'kill_switch_activate') {
+        await q('UPDATE risk_state SET kill_switch_active = true, kill_reason = $1, updated_at = NOW() WHERE id = $2', [body.reason || 'Manual', row.id]);
+      } else if (body.action === 'kill_switch_deactivate') {
+        await q('UPDATE risk_state SET kill_switch_active = false, kill_reason = NULL, updated_at = NOW() WHERE id = $1', [row.id]);
+      } else if (body.action === 'update_config') {
+        const newConfig = { ...row.config, ...body.config };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+      }
+
+      const updated = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+      const u = updated.rows[0] || {};
+      json(res, {
+        success: true,
+        state: {
+          bankroll: parseFloat(u.bankroll || 50),
+          daily_pnl: parseFloat(u.daily_pnl || 0),
+          open_exposure: parseFloat(u.open_exposure || 0),
+          max_drawdown_pct: parseFloat(u.max_drawdown_pct || 0),
+          kill_switch_active: u.kill_switch_active || false,
+          kill_reason: u.kill_reason,
+          config: u.config || {},
+        },
+      });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'GET /api/trades': async (req, res) => {
+    try {
+      const [orders, trades] = await Promise.all([
+        q('SELECT * FROM orders ORDER BY submitted_ts DESC LIMIT 50'),
+        q('SELECT * FROM trades ORDER BY match_time DESC NULLS LAST LIMIT 50'),
+      ]);
+      json(res, { orders: orders.rows, trades: trades.rows });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'POST /api/trades': async (req, res) => {
+    const body = await readBody(req);
+    try {
+      const r = await fetch('http://127.0.0.1:7800/api/trades', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(body),
+      });
+      json(res, await r.json());
+    } catch (err) {
+      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
+    }
+  },
+
+  'GET /api/bankroll': async (req, res) => {
+    try {
+      const [ledger, summary] = await Promise.all([
+        q('SELECT * FROM bankroll_ledger ORDER BY ts DESC LIMIT 200'),
+        q(`SELECT
+          COALESCE(SUM(CASE WHEN event_type='deposit' THEN amount ELSE 0 END), 0) as total_deposits,
+          COALESCE(SUM(CASE WHEN event_type='withdrawal' THEN amount ELSE 0 END), 0) as total_withdrawals,
+          COALESCE(SUM(CASE WHEN event_type='trade_pnl' THEN amount ELSE 0 END), 0) as total_pnl,
+          COALESCE(SUM(CASE WHEN event_type='fee' THEN amount ELSE 0 END), 0) as total_fees,
+          (SELECT balance_after FROM bankroll_ledger ORDER BY ts DESC LIMIT 1) as current_balance
+        FROM bankroll_ledger`),
+      ]);
+      json(res, { ledger: ledger.rows, summary: summary.rows[0] || {} });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'GET /api/alerts': async (req, res) => {
+    try {
+      const r = await q('SELECT * FROM alerts_log ORDER BY ts DESC LIMIT 50');
+      json(res, { alerts: r.rows });
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+  },
+
+  'POST /api/alerts': async (req, res) => {
+    const body = await readBody(req);
+    try {
+      const r = await fetch('http://127.0.0.1:7800/api/alerts', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(body),
+      });
+      json(res, await r.json());
+    } catch (err) {
+      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
+    }
+  },
+
+  'POST /api/workers': async (req, res) => {
+    const body = await readBody(req);
+    try {
+      const r = await fetch('http://127.0.0.1:7800/api/workers', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(body),
+      });
+      json(res, await r.json());
+    } catch (err) {
+      json(res, { error: 'Backend unavailable: ' + err.message }, 502);
+    }
+  },
+
+  // ── Polymarket CLOB API (direct HTTP) ──
+  'POST /api/polymarket': async (req, res) => {
+    const body = await readBody(req);
+    const CLOB = 'https://clob.polymarket.com';
+    const GAMMA = 'https://gamma-api.polymarket.com';
+
+    // Helper: HMAC-SHA256 sign for L2 auth
+    function hmacSign(secret, timestamp, method, path, bodyStr = '') {
+      const message = timestamp + method + path + bodyStr;
+      return crypto.createHmac('sha256', Buffer.from(secret, 'base64')).update(message).digest('base64');
+    }
+
+    // Helper: build L2 auth headers
+    function l2Headers(apiKey, secret, passphrase, method, urlPath, bodyStr = '') {
+      const ts = Math.floor(Date.now() / 1000).toString();
+      const sig = hmacSign(secret, ts, method, urlPath, bodyStr);
+      return { 'POLY_API_KEY': apiKey, 'POLY_PASSPHRASE': passphrase, 'POLY_TIMESTAMP': ts, 'POLY_SIGNATURE': sig };
+    }
+
+    if (body.action === 'test_public') {
+      // Test public CLOB connection — no auth needed
+      try {
+        const [okRes, timeRes] = await Promise.all([
+          fetch(`${CLOB}/ok`),
+          fetch(`${CLOB}/time`),
+        ]);
+        const ok = await okRes.text();
+        const time = await timeRes.text();
+        json(res, { success: true, ok: ok.trim(), server_time: time.trim(), endpoint: CLOB });
+      } catch (err) {
+        json(res, { success: false, error: err.message }, 500);
+      }
+    } else if (body.action === 'save_keys') {
+      // Save manually-entered API credentials (from polymarket.com/settings?tab=builder)
+      if (!body.api_key || !body.secret || !body.passphrase) {
+        return json(res, { error: 'api_key, secret, and passphrase required' }, 400);
+      }
+      try {
+        const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+        const row = current.rows[0] || {};
+        const newConfig = {
+          ...row.config,
+          poly_api_key: body.api_key,
+          poly_api_secret: body.secret,
+          poly_passphrase: body.passphrase,
+          poly_wallet: body.wallet || null,
+          poly_connected: true,
+          poly_connected_at: new Date().toISOString(),
+        };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+        json(res, { success: true, api_key: body.api_key.substring(0, 8) + '...', message: 'Credentials saved' });
+      } catch (err) {
+        json(res, { success: false, error: err.message }, 500);
+      }
+    } else if (body.action === 'auth_test') {
+      // Test authenticated connection with stored L2 credentials
+      try {
+        const current = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+        const cfg = current.rows[0]?.config || {};
+        if (!cfg.poly_api_key || !cfg.poly_api_secret || !cfg.poly_passphrase) {
+          return json(res, { error: 'No Polymarket credentials configured. Save API keys first.' }, 400);
+        }
+        const authHdrs = l2Headers(cfg.poly_api_key, cfg.poly_api_secret, cfg.poly_passphrase, 'GET', '/data/trades');
+        const r = await fetch(`${CLOB}/data/trades`, { headers: authHdrs });
+        if (r.ok) {
+          const data = await r.json();
+          json(res, { success: true, trades: Array.isArray(data) ? data.length : 0, wallet: cfg.poly_wallet, message: 'Authenticated OK' });
+        } else {
+          const errText = await r.text();
+          json(res, { success: false, error: `CLOB returned ${r.status}: ${errText}` });
+        }
+      } catch (err) {
+        json(res, { success: false, error: err.message }, 500);
+      }
+    } else if (body.action === 'get_markets') {
+      // Get live weather markets from Gamma API
+      try {
+        const r = await fetch(`${GAMMA}/markets?limit=50&active=true&tag=weather`);
+        const markets = await r.json();
+        // Also try getting prices for our tracked tokens
+        const tokens = await q('SELECT t.token_id, t.outcome, m.question FROM tokens t JOIN markets m ON t.condition_id = m.condition_id LIMIT 20');
+        const prices = [];
+        for (const tok of tokens.rows) {
+          try {
+            const pr = await fetch(`${CLOB}/price?token_id=${tok.token_id}&side=buy`);
+            if (pr.ok) { const d = await pr.json(); prices.push({ ...tok, price: d.price }); }
+          } catch { /* skip */ }
+        }
+        json(res, { success: true, gamma_markets: markets.length || 0, tracked_prices: prices });
+      } catch (err) {
+        json(res, { success: false, error: err.message }, 500);
+      }
+    } else if (body.action === 'get_orderbook') {
+      if (!body.token_id) return json(res, { error: 'token_id required' }, 400);
+      try {
+        const r = await fetch(`${CLOB}/book?token_id=${body.token_id}`);
+        const book = await r.json();
+        json(res, { success: true, orderbook: book });
+      } catch (err) {
+        json(res, { success: false, error: err.message }, 500);
+      }
+    } else if (body.action === 'status') {
+      // Get connection status from DB
+      try {
+        const current = await q('SELECT config FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+        const cfg = current.rows[0]?.config || {};
+        json(res, {
+          connected: !!cfg.poly_connected,
+          api_key: cfg.poly_api_key ? cfg.poly_api_key.substring(0, 8) + '...' : null,
+          wallet: cfg.poly_wallet || null,
+          connected_at: cfg.poly_connected_at || null,
+          has_secret: !!cfg.poly_api_secret,
+          has_passphrase: !!cfg.poly_passphrase,
+        });
+      } catch (err) {
+        json(res, { error: err.message }, 500);
+      }
+    } else if (body.action === 'disconnect') {
+      try {
+        const current = await q('SELECT * FROM risk_state ORDER BY updated_at DESC LIMIT 1');
+        const row = current.rows[0] || {};
+        const newConfig = { ...row.config, poly_api_key: null, poly_api_secret: null, poly_passphrase: null, poly_wallet: null, poly_connected: false, poly_connected_at: null };
+        await q('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [JSON.stringify(newConfig), row.id]);
+        json(res, { success: true, message: 'Disconnected' });
+      } catch (err) {
+        json(res, { error: err.message }, 500);
+      }
+    } else {
+      json(res, { error: 'Unknown action. Use: test_public, save_keys, auth_test, get_markets, get_orderbook, status, disconnect' }, 400);
+    }
+  },
+};
+
+const server = http.createServer(async (req, res) => {
+  // CORS preflight
+  if (req.method === 'OPTIONS') {
+    res.writeHead(200, {
+      'Access-Control-Allow-Origin': '*',
+      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
+      'Access-Control-Allow-Headers': 'Content-Type',
+    });
+    res.end();
+    return;
+  }
+
+  // API routes - direct DB access
+  const routeKey = `${req.method} ${req.url.split('?')[0]}`;
+  const urlPath = req.url.split('?')[0];
+
+  // Public endpoints (no auth required)
+  const publicPaths = ['/api/auth/login', '/api/auth/logout', '/api/auth/session', '/api/health'];
+  const isPublic = publicPaths.includes(urlPath);
+
+  // Enforce auth on all other API routes
+  if (!isPublic && urlPath.startsWith('/api/') && !isAuthenticated(req)) {
+    return json(res, { error: 'Authentication required' }, 401);
+  }
+
+  if (routes[routeKey]) {
+    try {
+      await routes[routeKey](req, res);
+    } catch (err) {
+      json(res, { error: err.message }, 500);
+    }
+    return;
+  }
+
+  // Fallback: proxy unknown API routes to Next.js backend
+  if (req.url.startsWith('/api/')) {
+    try {
+      let body = '';
+      if (req.method !== 'GET' && req.method !== 'HEAD') {
+        body = await new Promise(resolve => {
+          let d = '';
+          req.on('data', c => d += c);
+          req.on('end', () => resolve(d));
+        });
+      }
+      const fetchOpts = {
+        method: req.method,
+        headers: { 'Content-Type': 'application/json', 'Cookie': req.headers.cookie || '' },
+      };
+      if (body && req.method !== 'GET') fetchOpts.body = body;
+      const apiRes = await fetch(`http://127.0.0.1:7800${req.url}`, fetchOpts);
+      const apiBody = await apiRes.text();
+      res.writeHead(apiRes.status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
+      res.end(apiBody);
+    } catch (err) {
+      json(res, { error: err.message }, 502);
+    }
+    return;
+  }
+
+  // Serve static files
+  let filePath = path.join(DIST, req.url === '/' ? 'index.html' : req.url);
+  if (!fs.existsSync(filePath)) {
+    filePath = path.join(DIST, 'index.html');
+  }
+
+  try {
+    const content = fs.readFileSync(filePath);
+    const ext = path.extname(filePath);
+    res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
+    res.end(content);
+  } catch {
+    res.writeHead(404);
+    res.end('Not found');
+  }
+});
+
+server.listen(PORT, '0.0.0.0', () => {
+  console.log(`[Bertha React] Dashboard running on http://0.0.0.0:${PORT}`);
+  console.log(`[Bertha React] Direct DB + API proxy mode`);
+});
diff --git a/react-dash/src/Layout.jsx b/react-dash/src/Layout.jsx
new file mode 100644
index 0000000..0da7bcc
--- /dev/null
+++ b/react-dash/src/Layout.jsx
@@ -0,0 +1,82 @@
+import React, { useState, useEffect } from 'react';
+import { Outlet, Link, useLocation, useNavigate } from 'react-router-dom';
+import { api, checkSession, logout } from './api';
+
+const NAV = [
+  { to: '/', label: 'Overview', d: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
+  { to: '/markets', label: 'Markets', d: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
+  { to: '/polymarket', label: 'Polymarket', d: 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9' },
+  { to: '/weather', label: 'Weather', d: 'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z' },
+  { to: '/models', label: 'Models', d: 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z' },
+  { to: '/risk', label: 'Risk', d: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z' },
+  { to: '/trades', label: 'Trades', d: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4' },
+  { to: '/bankroll', label: 'Bankroll', d: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
+  { to: '/settings', label: 'Settings', d: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' },
+];
+
+export default function Layout() {
+  const loc = useLocation();
+  const nav = useNavigate();
+  const [h, setH] = useState(null);
+  const [col, setCol] = useState(false);
+  const [authChecked, setAuthChecked] = useState(false);
+
+  useEffect(() => {
+    // Unified auth: verify session with server
+    checkSession().then(ok => {
+      if (!ok) { nav('/login'); return; }
+      setAuthChecked(true);
+      const load = () => api('/health').then(d => {
+        setH(d);
+        if (d && !d.setup_complete) nav('/setup');
+      }).catch(() => {});
+      load();
+      const i = setInterval(load, 20000);
+      return () => clearInterval(i);
+    });
+  }, []);
+
+  if (!authChecked) {
+    return <div className="flex items-center justify-center min-h-screen"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+  }
+
+  const cfg = h?.paper_mode;
+
+  return (
+    <div className="flex min-h-screen">
+      <aside className={`${col ? 'w-16' : 'w-56'} bg-gray-900/50 border-r border-gray-800 flex flex-col transition-all duration-200`}>
+        <div className="p-4 border-b border-gray-800 flex items-center gap-3">
+          <div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0" style={{ background: 'linear-gradient(135deg,#3b82f6,#8b5cf6)' }}>
+            <span className="text-white font-bold text-sm">P</span>
+          </div>
+          {!col && <span className="font-bold text-sm bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">POLY</span>}
+        </div>
+        <nav className="flex-1 p-2 space-y-0.5">
+          {NAV.map(n => {
+            const active = loc.pathname === n.to || (n.to !== '/' && loc.pathname.startsWith(n.to));
+            return (
+              <Link key={n.to} to={n.to} className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition ${active ? 'bg-blue-500/15 text-blue-400 font-medium' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/50'}`}>
+                <svg className="flex-shrink-0" style={{ width: 18, height: 18 }} fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d={n.d} /></svg>
+                {!col && <span>{n.label}</span>}
+              </Link>
+            );
+          })}
+        </nav>
+        <div className="p-3 border-t border-gray-800">
+          {!col && h && <div className="space-y-1.5 text-[11px]">
+            <div className="flex justify-between"><span className="text-gray-500">Mode</span><span className={cfg ? 'text-yellow-400' : 'text-green-400'}>{cfg ? 'PAPER' : 'LIVE'}</span></div>
+            <div className="flex justify-between"><span className="text-gray-500">Kill Switch</span><span className={h.kill_switch ? 'text-red-400' : 'text-green-400'}>{h.kill_switch ? 'ACTIVE' : 'OFF'}</span></div>
+            <div className="flex justify-between"><span className="text-gray-500">DB</span><span className={h.db_connected ? 'text-green-400' : 'text-red-400'}>{h.db_connected ? 'OK' : 'DOWN'}</span></div>
+          </div>}
+          <div className="flex gap-1 mt-2">
+            <button onClick={() => setCol(!col)} className="flex-1 flex items-center justify-center py-1.5 text-gray-500 hover:text-gray-300">
+              <svg className={`w-4 h-4 transition-transform ${col ? '' : 'rotate-180'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" /></svg>
+            </button>
+            {!col && <button onClick={logout} className="px-2 py-1 text-xs text-gray-500 hover:text-red-400">Logout</button>}
+          </div>
+        </div>
+      </aside>
+      <main className="flex-1 overflow-auto"><div className="p-6 max-w-[1600px] mx-auto"><Outlet /></div></main>
+    </div>
+  );
+}
diff --git a/react-dash/src/Login.jsx b/react-dash/src/Login.jsx
new file mode 100644
index 0000000..1075e24
--- /dev/null
+++ b/react-dash/src/Login.jsx
@@ -0,0 +1,168 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { login, checkSession, googleLogin, getGoogleConfig } from './api';
+
+export default function Login() {
+  const [user, setUser] = useState('');
+  const [pass, setPass] = useState('');
+  const [showPass, setShowPass] = useState(false);
+  const [err, setErr] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [checking, setChecking] = useState(true);
+  const [googleReady, setGoogleReady] = useState(false);
+  const [googleClientId, setGoogleClientId] = useState(null);
+  const googleBtnRef = useRef(null);
+  const nav = useNavigate();
+
+  useEffect(() => {
+    checkSession().then(ok => {
+      if (ok) nav('/');
+      else setChecking(false);
+    });
+    // Load Google config
+    getGoogleConfig().then(cfg => {
+      if (cfg.configured && cfg.client_id) {
+        setGoogleClientId(cfg.client_id);
+        loadGoogleScript(cfg.client_id);
+      }
+    });
+  }, []);
+
+  function loadGoogleScript(clientId) {
+    if (document.getElementById('google-gsi')) return;
+    const script = document.createElement('script');
+    script.id = 'google-gsi';
+    script.src = 'https://accounts.google.com/gsi/client';
+    script.async = true;
+    script.defer = true;
+    script.onload = () => initGoogle(clientId);
+    document.head.appendChild(script);
+  }
+
+  function initGoogle(clientId) {
+    if (!window.google?.accounts?.id) return;
+    window.google.accounts.id.initialize({
+      client_id: clientId,
+      callback: handleGoogleResponse,
+      auto_select: false,
+    });
+    setGoogleReady(true);
+    if (googleBtnRef.current) {
+      window.google.accounts.id.renderButton(googleBtnRef.current, {
+        type: 'standard',
+        theme: 'filled_black',
+        size: 'large',
+        text: 'signin_with',
+        shape: 'pill',
+        width: '100%',
+      });
+    }
+  }
+
+  useEffect(() => {
+    if (googleReady && googleBtnRef.current && window.google?.accounts?.id) {
+      window.google.accounts.id.renderButton(googleBtnRef.current, {
+        type: 'standard',
+        theme: 'filled_black',
+        size: 'large',
+        text: 'signin_with',
+        shape: 'pill',
+        width: 360,
+      });
+    }
+  }, [googleReady]);
+
+  async function handleGoogleResponse(response) {
+    setErr('');
+    setLoading(true);
+    try {
+      const result = await googleLogin(response.credential);
+      if (result.success) {
+        nav('/');
+      } else {
+        setErr(result.error || 'Google login failed');
+      }
+    } catch {
+      setErr('Connection error');
+    }
+    setLoading(false);
+  }
+
+  async function submit(e) {
+    e.preventDefault();
+    setErr('');
+    setLoading(true);
+    try {
+      const ok = await login(user, pass);
+      if (ok) nav('/');
+      else setErr('Invalid credentials');
+    } catch {
+      setErr('Connection error');
+    }
+    setLoading(false);
+  }
+
+  if (checking) {
+    return <div className="min-h-screen flex items-center justify-center"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+  }
+
+  return (
+    <div className="min-h-screen flex items-center justify-center p-4">
+      <div className="w-full max-w-md">
+        <div className="text-center mb-8">
+          <div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl mb-4" style={{ background: 'linear-gradient(135deg,#3b82f6,#8b5cf6)' }}>
+            <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" /><circle cx="12" cy="12" r="3" /></svg>
+          </div>
+          <h1 className="text-3xl font-extrabold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">POLY</h1>
+          <p className="text-gray-400 mt-2 text-sm">Weather Forecast Micro-Betting System</p>
+        </div>
+
+        {err && <div className="text-red-400 text-sm bg-red-500/10 rounded-lg p-3 flex items-center gap-2 mb-4">
+          <svg className="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
+          {err}
+        </div>}
+
+        {/* Google Sign-In */}
+        {googleClientId && (
+          <div className="mb-4">
+            <div ref={googleBtnRef} className="flex justify-center" />
+            <div className="relative my-5">
+              <div className="absolute inset-0 flex items-center"><div className="w-full border-t border-gray-800" /></div>
+              <div className="relative flex justify-center text-xs"><span className="px-3 text-gray-500" style={{ background: 'var(--bg)' }}>or sign in with password</span></div>
+            </div>
+          </div>
+        )}
+
+        <form onSubmit={submit} className="card space-y-4">
+          <div>
+            <label className="block text-xs font-medium text-gray-400 mb-1.5">Username</label>
+            <input type="text" value={user} onChange={e => setUser(e.target.value)} required autoComplete="username" className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-blue-500 transition" placeholder="admin" />
+          </div>
+          <div>
+            <label className="block text-xs font-medium text-gray-400 mb-1.5">Password</label>
+            <div className="relative">
+              <input type={showPass ? 'text' : 'password'} value={pass} onChange={e => setPass(e.target.value)} required autoComplete="current-password" className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-3 py-2.5 pr-10 text-sm focus:outline-none focus:border-blue-500 transition" placeholder="Enter password" />
+              <button type="button" onClick={() => setShowPass(!showPass)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300">
+                <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={showPass ? 'M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.879L21 21' : 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z'} /></svg>
+              </button>
+            </div>
+          </div>
+          <button type="submit" disabled={loading} className="btn btn-p w-full py-2.5 flex items-center justify-center gap-2 disabled:opacity-50">
+            {loading && <span className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full" />}
+            {loading ? 'Signing in...' : 'Sign In'}
+          </button>
+        </form>
+
+        <div className="mt-6 grid grid-cols-3 gap-2 text-center">
+          {[{ l: 'Bankroll', v: '$50', c: 'text-green-400' }, { l: 'Mode', v: 'Paper', c: 'text-yellow-400' }, { l: 'Status', v: 'Active', c: 'text-blue-400' }].map(s =>
+            <div key={s.l} className="card !p-3"><div className={`text-lg font-bold ${s.c}`}>{s.v}</div><div className="text-[10px] text-gray-500 uppercase">{s.l}</div></div>
+          )}
+        </div>
+        <div className="text-center mt-4 space-y-1">
+          <p className="text-gray-600 text-[10px]">Session: httpOnly cookie / 30-day expiry / IP whitelist</p>
+          {googleClientId && <p className="text-gray-600 text-[10px]">Google OAuth 2.0 enabled</p>}
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/api.js b/react-dash/src/api.js
new file mode 100644
index 0000000..e97a08d
--- /dev/null
+++ b/react-dash/src/api.js
@@ -0,0 +1,67 @@
+const API = '/api';
+
+// ── Unified DW Auth System ──
+// Uses httpOnly cookie 'bertha_session' set by the server.
+// Client cannot read/write the cookie directly — server manages it.
+
+export async function api(path, opts = {}) {
+  const res = await fetch(`${API}${path}`, {
+    ...opts,
+    credentials: 'same-origin', // send cookies with requests
+    headers: { 'Content-Type': 'application/json', ...opts.headers },
+  });
+  if (res.status === 401 && !path.startsWith('/auth/')) {
+    // Session expired or not authenticated — redirect to login
+    window.location.href = '/login';
+    return {};
+  }
+  return res.json();
+}
+
+export async function post(path, body) {
+  return api(path, { method: 'POST', body: JSON.stringify(body) });
+}
+
+// Check if user has a valid session (server validates the httpOnly cookie)
+export async function checkSession() {
+  try {
+    const data = await api('/auth/session');
+    return data.authenticated === true;
+  } catch {
+    return false;
+  }
+}
+
+// Login via unified auth endpoint — server sets httpOnly session cookie
+export async function login(username, password) {
+  const data = await post('/auth/login', { username, password });
+  return data.success === true;
+}
+
+// Google OAuth login — send Google credential to server for verification
+export async function googleLogin(credential) {
+  const data = await post('/auth/google', { credential });
+  return data;
+}
+
+// Get Google OAuth config (client ID)
+export async function getGoogleConfig() {
+  try {
+    return await api('/auth/google-config');
+  } catch { return { configured: false }; }
+}
+
+// Logout — server clears the httpOnly session cookie
+export async function logout() {
+  await post('/auth/logout', {});
+  window.location.href = '/login';
+}
+
+// Legacy compat — isLoggedIn now checks session asynchronously
+// For sync checks in Layout, we use a lightweight approach
+export function isLoggedIn() {
+  // We can't read httpOnly cookies from JS, so we rely on
+  // the session check endpoint. For immediate nav guard,
+  // Layout.jsx calls checkSession() on mount.
+  return true; // Let Layout handle async check
+}
diff --git a/react-dash/src/index.css b/react-dash/src/index.css
new file mode 100644
index 0000000..e57d00a
--- /dev/null
+++ b/react-dash/src/index.css
@@ -0,0 +1,38 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root { --bg: #0a0e1a; --card: #111827; --border: #1e293b; }
+body { background: var(--bg); color: #e2e8f0; font-family: 'Inter', sans-serif; margin: 0; }
+::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: var(--bg); } ::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
+.card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 1.25rem; }
+.stat-v { font-size: 1.75rem; font-weight: 700; line-height: 1; }
+.badge { display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
+.bg-green { background: rgba(16,185,129,0.15); color: #10b981; }
+.bg-red { background: rgba(239,68,68,0.15); color: #ef4444; }
+.bg-yellow { background: rgba(245,158,11,0.15); color: #f59e0b; }
+.bg-blue { background: rgba(59,130,246,0.15); color: #3b82f6; }
+.bg-purple { background: rgba(139,92,246,0.15); color: #8b5cf6; }
+.pulse-g { animation: pg 2s infinite; }
+@keyframes pg { 0%,100%{box-shadow:0 0 0 0 rgba(16,185,129,0.4)} 50%{box-shadow:0 0 0 6px rgba(16,185,129,0)} }
+.btn { padding: 0.5rem 1rem; border-radius: 8px; font-weight: 600; font-size: 0.875rem; cursor: pointer; border: none; transition: all 0.15s; }
+.btn-p { background: #3b82f6; color: white; } .btn-p:hover { background: #2563eb; }
+.btn-d { background: #ef4444; color: white; } .btn-d:hover { background: #dc2626; }
+.btn-s { background: #10b981; color: white; } .btn-s:hover { background: #059669; }
+table { width: 100%; border-collapse: collapse; }
+th { text-align: left; padding: 0.75rem; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; border-bottom: 1px solid var(--border); }
+td { padding: 0.75rem; border-bottom: 1px solid rgba(30,41,59,0.5); font-size: 0.85rem; }
+tr:hover td { background: rgba(59,130,246,0.03); }
+
+/* Setup Wizard */
+.step-enter { opacity: 0; transform: translateX(24px); }
+.step-active { opacity: 1; transform: translateX(0); transition: opacity 0.35s ease, transform 0.35s ease; }
+.step-exit { opacity: 0; transform: translateX(-24px); transition: opacity 0.2s ease, transform 0.2s ease; }
+@keyframes pulse-blue { 0%,100%{box-shadow:0 0 0 0 rgba(59,130,246,0.4)} 50%{box-shadow:0 0 0 8px rgba(59,130,246,0)} }
+.step-active-pulse { animation: pulse-blue 2s infinite; }
+.stepper-line { height: 2px; flex: 1; background: #1e293b; transition: background 0.3s; }
+.stepper-line.done { background: #22c55e; }
+.wizard-gradient { background: linear-gradient(135deg, #3b82f6, #8b5cf6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
+.wizard-btn { background: linear-gradient(135deg, #3b82f6, #8b5cf6); color: white; padding: 0.75rem 2rem; border-radius: 10px; font-weight: 700; font-size: 1rem; border: none; cursor: pointer; transition: all 0.2s; }
+.wizard-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 20px rgba(59,130,246,0.3); }
+.wizard-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; }
diff --git a/react-dash/src/main.jsx b/react-dash/src/main.jsx
new file mode 100644
index 0000000..fe36d3f
--- /dev/null
+++ b/react-dash/src/main.jsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
+import './index.css';
+import Login from './Login';
+import Layout from './Layout';
+import SetupWizard from './setup/SetupWizard';
+import Dashboard from './pages/Dashboard';
+import Markets from './pages/Markets';
+import Weather from './pages/Weather';
+import Models from './pages/Models';
+import Risk from './pages/Risk';
+import Trades from './pages/Trades';
+import Bankroll from './pages/Bankroll';
+import Settings from './pages/Settings';
+import Polymarket from './pages/Polymarket';
+
+function App() {
+  return (
+    <BrowserRouter>
+      <Routes>
+        <Route path="/login" element={<Login />} />
+        <Route path="/setup" element={<SetupWizard />} />
+        <Route path="/" element={<Layout />}>
+          <Route index element={<Dashboard />} />
+          <Route path="markets" element={<Markets />} />
+          <Route path="polymarket" element={<Polymarket />} />
+          <Route path="weather" element={<Weather />} />
+          <Route path="models" element={<Models />} />
+          <Route path="risk" element={<Risk />} />
+          <Route path="trades" element={<Trades />} />
+          <Route path="bankroll" element={<Bankroll />} />
+          <Route path="settings" element={<Settings />} />
+        </Route>
+      </Routes>
+    </BrowserRouter>
+  );
+}
+
+ReactDOM.createRoot(document.getElementById('root')).render(<App />);
diff --git a/react-dash/src/pages/Bankroll.jsx b/react-dash/src/pages/Bankroll.jsx
new file mode 100644
index 0000000..5041c47
--- /dev/null
+++ b/react-dash/src/pages/Bankroll.jsx
@@ -0,0 +1,153 @@
+import React, { useState, useEffect } from 'react';
+import { api } from '../api';
+import { AreaChart, Area, LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell, ComposedChart } from 'recharts';
+
+const ChartTooltip = ({ active, payload, label, prefix = '$' }) => {
+  if (!active || !payload?.length) return null;
+  return (
+    <div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
+      <p className="text-gray-400 mb-1">{label}</p>
+      {payload.map((p, i) => (
+        <p key={i} style={{ color: p.color }} className="font-mono">{p.name}: {prefix}{typeof p.value === 'number' ? p.value.toFixed(2) : p.value}</p>
+      ))}
+    </div>
+  );
+};
+
+export default function Bankroll() {
+  const [ledger, setLe] = useState([]);
+  const [sum, setSum] = useState({});
+  const [loading, setL] = useState(true);
+
+  useEffect(() => { api('/bankroll').then(d => { setLe(d.ledger || []); setSum(d.summary || {}); setL(false); }); }, []);
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  const ec = { deposit: 'bg-green', withdrawal: 'bg-red', trade_pnl: 'bg-blue', fee: 'bg-yellow', adjustment: 'bg-purple' };
+
+  // Balance over time chart data
+  const balanceData = ledger.slice().reverse().map((e, i) => ({
+    idx: i,
+    time: new Date(e.ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }),
+    balance: parseFloat(e.balance_after || 0),
+    amount: parseFloat(e.amount || 0),
+    event: e.event_type,
+  }));
+
+  // Running P&L chart
+  const pnlData = ledger.slice().reverse().filter(e => e.event_type === 'trade_pnl' || e.event_type === 'fee').map((e, i) => ({
+    idx: i,
+    time: new Date(e.ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
+    pnl: parseFloat(e.amount || 0),
+    event: e.event_type,
+  }));
+
+  // Cumulative P&L
+  let cumPnl = 0;
+  const cumData = pnlData.map(d => {
+    cumPnl += d.pnl;
+    return { ...d, cumPnl };
+  });
+
+  // Event type breakdown
+  const eventCounts = {};
+  ledger.forEach(e => { eventCounts[e.event_type] = (eventCounts[e.event_type] || 0) + 1; });
+  const eventData = Object.entries(eventCounts).map(([name, count]) => ({ name, count }));
+
+  const curBal = parseFloat(sum.current_balance || 50);
+  const pnlPct = ((curBal - 50) / 50 * 100);
+
+  return (
+    <div className="space-y-6">
+      <div><h1 className="text-2xl font-bold">Bankroll</h1><p className="text-gray-500 text-sm">Balance tracking and P&L analysis</p></div>
+
+      <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Balance</p><p className="stat-v text-green-400">${curBal.toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Deposits</p><p className="stat-v text-blue-400">${parseFloat(sum.total_deposits || 50).toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Total P&L</p><p className={`stat-v ${parseFloat(sum.total_pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>${parseFloat(sum.total_pnl || 0).toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Fees</p><p className="stat-v text-yellow-400">${parseFloat(sum.total_fees || 0).toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">ROI</p><p className={`stat-v ${pnlPct >= 0 ? 'text-green-400' : 'text-red-400'}`}>{pnlPct >= 0 ? '+' : ''}{pnlPct.toFixed(1)}%</p></div>
+      </div>
+
+      {/* Balance History Chart */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Balance History</h2>
+        {balanceData.length > 1 ? (
+          <ResponsiveContainer width="100%" height={280}>
+            <AreaChart data={balanceData}>
+              <defs>
+                <linearGradient id="balGrad" x1="0" y1="0" x2="0" y2="1">
+                  <stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
+                  <stop offset="95%" stopColor="#10b981" stopOpacity={0} />
+                </linearGradient>
+              </defs>
+              <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+              <XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} interval="preserveStartEnd" />
+              <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} domain={['dataMin - 1', 'dataMax + 1']} tickFormatter={v => `$${v}`} />
+              <Tooltip content={<ChartTooltip />} />
+              <Area type="monotone" dataKey="balance" stroke="#10b981" strokeWidth={2} fill="url(#balGrad)" name="Balance" dot={false} />
+            </AreaChart>
+          </ResponsiveContainer>
+        ) : <p className="text-gray-500 text-sm text-center py-12">Chart appears after multiple entries</p>}
+      </div>
+
+      {/* P&L Charts */}
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+        {/* Individual P&L events */}
+        <div className="card">
+          <h2 className="font-semibold mb-3">P&L Events</h2>
+          {pnlData.length > 0 ? (
+            <ResponsiveContainer width="100%" height={220}>
+              <BarChart data={pnlData} barSize={16}>
+                <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+                <XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} />
+                <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `$${v}`} />
+                <Tooltip content={<ChartTooltip />} />
+                <Bar dataKey="pnl" name="P&L">
+                  {pnlData.map((entry, i) => (
+                    <Cell key={i} fill={entry.pnl >= 0 ? '#10b981' : '#ef4444'} fillOpacity={0.8} />
+                  ))}
+                </Bar>
+              </BarChart>
+            </ResponsiveContainer>
+          ) : <p className="text-gray-500 text-sm text-center py-12">No P&L events yet</p>}
+        </div>
+
+        {/* Cumulative P&L */}
+        <div className="card">
+          <h2 className="font-semibold mb-3">Cumulative P&L</h2>
+          {cumData.length > 0 ? (
+            <ResponsiveContainer width="100%" height={220}>
+              <AreaChart data={cumData}>
+                <defs>
+                  <linearGradient id="cumGrad" x1="0" y1="0" x2="0" y2="1">
+                    <stop offset="5%" stopColor={cumPnl >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0.3} />
+                    <stop offset="95%" stopColor={cumPnl >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0} />
+                  </linearGradient>
+                </defs>
+                <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+                <XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} />
+                <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `$${v}`} />
+                <Tooltip content={<ChartTooltip />} />
+                <Area type="monotone" dataKey="cumPnl" stroke={cumPnl >= 0 ? '#10b981' : '#ef4444'} strokeWidth={2} fill="url(#cumGrad)" name="Cumulative P&L" />
+              </AreaChart>
+            </ResponsiveContainer>
+          ) : <p className="text-gray-500 text-sm text-center py-12">No P&L events yet</p>}
+        </div>
+      </div>
+
+      {/* Ledger Table */}
+      <div className="card overflow-x-auto"><h2 className="font-semibold mb-3">Ledger Entries</h2>
+        <table><thead><tr><th>Time</th><th>Event</th><th>Amount</th><th>Balance</th><th>Note</th></tr></thead><tbody>
+          {ledger.map((e, i) => <tr key={i}>
+            <td className="text-gray-400 text-sm">{new Date(e.ts).toLocaleString()}</td>
+            <td><span className={`badge ${ec[e.event_type] || 'bg-blue'}`}>{e.event_type}</span></td>
+            <td className={`font-mono ${parseFloat(e.amount) >= 0 ? 'text-green-400' : 'text-red-400'}`}>{parseFloat(e.amount) >= 0 ? '+' : ''}${parseFloat(e.amount).toFixed(2)}</td>
+            <td className="font-mono font-medium">${parseFloat(e.balance_after).toFixed(2)}</td>
+            <td className="text-gray-400 text-sm max-w-[200px] truncate">{e.note || '-'}</td>
+          </tr>)}
+        </tbody></table>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/pages/Dashboard.jsx b/react-dash/src/pages/Dashboard.jsx
new file mode 100644
index 0000000..fabe916
--- /dev/null
+++ b/react-dash/src/pages/Dashboard.jsx
@@ -0,0 +1,347 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { api, post } from '../api';
+import { AreaChart, Area, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell, PieChart, Pie } from 'recharts';
+
+function Stat({ label, value, color, sub, onClick }) {
+  return (
+    <div className={`card ${onClick ? 'cursor-pointer hover:border-blue-500/30' : ''}`} onClick={onClick}>
+      <p className="text-xs text-gray-500 uppercase tracking-wide mb-1">{label}</p>
+      <p className={`stat-v ${color || 'text-white'}`}>{value}</p>
+      {sub && <p className="text-xs text-gray-500 mt-1">{sub}</p>}
+    </div>
+  );
+}
+
+function ActionCard({ icon, title, desc, onClick, busy, color, badge }) {
+  return (
+    <button onClick={onClick} disabled={busy} className={`card text-left hover:border-${color || 'blue'}-500/40 hover:bg-${color || 'blue'}-500/5 transition-all group cursor-pointer disabled:opacity-50`}>
+      <div className="flex items-start gap-3">
+        <div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 bg-${color || 'blue'}-500/15`}>
+          <svg className={`w-5 h-5 text-${color || 'blue'}-400`} fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d={icon} /></svg>
+        </div>
+        <div className="flex-1 min-w-0">
+          <div className="flex items-center gap-2">
+            <p className="font-semibold text-sm group-hover:text-white transition">{title}</p>
+            {badge && <span className={`badge bg-${badge.color || 'blue'}`}>{badge.text}</span>}
+          </div>
+          <p className="text-xs text-gray-500 mt-0.5">{desc}</p>
+        </div>
+        {busy ? (
+          <div className="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full flex-shrink-0 mt-1" />
+        ) : (
+          <svg className="w-4 h-4 text-gray-600 group-hover:text-gray-400 transition flex-shrink-0 mt-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" /></svg>
+        )}
+      </div>
+    </button>
+  );
+}
+
+const ChartTooltip = ({ active, payload, label, prefix = '$' }) => {
+  if (!active || !payload?.length) return null;
+  return (
+    <div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
+      <p className="text-gray-400 mb-1">{label}</p>
+      {payload.map((p, i) => (
+        <p key={i} style={{ color: p.color }} className="font-mono">{p.name}: {prefix}{typeof p.value === 'number' ? p.value.toFixed(2) : p.value}</p>
+      ))}
+    </div>
+  );
+};
+
+export default function Dashboard() {
+  const [d, setD] = useState(null);
+  const [r, setR] = useState(null);
+  const [bankroll, setBankroll] = useState(null);
+  const [loading, setL] = useState(true);
+  const [busy, setBusy] = useState({});
+  const [results, setResults] = useState([]);
+  const nav = useNavigate();
+
+  const load = () => Promise.all([
+    api('/dashboard'), api('/risk'), api('/bankroll')
+  ]).then(([dash, risk, bank]) => {
+    setD(dash); setR(risk?.state); setBankroll(bank); setL(false);
+  }).catch(() => setL(false));
+
+  useEffect(() => { load(); const i = setInterval(load, 12000); return () => clearInterval(i); }, []);
+
+  function addResult(msg, type = 'info') {
+    setResults(prev => [{ msg, type, ts: Date.now() }, ...prev].slice(0, 5));
+  }
+
+  async function runAction(key, fn) {
+    setBusy(b => ({ ...b, [key]: true }));
+    try { await fn(); load(); }
+    catch (e) { addResult(`Error: ${e.message}`, 'error'); }
+    setBusy(b => ({ ...b, [key]: false }));
+  }
+
+  async function ingestMarkets() {
+    await runAction('ingest', async () => {
+      const r = await post('/markets', { action: 'ingest' });
+      addResult(r.error ? `Ingest failed: ${r.error}` : `Ingested ${r.ingested || 0} markets from ${r.total_scanned || 0} scanned`, r.error ? 'error' : 'success');
+    });
+  }
+
+  async function fetchWeather() {
+    await runAction('weather', async () => {
+      const r = await post('/weather', { action: 'fetch_all' });
+      addResult(r.error ? `Weather fetch failed: ${r.error}` : `Fetched forecasts for ${r.cities_fetched || 0} cities`, r.error ? 'error' : 'success');
+    });
+  }
+
+  async function runPredictions() {
+    await runAction('predict', async () => {
+      const r = await post('/models', { action: 'run_predictions' });
+      addResult(r.error ? `Predictions failed: ${r.error}` : `Generated ${r.predictions_made || 0} predictions`, r.error ? 'error' : 'success');
+    });
+  }
+
+  async function runRiskScan() {
+    await runAction('risk', async () => {
+      const r = await api('/risk');
+      const s = r.state || {};
+      addResult(`Risk: $${parseFloat(s.bankroll || 50).toFixed(2)} bankroll, $${parseFloat(s.open_exposure || 0).toFixed(2)} exposure, Kill: ${s.kill_switch_active ? 'ON' : 'off'}`, 'success');
+    });
+  }
+
+  async function runFullPipeline() {
+    await runAction('pipeline', async () => {
+      addResult('Pipeline: Ingest -> Weather -> Predict...', 'info');
+      const m = await post('/markets', { action: 'ingest' });
+      addResult(`1/3 Ingested ${m.ingested || 0} markets`, m.error ? 'error' : 'info');
+      const w = await post('/weather', { action: 'fetch_all' });
+      addResult(`2/3 Weather for ${w.cities_fetched || 0} cities`, w.error ? 'error' : 'info');
+      const p = await post('/models', { action: 'run_predictions' });
+      addResult(`3/3 ${p.predictions_made || 0} predictions generated`, p.error ? 'error' : 'success');
+      addResult('Pipeline complete!', 'success');
+    });
+  }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  const cfg = r?.config || {};
+  const pnl = parseFloat(r?.daily_pnl || 0);
+  const hasMarkets = (d?.market_count || 0) > 0;
+  const hasForecasts = (d?.forecast_count || 0) > 0;
+  const hasPredictions = (d?.recent_predictions?.length || 0) > 0;
+
+  // Bankroll chart data
+  const ledgerData = (bankroll?.ledger || []).slice().reverse().map((e, i) => ({
+    idx: i,
+    time: new Date(e.ts).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
+    balance: parseFloat(e.balance_after || 0),
+    amount: parseFloat(e.amount || 0),
+    event: e.event_type,
+  }));
+
+  // Edge distribution for predictions
+  const edgeData = (d?.recent_predictions || []).filter(p => p.edge != null).map(p => ({
+    name: (p.condition_id || '').substring(0, 8),
+    edge: parseFloat((p.edge * 100).toFixed(2)),
+    pYes: parseFloat((p.p_yes * 100).toFixed(1)),
+    signal: p.recommendation || 'HOLD',
+  }));
+
+  // Risk allocation pie
+  const bankrollVal = parseFloat(r?.bankroll || 50);
+  const exposureVal = parseFloat(r?.open_exposure || 0);
+  const availableVal = bankrollVal - exposureVal;
+  const riskPie = [
+    { name: 'Available', value: Math.max(0, availableVal), fill: '#10b981' },
+    { name: 'Exposed', value: exposureVal, fill: '#3b82f6' },
+  ];
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div><h1 className="text-2xl font-bold">Dashboard</h1><p className="text-gray-500 text-sm mt-0.5">Weather micro-betting system overview</p></div>
+        <div className="flex items-center gap-3">
+          <span className={`badge ${cfg.paper_mode !== false ? 'bg-yellow' : 'bg-green'}`}>{cfg.paper_mode !== false ? 'PAPER MODE' : 'LIVE MODE'}</span>
+          {r?.kill_switch_active && <span className="badge bg-red">KILL SWITCH</span>}
+          <span className={`w-2.5 h-2.5 rounded-full ${r?.kill_switch_active ? 'bg-red-500' : 'bg-green-500 pulse-g'}`} />
+        </div>
+      </div>
+
+      {/* Top Stats */}
+      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <Stat label="Bankroll" value={`$${bankrollVal.toFixed(2)}`} color="text-green-400" sub="Starting: $50.00" onClick={() => nav('/bankroll')} />
+        <Stat label="Daily P&L" value={`$${pnl.toFixed(2)}`} color={pnl >= 0 ? 'text-green-400' : 'text-red-400'} sub={`Limit: -$${cfg.max_daily_loss_usd || 5}`} />
+        <Stat label="Open Exposure" value={`$${exposureVal.toFixed(2)}`} color="text-blue-400" sub={`Max: $${cfg.max_open_exposure_usd || 5}`} />
+        <Stat label="Max Drawdown" value={`${parseFloat(r?.max_drawdown_pct || 0).toFixed(1)}%`} color="text-purple-400" sub={`Limit: ${cfg.max_drawdown_pct || 20}%`} />
+      </div>
+
+      {/* Charts Row */}
+      <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
+        {/* Bankroll Chart */}
+        <div className="card lg:col-span-2">
+          <h2 className="font-semibold mb-3">Bankroll History</h2>
+          {ledgerData.length > 1 ? (
+            <ResponsiveContainer width="100%" height={200}>
+              <AreaChart data={ledgerData}>
+                <defs>
+                  <linearGradient id="bgGreen" x1="0" y1="0" x2="0" y2="1">
+                    <stop offset="5%" stopColor="#10b981" stopOpacity={0.3} />
+                    <stop offset="95%" stopColor="#10b981" stopOpacity={0} />
+                  </linearGradient>
+                </defs>
+                <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+                <XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
+                <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} domain={['dataMin - 2', 'dataMax + 2']} tickFormatter={v => `$${v}`} />
+                <Tooltip content={<ChartTooltip />} />
+                <Area type="monotone" dataKey="balance" stroke="#10b981" strokeWidth={2} fill="url(#bgGreen)" name="Balance" />
+              </AreaChart>
+            </ResponsiveContainer>
+          ) : <p className="text-gray-500 text-sm text-center py-12">Chart appears after trades begin</p>}
+        </div>
+
+        {/* Risk Allocation Pie */}
+        <div className="card">
+          <h2 className="font-semibold mb-3">Capital Allocation</h2>
+          <ResponsiveContainer width="100%" height={200}>
+            <PieChart>
+              <Pie data={riskPie} cx="50%" cy="50%" innerRadius={50} outerRadius={80} paddingAngle={4} dataKey="value">
+                {riskPie.map((entry, i) => <Cell key={i} fill={entry.fill} />)}
+              </Pie>
+              <Tooltip content={<ChartTooltip />} />
+            </PieChart>
+          </ResponsiveContainer>
+          <div className="flex justify-center gap-4 text-xs mt-2">
+            <span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-green-500" />Available ${availableVal.toFixed(2)}</span>
+            <span className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-blue-500" />Exposed ${exposureVal.toFixed(2)}</span>
+          </div>
+        </div>
+      </div>
+
+      {/* Edge Distribution */}
+      {edgeData.length > 0 && (
+        <div className="card">
+          <h2 className="font-semibold mb-3">Prediction Edge Distribution</h2>
+          <ResponsiveContainer width="100%" height={180}>
+            <BarChart data={edgeData} barSize={24}>
+              <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+              <XAxis dataKey="name" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} />
+              <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} />
+              <Tooltip content={<ChartTooltip prefix="" />} />
+              <Bar dataKey="edge" name="Edge %">
+                {edgeData.map((entry, i) => (
+                  <Cell key={i} fill={entry.edge > 0 ? '#10b981' : '#ef4444'} fillOpacity={0.8} />
+                ))}
+              </Bar>
+            </BarChart>
+          </ResponsiveContainer>
+        </div>
+      )}
+
+      {/* Quick Actions */}
+      <div className="card" style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.05), rgba(139,92,246,0.05))' }}>
+        <div className="flex items-center justify-between mb-4">
+          <div>
+            <h2 className="font-semibold">Quick Actions</h2>
+            <p className="text-xs text-gray-500 mt-0.5">Run pipeline steps or navigate to details</p>
+          </div>
+          <button onClick={runFullPipeline} disabled={busy.pipeline} className="btn btn-p text-xs flex items-center gap-2">
+            {busy.pipeline ? <span className="animate-spin w-3 h-3 border-2 border-white border-t-transparent rounded-full" /> : <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>}
+            Run Full Pipeline
+          </button>
+        </div>
+        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
+          <ActionCard icon="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" title="Ingest Markets" desc="Scan Polymarket for weather events" onClick={ingestMarkets} busy={busy.ingest} color="blue" badge={hasMarkets ? { text: `${d.market_count} tracked`, color: 'blue' } : { text: 'Start here', color: 'yellow' }} />
+          <ActionCard icon="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z" title="Fetch Weather" desc="Pull latest NWS forecasts" onClick={fetchWeather} busy={busy.weather} color="green" badge={hasForecasts ? { text: `${d.forecast_count} runs`, color: 'green' } : null} />
+          <ActionCard icon="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" title="Run Predictions" desc="Generate P(YES) + edge" onClick={runPredictions} busy={busy.predict} color="purple" badge={hasPredictions ? { text: `${d.recent_predictions.length} active`, color: 'purple' } : null} />
+          <ActionCard icon="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" title="Risk Scan" desc="Check limits and kill switch" onClick={runRiskScan} busy={busy.risk} color="yellow" />
+          <ActionCard icon="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8V7m0 1v8m0 0v1" title="View Bankroll" desc="Balance history and P&L" onClick={() => nav('/bankroll')} color="green" />
+          <ActionCard icon="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9" title="Polymarket CLOB" desc="Live prices, orderbooks & trading" onClick={() => nav('/polymarket')} color="purple" badge={{ text: 'LIVE', color: 'purple' }} />
+        </div>
+      </div>
+
+      {/* Action Log */}
+      {results.length > 0 && (
+        <div className="card">
+          <div className="flex items-center justify-between mb-3">
+            <h2 className="font-semibold text-sm">Action Log</h2>
+            <button onClick={() => setResults([])} className="text-xs text-gray-500 hover:text-gray-300">Clear</button>
+          </div>
+          <div className="space-y-1.5">
+            {results.map((r, i) => (
+              <div key={r.ts + i} className={`flex items-center gap-2 text-xs px-3 py-2 rounded-lg ${r.type === 'error' ? 'bg-red-500/10 text-red-400' : r.type === 'success' ? 'bg-green-500/10 text-green-400' : 'bg-blue-500/10 text-blue-400'}`}>
+                <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${r.type === 'error' ? 'bg-red-500' : r.type === 'success' ? 'bg-green-500' : 'bg-blue-500'}`} />
+                <span className="flex-1">{r.msg}</span>
+                <span className="text-gray-600">{new Date(r.ts).toLocaleTimeString()}</span>
+              </div>
+            ))}
+          </div>
+        </div>
+      )}
+
+      {/* Predictions & Trades */}
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+        <div className="card">
+          <div className="flex items-center justify-between mb-3">
+            <h2 className="font-semibold">Recent Predictions</h2>
+            <button onClick={() => nav('/models')} className="text-xs text-blue-400 hover:text-blue-300">View All</button>
+          </div>
+          {d?.recent_predictions?.length > 0 ? (
+            <table><thead><tr><th>Market</th><th>P(Yes)</th><th>Edge</th><th>Signal</th></tr></thead><tbody>
+              {d.recent_predictions.slice(0, 6).map((p, i) => <tr key={i}>
+                <td className="max-w-[180px] truncate font-mono text-xs">{p.condition_id?.substring(0, 20)}</td>
+                <td className="font-mono text-green-400">{(p.p_yes * 100).toFixed(1)}%</td>
+                <td className={`font-mono ${p.edge > 0 ? 'text-green-400' : 'text-red-400'}`}>{p.edge ? (p.edge * 100).toFixed(1) + '%' : '-'}</td>
+                <td><span className={`badge ${p.recommendation?.startsWith('BUY') ? 'bg-green' : 'bg-yellow'}`}>{p.recommendation || 'HOLD'}</span></td>
+              </tr>)}
+            </tbody></table>
+          ) : <p className="text-gray-500 text-sm">No predictions yet. Ingest markets to begin.</p>}
+        </div>
+        <div className="card">
+          <div className="flex items-center justify-between mb-3">
+            <h2 className="font-semibold">Recent Trades</h2>
+            <button onClick={() => nav('/trades')} className="text-xs text-blue-400 hover:text-blue-300">View All</button>
+          </div>
+          {d?.recent_trades?.length > 0 ? (
+            <table><thead><tr><th>Time</th><th>Side</th><th>Size</th><th>P&L</th></tr></thead><tbody>
+              {d.recent_trades.map((t, i) => <tr key={i}>
+                <td className="text-gray-400 text-sm">{new Date(t.match_time).toLocaleString()}</td>
+                <td><span className={`badge ${t.side === 'buy' ? 'bg-green' : 'bg-red'}`}>{t.side?.toUpperCase()}</span></td>
+                <td className="font-mono">${parseFloat(t.size_usd || 0).toFixed(2)}</td>
+                <td className={`font-mono ${parseFloat(t.pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>${parseFloat(t.pnl || 0).toFixed(2)}</td>
+              </tr>)}
+            </tbody></table>
+          ) : <p className="text-gray-500 text-sm">No trades yet. System in paper mode.</p>}
+        </div>
+      </div>
+
+      {/* Bottom Stats */}
+      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+        <div className="card cursor-pointer hover:border-blue-500/30 transition" onClick={() => nav('/markets')}>
+          <p className="text-xs text-gray-500 uppercase mb-1">Markets Tracked</p>
+          <p className="stat-v text-blue-400">{d?.market_count || 0}</p>
+          <p className="text-xs text-gray-500 mt-1">{d?.weather_market_count || 0} weather</p>
+        </div>
+        <div className="card cursor-pointer hover:border-purple-500/30 transition" onClick={() => nav('/weather')}>
+          <p className="text-xs text-gray-500 uppercase mb-1">Forecast Runs</p>
+          <p className="stat-v text-purple-400">{d?.forecast_count || 0}</p>
+        </div>
+        <div className="card cursor-pointer hover:border-green-500/30 transition" onClick={() => nav('/risk')}>
+          <p className="text-xs text-gray-500 uppercase mb-1">Risk Config</p>
+          <div className="space-y-1 text-sm">
+            <div className="flex justify-between"><span className="text-gray-500">Max Bet</span><span>${cfg.max_bet_usd || 0.50}</span></div>
+            <div className="flex justify-between"><span className="text-gray-500">Min Edge</span><span>{((cfg.min_edge_threshold || 0.03) * 100).toFixed(0)}%</span></div>
+            <div className="flex justify-between"><span className="text-gray-500">Kelly</span><span>{cfg.fractional_kelly || 0.10}x</span></div>
+          </div>
+        </div>
+      </div>
+
+      {/* Alerts */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">System Alerts</h2>
+        {d?.recent_alerts?.length > 0 ? <div className="space-y-2">
+          {d.recent_alerts.map((a, i) => <div key={i} className={`flex items-start gap-3 p-3 rounded-lg ${a.severity === 'critical' ? 'bg-red-500/10' : a.severity === 'warning' ? 'bg-yellow-500/10' : 'bg-blue-500/10'}`}>
+            <span className={`badge ${a.severity === 'critical' ? 'bg-red' : a.severity === 'warning' ? 'bg-yellow' : 'bg-blue'}`}>{a.severity}</span>
+            <div><p className="text-sm">{a.message}</p><p className="text-xs text-gray-500 mt-0.5">{new Date(a.ts).toLocaleString()}</p></div>
+          </div>)}
+        </div> : <p className="text-gray-500 text-sm">No alerts. System healthy.</p>}
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/pages/Markets.jsx b/react-dash/src/pages/Markets.jsx
new file mode 100644
index 0000000..99c6fb6
--- /dev/null
+++ b/react-dash/src/pages/Markets.jsx
@@ -0,0 +1,43 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+
+export default function Markets() {
+  const [markets, setM] = useState([]);
+  const [loading, setL] = useState(true);
+  const [busy, setB] = useState(false);
+
+  const load = () => api('/markets').then(d => { setM(d.markets||[]); setL(false); });
+  useEffect(() => { load(); }, []);
+
+  async function ingest() { setB(true); const r = await post('/markets', {action:'ingest'}); alert(`Ingested ${r.ingested||0} weather markets from ${r.total_scanned||0} scanned`); load(); setB(false); }
+  async function parse(id) { const r = await post('/markets', {action:'parse', condition_id:id}); alert(JSON.stringify(r.parsed,null,2)); load(); }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full"/></div>;
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div><h1 className="text-2xl font-bold">Markets</h1><p className="text-gray-500 text-sm">Polymarket weather-related markets</p></div>
+        <button onClick={ingest} disabled={busy} className="btn btn-p">{busy?'Ingesting...':'Ingest Markets'}</button>
+      </div>
+      <div className="grid grid-cols-3 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Total</p><p className="stat-v text-blue-400">{markets.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Active</p><p className="stat-v text-green-400">{markets.filter(m=>m.active).length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Parsed</p><p className="stat-v text-purple-400">{markets.filter(m=>m.parsed_event?.variable).length}</p></div>
+      </div>
+      <div className="card overflow-x-auto">
+        <table><thead><tr><th>Question</th><th>Category</th><th>Liquidity</th><th>End Date</th><th>Parsed</th><th>Actions</th></tr></thead><tbody>
+          {markets.length===0?<tr><td colSpan={6} className="text-center text-gray-500 py-8">No markets. Click Ingest.</td></tr>
+          :markets.map(m=><tr key={m.condition_id}>
+            <td className="max-w-[300px]"><p className="truncate font-medium">{m.question}</p><p className="text-xs text-gray-500 truncate">{m.condition_id}</p></td>
+            <td><span className="badge bg-blue">{m.category||'weather'}</span></td>
+            <td className="font-mono">${parseFloat(m.liquidity||0).toFixed(0)}</td>
+            <td className="text-gray-400 text-sm">{m.end_ts?new Date(m.end_ts).toLocaleDateString():'-'}</td>
+            <td>{m.parsed_event?.variable?<span className="badge bg-green">Yes</span>:<span className="badge bg-yellow">No</span>}</td>
+            <td><button onClick={()=>parse(m.condition_id)} className="text-xs text-blue-400 hover:text-blue-300">Parse</button></td>
+          </tr>)}
+        </tbody></table>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/pages/Models.jsx b/react-dash/src/pages/Models.jsx
new file mode 100644
index 0000000..723da07
--- /dev/null
+++ b/react-dash/src/pages/Models.jsx
@@ -0,0 +1,162 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+import { ScatterChart, Scatter, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell, ZAxis, Legend } from 'recharts';
+
+const ChartTooltip = ({ active, payload }) => {
+  if (!active || !payload?.length) return null;
+  const d = payload[0]?.payload;
+  if (!d) return null;
+  return (
+    <div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
+      <p className="text-gray-400 mb-1 font-mono">{d.id}</p>
+      <p className="text-green-400">P(Yes): {d.pYes?.toFixed(1)}%</p>
+      <p className={d.edge >= 0 ? 'text-green-400' : 'text-red-400'}>Edge: {d.edge?.toFixed(2)}%</p>
+      <p className="text-blue-400">Confidence: {d.confidence?.toFixed(0)}%</p>
+      <p className="text-purple-400">Signal: {d.signal}</p>
+    </div>
+  );
+};
+
+export default function Models() {
+  const [models, setMo] = useState([]);
+  const [preds, setPr] = useState([]);
+  const [loading, setL] = useState(true);
+
+  useEffect(() => { api('/models').then(d => { setMo(d.models || []); setPr(d.predictions || []); setL(false); }).catch(() => setL(false)); }, []);
+
+  async function run() {
+    const r = await post('/models', { action: 'run_predictions' });
+    window.location.reload();
+  }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  // Scatter data: P(Yes) vs Edge
+  const scatterData = preds.filter(p => p.edge != null && p.p_yes != null).map(p => ({
+    id: (p.condition_id || '').substring(0, 12),
+    pYes: parseFloat((p.p_yes * 100).toFixed(1)),
+    edge: parseFloat((p.edge * 100).toFixed(2)),
+    confidence: parseFloat(((p.confidence || 0.5) * 100).toFixed(0)),
+    signal: p.recommendation || 'HOLD',
+  }));
+
+  const buyData = scatterData.filter(d => d.signal.startsWith('BUY'));
+  const holdData = scatterData.filter(d => !d.signal.startsWith('BUY') && d.signal !== 'SKIP');
+  const skipData = scatterData.filter(d => d.signal === 'SKIP');
+
+  // Edge distribution bar
+  const edgeBars = scatterData.map(d => ({
+    name: d.id,
+    edge: d.edge,
+  })).sort((a, b) => b.edge - a.edge);
+
+  // Confidence histogram
+  const confBuckets = [0, 0, 0, 0, 0]; // 0-20, 20-40, 40-60, 60-80, 80-100
+  scatterData.forEach(d => {
+    const idx = Math.min(Math.floor(d.confidence / 20), 4);
+    confBuckets[idx]++;
+  });
+  const confData = confBuckets.map((count, i) => ({
+    range: `${i * 20}-${(i + 1) * 20}%`,
+    count,
+  }));
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div><h1 className="text-2xl font-bold">Models & Predictions</h1><p className="text-gray-500 text-sm">Probabilistic forecasting pipeline</p></div>
+        <button onClick={run} className="btn btn-p">Run Predictions</button>
+      </div>
+
+      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Model Versions</p><p className="stat-v text-blue-400">{models.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Predictions</p><p className="stat-v text-purple-400">{preds.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Active</p><p className="stat-v text-green-400">{models.filter(m => m.active).length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Buy Signals</p><p className="stat-v text-green-400">{buyData.length}</p></div>
+      </div>
+
+      {/* Charts Row */}
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+        {/* P(Yes) vs Edge Scatter */}
+        <div className="card">
+          <h2 className="font-semibold mb-3">P(Yes) vs Edge</h2>
+          {scatterData.length > 0 ? (
+            <ResponsiveContainer width="100%" height={280}>
+              <ScatterChart margin={{ top: 10, right: 10, bottom: 10, left: 0 }}>
+                <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+                <XAxis type="number" dataKey="pYes" name="P(Yes)" unit="%" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} domain={[0, 100]} />
+                <YAxis type="number" dataKey="edge" name="Edge" unit="%" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
+                <ZAxis type="number" dataKey="confidence" range={[40, 200]} />
+                <Tooltip content={<ChartTooltip />} />
+                {buyData.length > 0 && <Scatter name="BUY" data={buyData} fill="#10b981" fillOpacity={0.8} />}
+                {holdData.length > 0 && <Scatter name="HOLD" data={holdData} fill="#eab308" fillOpacity={0.8} />}
+                {skipData.length > 0 && <Scatter name="SKIP" data={skipData} fill="#ef4444" fillOpacity={0.6} />}
+                <Legend wrapperStyle={{ fontSize: 11, color: '#9ca3af' }} />
+              </ScatterChart>
+            </ResponsiveContainer>
+          ) : <p className="text-gray-500 text-sm text-center py-16">Run predictions to see chart</p>}
+        </div>
+
+        {/* Edge Distribution */}
+        <div className="card">
+          <h2 className="font-semibold mb-3">Edge Distribution</h2>
+          {edgeBars.length > 0 ? (
+            <ResponsiveContainer width="100%" height={280}>
+              <BarChart data={edgeBars} barSize={20}>
+                <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+                <XAxis dataKey="name" tick={{ fill: '#6b7280', fontSize: 9 }} axisLine={false} tickLine={false} />
+                <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} />
+                <Tooltip formatter={(v) => [`${v}%`, 'Edge']} contentStyle={{ background: '#111827', border: '1px solid #374151', borderRadius: 8, fontSize: 12 }} />
+                <Bar dataKey="edge" name="Edge %">
+                  {edgeBars.map((entry, i) => (
+                    <Cell key={i} fill={entry.edge > 0 ? '#10b981' : '#ef4444'} fillOpacity={0.8} />
+                  ))}
+                </Bar>
+              </BarChart>
+            </ResponsiveContainer>
+          ) : <p className="text-gray-500 text-sm text-center py-16">No predictions yet</p>}
+        </div>
+      </div>
+
+      {/* Confidence Distribution */}
+      {scatterData.length > 0 && (
+        <div className="card">
+          <h2 className="font-semibold mb-3">Confidence Distribution</h2>
+          <ResponsiveContainer width="100%" height={160}>
+            <BarChart data={confData} barSize={40}>
+              <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+              <XAxis dataKey="range" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
+              <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} allowDecimals={false} />
+              <Tooltip formatter={(v) => [v, 'Predictions']} contentStyle={{ background: '#111827', border: '1px solid #374151', borderRadius: 8, fontSize: 12 }} />
+              <Bar dataKey="count" fill="#8b5cf6" fillOpacity={0.7} radius={[4, 4, 0, 0]} />
+            </BarChart>
+          </ResponsiveContainer>
+        </div>
+      )}
+
+      {/* Model Versions Table */}
+      <div className="card"><h2 className="font-semibold mb-3">Model Versions</h2>
+        <table><thead><tr><th>ID</th><th>Type</th><th>Tag</th><th>Status</th><th>Created</th></tr></thead><tbody>
+          {models.map(m => <tr key={m.id}><td className="font-mono">#{m.id}</td><td>{m.model_type}</td><td>{m.version_tag}</td>
+            <td><span className={`badge ${m.active ? 'bg-green' : 'bg-yellow'}`}>{m.active ? 'Active' : 'Off'}</span></td>
+            <td className="text-gray-400 text-sm">{new Date(m.created_at).toLocaleDateString()}</td></tr>)}
+        </tbody></table>
+      </div>
+
+      {/* Predictions Table */}
+      <div className="card"><h2 className="font-semibold mb-3">Predictions</h2>
+        <table><thead><tr><th>Market</th><th>P(Yes)</th><th>Confidence</th><th>Edge</th><th>Signal</th><th>Time</th></tr></thead><tbody>
+          {preds.length === 0 ? <tr><td colSpan={6} className="text-center text-gray-500 py-8">No predictions yet</td></tr>
+            : preds.map((p, i) => <tr key={i}>
+              <td className="font-mono text-xs max-w-[140px] truncate">{p.question || p.condition_id?.substring(0, 20)}</td>
+              <td className="font-mono text-green-400">{(p.p_yes * 100).toFixed(1)}%</td>
+              <td className="font-mono">{p.confidence ? (p.confidence * 100).toFixed(0) + '%' : '-'}</td>
+              <td className={`font-mono ${(p.edge || 0) > 0 ? 'text-green-400' : 'text-red-400'}`}>{p.edge ? (p.edge * 100).toFixed(2) + '%' : '-'}</td>
+              <td><span className={`badge ${p.recommendation?.startsWith('BUY') ? 'bg-green' : p.recommendation === 'SKIP' ? 'bg-red' : 'bg-yellow'}`}>{p.recommendation || '-'}</span></td>
+              <td className="text-gray-400 text-sm">{new Date(p.asof_ts).toLocaleString()}</td>
+            </tr>)}
+        </tbody></table>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/pages/Polymarket.jsx b/react-dash/src/pages/Polymarket.jsx
new file mode 100644
index 0000000..478aaa7
--- /dev/null
+++ b/react-dash/src/pages/Polymarket.jsx
@@ -0,0 +1,325 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts';
+
+export default function Polymarket() {
+  const [status, setStatus] = useState(null);
+  const [clobOk, setClobOk] = useState(null);
+  const [markets, setMarkets] = useState([]);
+  const [prices, setPrices] = useState([]);
+  const [selectedToken, setSelectedToken] = useState(null);
+  const [orderbook, setOrderbook] = useState(null);
+  const [loading, setLoading] = useState(true);
+  const [liveLoading, setLiveLoading] = useState(false);
+  const [obLoading, setObLoading] = useState(false);
+
+  useEffect(() => {
+    loadAll();
+  }, []);
+
+  async function loadAll() {
+    setLoading(true);
+    try {
+      const [s, m] = await Promise.all([
+        post('/polymarket', { action: 'status' }),
+        api('/markets'),
+      ]);
+      setStatus(s);
+      setMarkets(m.markets || []);
+    } catch {}
+    setLoading(false);
+  }
+
+  async function testClob() {
+    setClobOk('testing');
+    try {
+      const r = await post('/polymarket', { action: 'test_public' });
+      setClobOk(r.success ? r : { error: true });
+    } catch {
+      setClobOk({ error: true });
+    }
+  }
+
+  async function fetchLivePrices() {
+    setLiveLoading(true);
+    try {
+      const r = await post('/polymarket', { action: 'get_markets' });
+      if (r.success) setPrices(r.tracked_prices || []);
+    } catch {}
+    setLiveLoading(false);
+  }
+
+  async function fetchOrderbook(tokenId) {
+    setSelectedToken(tokenId);
+    setObLoading(true);
+    setOrderbook(null);
+    try {
+      const r = await post('/polymarket', { action: 'get_orderbook', token_id: tokenId });
+      if (r.success) setOrderbook(r.orderbook);
+    } catch {}
+    setObLoading(false);
+  }
+
+  const dot = (v) => v ? 'bg-green-500' : 'bg-gray-600';
+
+  // Build orderbook chart data
+  const obChartData = [];
+  if (orderbook) {
+    const bids = (orderbook.bids || []).slice(0, 10).map(b => ({
+      price: parseFloat(b.price),
+      size: parseFloat(b.size),
+      side: 'bid',
+    }));
+    const asks = (orderbook.asks || []).slice(0, 10).map(a => ({
+      price: parseFloat(a.price),
+      size: parseFloat(a.size),
+      side: 'ask',
+    }));
+    bids.reverse().forEach(b => obChartData.push(b));
+    asks.forEach(a => obChartData.push(a));
+  }
+
+  // Price chart for tracked tokens
+  const priceChartData = prices.map(p => ({
+    name: (p.question || '').substring(0, 30) + '...',
+    price: parseFloat(p.price || 0),
+    outcome: p.outcome,
+  }));
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-purple-500 border-t-transparent rounded-full" /></div>;
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-2xl font-bold">Polymarket</h1>
+          <p className="text-gray-500 text-sm">Live CLOB data, orderbooks & market prices</p>
+        </div>
+        <div className="flex gap-3">
+          <button onClick={testClob} className={`btn btn-s text-sm ${clobOk?.ok ? 'text-green-400 border-green-500/30' : ''}`}>
+            {clobOk === 'testing' ? 'Testing...' : clobOk?.ok ? 'CLOB OK' : clobOk?.error ? 'Error' : 'Test CLOB'}
+          </button>
+          <button onClick={fetchLivePrices} disabled={liveLoading} className="btn btn-p text-sm">
+            {liveLoading ? 'Fetching...' : 'Fetch Live Prices'}
+          </button>
+        </div>
+      </div>
+
+      {/* Connection Status Bar */}
+      <div className="card">
+        <div className="flex items-center gap-6">
+          <div className="flex items-center gap-2">
+            <span className={`w-2 h-2 rounded-full ${dot(status?.connected)}`} />
+            <span className="text-sm">{status?.connected ? 'API Connected' : 'Not Connected'}</span>
+          </div>
+          {status?.api_key && <span className="text-xs text-gray-500 font-mono">Key: {status.api_key}</span>}
+          {status?.wallet && <span className="text-xs text-gray-500 font-mono">Wallet: {status.wallet}</span>}
+          {clobOk?.server_time && <span className="text-xs text-gray-500">Server: {new Date(parseInt(clobOk.server_time) * 1000).toLocaleTimeString()}</span>}
+          <span className="text-xs text-gray-500 ml-auto">{markets.length} tracked markets</span>
+        </div>
+      </div>
+
+      {/* Stats Cards */}
+      <div className="grid grid-cols-4 gap-4">
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Tracked Markets</p>
+          <p className="stat-v text-purple-400">{markets.length}</p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Active</p>
+          <p className="stat-v text-green-400">{markets.filter(m => m.active).length}</p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Live Prices</p>
+          <p className="stat-v text-blue-400">{prices.length}</p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">API Status</p>
+          <p className={`stat-v ${status?.connected ? 'text-green-400' : 'text-gray-500'}`}>{status?.connected ? 'LIVE' : 'OFF'}</p>
+        </div>
+      </div>
+
+      {/* Live Prices Chart */}
+      {priceChartData.length > 0 && (
+        <div className="card">
+          <h2 className="font-semibold mb-3">Live Token Prices</h2>
+          <ResponsiveContainer width="100%" height={250}>
+            <BarChart data={priceChartData} layout="vertical">
+              <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+              <XAxis type="number" domain={[0, 1]} tickFormatter={v => `${(v * 100).toFixed(0)}%`} stroke="#6b7280" />
+              <YAxis type="category" dataKey="name" width={200} tick={{ fontSize: 11, fill: '#9ca3af' }} />
+              <Tooltip formatter={v => `${(v * 100).toFixed(1)}%`} contentStyle={{ background: '#1f2937', border: '1px solid #374151', borderRadius: 8 }} />
+              <Bar dataKey="price" radius={[0, 4, 4, 0]}>
+                {priceChartData.map((d, i) => (
+                  <Cell key={i} fill={d.outcome === 'YES' ? '#22c55e' : '#ef4444'} fillOpacity={0.7} />
+                ))}
+              </Bar>
+            </BarChart>
+          </ResponsiveContainer>
+        </div>
+      )}
+
+      {/* Markets Table with Live Prices */}
+      <div className="card overflow-x-auto">
+        <div className="flex items-center justify-between mb-3">
+          <h2 className="font-semibold">Weather Markets</h2>
+          <span className="text-xs text-gray-500">{markets.length} markets</span>
+        </div>
+        <table>
+          <thead>
+            <tr><th>Question</th><th>Category</th><th>Liquidity</th><th>End Date</th><th>Live Price</th><th>Orderbook</th></tr>
+          </thead>
+          <tbody>
+            {markets.length === 0 ? (
+              <tr><td colSpan={6} className="text-center text-gray-500 py-8">No markets tracked. Go to Markets page to ingest.</td></tr>
+            ) : markets.map(m => {
+              const livePrice = prices.find(p => p.condition_id === m.condition_id);
+              return (
+                <tr key={m.condition_id}>
+                  <td className="max-w-[300px]">
+                    <p className="truncate font-medium">{m.question}</p>
+                    <p className="text-xs text-gray-600 truncate font-mono">{m.condition_id?.substring(0, 12)}...</p>
+                  </td>
+                  <td><span className="badge bg-purple">{m.category || 'weather'}</span></td>
+                  <td className="font-mono text-sm">${parseFloat(m.liquidity || 0).toFixed(0)}</td>
+                  <td className="text-gray-400 text-sm">{m.end_ts ? new Date(m.end_ts).toLocaleDateString() : '-'}</td>
+                  <td>
+                    {livePrice ? (
+                      <span className="font-mono text-green-400 font-medium">{(parseFloat(livePrice.price) * 100).toFixed(1)}%</span>
+                    ) : (
+                      <span className="text-gray-600 text-xs">—</span>
+                    )}
+                  </td>
+                  <td>
+                    {m.tokens?.length > 0 || livePrice?.token_id ? (
+                      <button
+                        onClick={() => fetchOrderbook(livePrice?.token_id || m.tokens?.[0]?.token_id)}
+                        className="text-xs text-purple-400 hover:text-purple-300"
+                      >
+                        View
+                      </button>
+                    ) : (
+                      <span className="text-gray-600 text-xs">—</span>
+                    )}
+                  </td>
+                </tr>
+              );
+            })}
+          </tbody>
+        </table>
+      </div>
+
+      {/* Orderbook View */}
+      {(orderbook || obLoading) && (
+        <div className="card">
+          <div className="flex items-center justify-between mb-4">
+            <h2 className="font-semibold">Orderbook</h2>
+            <div className="flex items-center gap-3">
+              <span className="text-xs text-gray-500 font-mono">{selectedToken?.substring(0, 12)}...</span>
+              <button onClick={() => { setOrderbook(null); setSelectedToken(null); }} className="text-xs text-gray-500 hover:text-gray-300">Close</button>
+            </div>
+          </div>
+
+          {obLoading ? (
+            <div className="flex items-center justify-center h-32"><div className="animate-spin w-6 h-6 border-2 border-purple-500 border-t-transparent rounded-full" /></div>
+          ) : orderbook ? (
+            <div className="grid grid-cols-2 gap-6">
+              {/* Bids */}
+              <div>
+                <h3 className="text-sm font-medium text-green-400 mb-2">Bids (Buy Orders)</h3>
+                <div className="space-y-1">
+                  <div className="flex justify-between text-[10px] text-gray-500 uppercase px-2">
+                    <span>Price</span><span>Size</span>
+                  </div>
+                  {(orderbook.bids || []).slice(0, 12).map((b, i) => (
+                    <div key={i} className="flex justify-between text-sm px-2 py-1 rounded relative overflow-hidden">
+                      <div className="absolute inset-0 bg-green-500/10" style={{ width: `${Math.min(parseFloat(b.size) * 10, 100)}%` }} />
+                      <span className="font-mono text-green-400 relative z-10">{parseFloat(b.price).toFixed(3)}</span>
+                      <span className="font-mono text-gray-400 relative z-10">{parseFloat(b.size).toFixed(1)}</span>
+                    </div>
+                  ))}
+                  {(!orderbook.bids || orderbook.bids.length === 0) && <p className="text-gray-600 text-xs text-center py-4">No bids</p>}
+                </div>
+              </div>
+              {/* Asks */}
+              <div>
+                <h3 className="text-sm font-medium text-red-400 mb-2">Asks (Sell Orders)</h3>
+                <div className="space-y-1">
+                  <div className="flex justify-between text-[10px] text-gray-500 uppercase px-2">
+                    <span>Price</span><span>Size</span>
+                  </div>
+                  {(orderbook.asks || []).slice(0, 12).map((a, i) => (
+                    <div key={i} className="flex justify-between text-sm px-2 py-1 rounded relative overflow-hidden">
+                      <div className="absolute inset-0 bg-red-500/10 right-0" style={{ width: `${Math.min(parseFloat(a.size) * 10, 100)}%` }} />
+                      <span className="font-mono text-red-400 relative z-10">{parseFloat(a.price).toFixed(3)}</span>
+                      <span className="font-mono text-gray-400 relative z-10">{parseFloat(a.size).toFixed(1)}</span>
+                    </div>
+                  ))}
+                  {(!orderbook.asks || orderbook.asks.length === 0) && <p className="text-gray-600 text-xs text-center py-4">No asks</p>}
+                </div>
+              </div>
+            </div>
+          ) : null}
+
+          {/* Orderbook Depth Chart */}
+          {obChartData.length > 0 && (
+            <div className="mt-4 pt-4 border-t border-gray-800">
+              <h3 className="text-sm font-medium text-gray-400 mb-2">Depth Chart</h3>
+              <ResponsiveContainer width="100%" height={200}>
+                <BarChart data={obChartData}>
+                  <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+                  <XAxis dataKey="price" tickFormatter={v => v.toFixed(2)} stroke="#6b7280" />
+                  <YAxis stroke="#6b7280" />
+                  <Tooltip formatter={v => v.toFixed(2)} contentStyle={{ background: '#1f2937', border: '1px solid #374151', borderRadius: 8 }} />
+                  <Bar dataKey="size" radius={[4, 4, 0, 0]}>
+                    {obChartData.map((d, i) => (
+                      <Cell key={i} fill={d.side === 'bid' ? '#22c55e' : '#ef4444'} fillOpacity={0.6} />
+                    ))}
+                  </Bar>
+                </BarChart>
+              </ResponsiveContainer>
+            </div>
+          )}
+        </div>
+      )}
+
+      {/* Quick Reference */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">CLOB Quick Reference</h2>
+        <div className="grid grid-cols-3 gap-4 text-sm">
+          <div>
+            <p className="text-gray-500 text-xs uppercase mb-2">Public Endpoints</p>
+            <ul className="space-y-1 text-gray-400 text-xs font-mono">
+              <li>/ok — Health check</li>
+              <li>/time — Server time</li>
+              <li>/book?token_id=X — Orderbook</li>
+              <li>/price?token_id=X — Best price</li>
+              <li>/midpoint?token_id=X — Mid price</li>
+              <li>/spread?token_id=X — Bid-ask spread</li>
+            </ul>
+          </div>
+          <div>
+            <p className="text-gray-500 text-xs uppercase mb-2">Auth Endpoints</p>
+            <ul className="space-y-1 text-gray-400 text-xs font-mono">
+              <li>POST /order — Place order</li>
+              <li>DELETE /order — Cancel order</li>
+              <li>POST /orders — Batch orders</li>
+              <li>GET /data/trades — User trades</li>
+              <li>GET /data/orders — User orders</li>
+            </ul>
+          </div>
+          <div>
+            <p className="text-gray-500 text-xs uppercase mb-2">Trading Notes</p>
+            <ul className="space-y-1 text-gray-400 text-xs">
+              <li>Chain: Polygon (137)</li>
+              <li>Collateral: USDC (6 decimals)</li>
+              <li>Conditional: 6 decimals</li>
+              <li>Min order: varies per market</li>
+              <li>Paper mode: no real funds needed</li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/pages/Risk.jsx b/react-dash/src/pages/Risk.jsx
new file mode 100644
index 0000000..1b4cb2b
--- /dev/null
+++ b/react-dash/src/pages/Risk.jsx
@@ -0,0 +1,255 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, RadialBarChart, RadialBar, Legend } from 'recharts';
+
+// Risk presets calculated for $50 bankroll, 30-day experiment
+const PRESETS = {
+  conservative: {
+    label: 'Conservative',
+    color: 'blue',
+    desc: 'Preserve capital. Small bets, strict limits.',
+    icon: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
+    config: {
+      paper_mode: true,
+      max_bet_usd: 0.25,
+      max_daily_loss_usd: 2.00,
+      max_open_exposure_usd: 3.00,
+      max_drawdown_pct: 10,
+      min_edge_threshold: 0.05,
+      min_liquidity: 200,
+      fractional_kelly: 0.05,
+    },
+    kelly_note: '5% Kelly -> ~$0.12 avg bet on 10% edge',
+    survival: '99% chance to survive 30 days',
+  },
+  moderate: {
+    label: 'Moderate',
+    color: 'yellow',
+    desc: 'Balanced risk/reward. Default for $50 experiment.',
+    icon: 'M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3',
+    config: {
+      paper_mode: true,
+      max_bet_usd: 0.50,
+      max_daily_loss_usd: 5.00,
+      max_open_exposure_usd: 5.00,
+      max_drawdown_pct: 20,
+      min_edge_threshold: 0.03,
+      min_liquidity: 100,
+      fractional_kelly: 0.10,
+    },
+    kelly_note: '10% Kelly -> ~$0.25 avg bet on 5% edge',
+    survival: '90% chance to survive 30 days',
+  },
+  aggressive: {
+    label: 'Aggressive',
+    color: 'red',
+    desc: 'Max growth. Higher risk of ruin.',
+    icon: 'M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z',
+    config: {
+      paper_mode: true,
+      max_bet_usd: 1.00,
+      max_daily_loss_usd: 8.00,
+      max_open_exposure_usd: 10.00,
+      max_drawdown_pct: 35,
+      min_edge_threshold: 0.02,
+      min_liquidity: 50,
+      fractional_kelly: 0.20,
+    },
+    kelly_note: '20% Kelly -> ~$0.50 avg bet on 5% edge',
+    survival: '70% chance to survive 30 days',
+  },
+};
+
+export default function Risk() {
+  const [r, setR] = useState(null);
+  const [loading, setL] = useState(true);
+  const [applying, setApplying] = useState(null);
+
+  const load = () => api('/risk').then(d => { setR(d.state); setL(false); });
+  useEffect(() => { load(); const i = setInterval(load, 10000); return () => clearInterval(i); }, []);
+
+  async function toggleKill() {
+    const active = r?.kill_switch_active;
+    if (!active) {
+      const reason = prompt('Kill switch reason:');
+      if (!reason) return;
+      await post('/risk', { action: 'kill_switch_activate', reason });
+    } else {
+      await post('/risk', { action: 'kill_switch_deactivate' });
+    }
+    load();
+  }
+
+  async function updateCfg(key, val) {
+    const cfg = { ...r.config, [key]: val };
+    await post('/risk', { action: 'update_config', config: cfg });
+    load();
+  }
+
+  async function applyPreset(key) {
+    setApplying(key);
+    const preset = PRESETS[key];
+    await post('/risk', { action: 'update_config', config: preset.config });
+    load();
+    setTimeout(() => setApplying(null), 600);
+  }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  const c = r?.config || {};
+  const bankroll = parseFloat(r?.bankroll || 50);
+  const exposure = parseFloat(r?.open_exposure || 0);
+  const dailyPnl = parseFloat(r?.daily_pnl || 0);
+  const drawdown = parseFloat(r?.max_drawdown_pct || 0);
+
+  // Gauge data for risk metrics
+  const gaugeData = [
+    { name: 'Exposure', value: Math.min(100, (exposure / (c.max_open_exposure_usd || 5)) * 100), fill: '#3b82f6' },
+    { name: 'Daily Loss', value: Math.min(100, (Math.abs(dailyPnl) / (c.max_daily_loss_usd || 5)) * 100), fill: dailyPnl < 0 ? '#ef4444' : '#10b981' },
+    { name: 'Drawdown', value: Math.min(100, (drawdown / (c.max_drawdown_pct || 20)) * 100), fill: '#8b5cf6' },
+  ];
+
+  // Detect active preset
+  const activePreset = Object.entries(PRESETS).find(([_, p]) =>
+    p.config.fractional_kelly === c.fractional_kelly &&
+    p.config.max_bet_usd === c.max_bet_usd
+  )?.[0];
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div><h1 className="text-2xl font-bold">Risk Management</h1><p className="text-gray-500 text-sm">Controls, presets & kill switches</p></div>
+        <button onClick={toggleKill} className={`btn ${r?.kill_switch_active ? 'btn-s' : 'btn-d'}`}>{r?.kill_switch_active ? 'Deactivate Kill Switch' : 'Activate Kill Switch'}</button>
+      </div>
+
+      {r?.kill_switch_active && (
+        <div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4 flex items-center gap-3">
+          <span className="text-2xl">&#128680;</span>
+          <div><p className="font-semibold text-red-400">KILL SWITCH ACTIVE</p><p className="text-sm text-gray-400">{r.kill_reason}</p></div>
+        </div>
+      )}
+
+      {/* Stats */}
+      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Bankroll</p><p className="stat-v text-green-400">${bankroll.toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Daily P&L</p><p className={`stat-v ${dailyPnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>${dailyPnl.toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Exposure</p><p className="stat-v text-blue-400">${exposure.toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Drawdown</p><p className="stat-v text-purple-400">{drawdown.toFixed(1)}%</p></div>
+      </div>
+
+      {/* Risk Gauge Chart */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Risk Utilization</h2>
+        <ResponsiveContainer width="100%" height={160}>
+          <BarChart data={gaugeData} layout="vertical" barSize={20}>
+            <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" horizontal={false} />
+            <XAxis type="number" domain={[0, 100]} tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `${v}%`} />
+            <YAxis type="category" dataKey="name" tick={{ fill: '#9ca3af', fontSize: 12 }} axisLine={false} tickLine={false} width={80} />
+            <Tooltip formatter={(v) => [`${v.toFixed(0)}%`, 'Utilization']} contentStyle={{ background: '#111827', border: '1px solid #374151', borderRadius: 8, fontSize: 12 }} />
+            <Bar dataKey="value" radius={[0, 4, 4, 0]}>
+              {gaugeData.map((entry, i) => (
+                <React.Fragment key={i}>
+                  {React.createElement('rect', null)}
+                </React.Fragment>
+              ))}
+            </Bar>
+          </BarChart>
+        </ResponsiveContainer>
+      </div>
+
+      {/* Risk Presets */}
+      <div className="card" style={{ background: 'linear-gradient(135deg, rgba(59,130,246,0.03), rgba(139,92,246,0.03))' }}>
+        <div className="mb-4">
+          <h2 className="font-semibold">Risk Presets</h2>
+          <p className="text-xs text-gray-500 mt-0.5">One-click profiles optimized for $50 bankroll / 30-day experiment</p>
+        </div>
+        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+          {Object.entries(PRESETS).map(([key, preset]) => {
+            const isActive = activePreset === key;
+            return (
+              <button
+                key={key}
+                onClick={() => applyPreset(key)}
+                disabled={applying === key}
+                className={`text-left rounded-xl border p-4 transition-all ${isActive
+                  ? `border-${preset.color}-500/50 bg-${preset.color}-500/10`
+                  : 'border-gray-800 hover:border-gray-700 bg-gray-800/20 hover:bg-gray-800/40'
+                  } ${applying === key ? 'opacity-50' : ''}`}
+              >
+                <div className="flex items-center gap-3 mb-3">
+                  <div className={`w-10 h-10 rounded-lg flex items-center justify-center bg-${preset.color}-500/15`}>
+                    <svg className={`w-5 h-5 text-${preset.color}-400`} fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d={preset.icon} /></svg>
+                  </div>
+                  <div>
+                    <div className="flex items-center gap-2">
+                      <p className="font-semibold">{preset.label}</p>
+                      {isActive && <span className={`badge bg-${preset.color}`}>Active</span>}
+                    </div>
+                    <p className="text-xs text-gray-500">{preset.desc}</p>
+                  </div>
+                </div>
+                <div className="space-y-1.5 text-xs">
+                  <div className="flex justify-between"><span className="text-gray-500">Max Bet</span><span className="font-mono">${preset.config.max_bet_usd}</span></div>
+                  <div className="flex justify-between"><span className="text-gray-500">Kelly Fraction</span><span className="font-mono">{preset.config.fractional_kelly}x</span></div>
+                  <div className="flex justify-between"><span className="text-gray-500">Max Exposure</span><span className="font-mono">${preset.config.max_open_exposure_usd}</span></div>
+                  <div className="flex justify-between"><span className="text-gray-500">Min Edge</span><span className="font-mono">{(preset.config.min_edge_threshold * 100)}%</span></div>
+                  <div className="flex justify-between"><span className="text-gray-500">Max Drawdown</span><span className="font-mono">{preset.config.max_drawdown_pct}%</span></div>
+                </div>
+                <div className="mt-3 pt-3 border-t border-gray-800">
+                  <p className="text-[10px] text-gray-500">{preset.kelly_note}</p>
+                  <p className="text-[10px] text-gray-400 mt-0.5">{preset.survival}</p>
+                </div>
+              </button>
+            );
+          })}
+        </div>
+      </div>
+
+      {/* Manual Config */}
+      <div className="card">
+        <h2 className="font-semibold mb-4">Risk Configuration</h2>
+        <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
+          {[
+            { k: 'paper_mode', l: 'Paper Mode', t: 'toggle', desc: 'No real orders' },
+            { k: 'max_bet_usd', l: 'Max Bet ($)', t: 'num', s: 0.05 },
+            { k: 'max_daily_loss_usd', l: 'Max Daily Loss ($)', t: 'num', s: 0.5 },
+            { k: 'max_open_exposure_usd', l: 'Max Exposure ($)', t: 'num', s: 0.5 },
+            { k: 'max_drawdown_pct', l: 'Max Drawdown (%)', t: 'num', s: 1 },
+            { k: 'min_edge_threshold', l: 'Min Edge', t: 'num', s: 0.005 },
+            { k: 'min_liquidity', l: 'Min Liquidity ($)', t: 'num', s: 10 },
+            { k: 'fractional_kelly', l: 'Kelly Fraction', t: 'num', s: 0.05 },
+          ].map(item => (
+            <div key={item.k} className="flex items-center justify-between bg-gray-800/30 rounded-lg p-3">
+              <div><p className="text-sm font-medium">{item.l}</p>{item.desc && <p className="text-xs text-gray-500">{item.desc}</p>}</div>
+              {item.t === 'toggle' ?
+                <button onClick={() => updateCfg(item.k, !c[item.k])} className={`w-12 h-6 rounded-full relative ${c[item.k] ? 'bg-green-500' : 'bg-gray-600'}`}>
+                  <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${c[item.k] ? 'left-6' : 'left-0.5'}`} />
+                </button>
+                : <input type="number" step={item.s} value={c[item.k] ?? ''} onChange={e => updateCfg(item.k, parseFloat(e.target.value))} className="w-24 bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-right font-mono" />}
+            </div>
+          ))}
+        </div>
+      </div>
+
+      {/* Kelly Calculator */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Kelly Bet Calculator</h2>
+        <p className="text-xs text-gray-500 mb-4">Suggested bet sizes for current config with $50 bankroll</p>
+        <div className="grid grid-cols-3 md:grid-cols-5 gap-3">
+          {[0.02, 0.03, 0.05, 0.08, 0.10].map(edge => {
+            const kelly = c.fractional_kelly || 0.10;
+            const fullKelly = edge / (1 - edge); // simplified for binary bet
+            const betSize = Math.min(c.max_bet_usd || 0.50, bankroll * kelly * fullKelly);
+            return (
+              <div key={edge} className="bg-gray-800/30 rounded-lg p-3 text-center">
+                <p className="text-xs text-gray-500 mb-1">{(edge * 100).toFixed(0)}% edge</p>
+                <p className={`font-mono font-semibold ${betSize > 0 ? 'text-green-400' : 'text-gray-500'}`}>${betSize.toFixed(2)}</p>
+                <p className="text-[10px] text-gray-600 mt-0.5">of ${bankroll.toFixed(0)}</p>
+              </div>
+            );
+          })}
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/pages/Settings.jsx b/react-dash/src/pages/Settings.jsx
new file mode 100644
index 0000000..f92e738
--- /dev/null
+++ b/react-dash/src/pages/Settings.jsx
@@ -0,0 +1,331 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+
+export default function Settings() {
+  const [h, setH] = useState(null);
+  // Polymarket credentials (manual entry from polymarket.com/settings?tab=builder)
+  const [polyKey, setPolyKey] = useState('');
+  const [polySecret, setPolySecret] = useState('');
+  const [polyPass, setPolyPass] = useState('');
+  const [polyWallet, setPolyWallet] = useState('');
+  const [showSecret, setShowSecret] = useState(false);
+  // Connection state
+  const [polyStatus, setPolyStatus] = useState(null);
+  const [polyInfo, setPolyInfo] = useState(null);
+  const [polyMsg, setPolyMsg] = useState('');
+  const [clobOk, setClobOk] = useState(null);
+
+  const [testResults, setTestResults] = useState({});
+
+  useEffect(() => {
+    api('/health').then(setH);
+    checkPolyStatus();
+  }, []);
+
+  async function checkPolyStatus() {
+    setPolyStatus('checking');
+    try {
+      const r = await post('/polymarket', { action: 'status' });
+      setPolyInfo(r);
+      setPolyStatus(r.connected ? 'connected' : 'disconnected');
+    } catch {
+      setPolyStatus('disconnected');
+    }
+  }
+
+  async function testPublicClob() {
+    setClobOk('testing');
+    try {
+      const r = await post('/polymarket', { action: 'test_public' });
+      setClobOk(r.success ? 'ok' : 'error');
+      if (r.success) setPolyMsg(`CLOB OK — Server time: ${r.server_time}`);
+    } catch {
+      setClobOk('error');
+    }
+  }
+
+  async function saveKeys() {
+    if (!polyKey || !polySecret || !polyPass) return;
+    setPolyStatus('saving');
+    setPolyMsg('');
+    try {
+      const r = await post('/polymarket', {
+        action: 'save_keys',
+        api_key: polyKey,
+        secret: polySecret,
+        passphrase: polyPass,
+        wallet: polyWallet || null,
+      });
+      if (r.success) {
+        setPolyStatus('connected');
+        setPolyInfo({ api_key: r.api_key, wallet: polyWallet, connected_at: new Date().toISOString(), connected: true });
+        setPolyMsg('Credentials saved! Use "Test Auth" to verify.');
+        setPolyKey(''); setPolySecret(''); setPolyPass('');
+      } else {
+        setPolyStatus('error');
+        setPolyMsg(r.error || 'Failed to save');
+      }
+    } catch (err) {
+      setPolyStatus('error');
+      setPolyMsg('Error: ' + err.message);
+    }
+  }
+
+  async function testAuth() {
+    setPolyMsg('Testing authenticated connection...');
+    try {
+      const r = await post('/polymarket', { action: 'auth_test' });
+      if (r.success) {
+        setPolyMsg(`${r.message} | ${r.trades ?? 0} trades found`);
+        setPolyStatus('connected');
+      } else {
+        setPolyMsg(r.error || 'Auth test failed');
+      }
+    } catch (err) {
+      setPolyMsg('Error: ' + err.message);
+    }
+  }
+
+  async function disconnectPoly() {
+    try {
+      await post('/polymarket', { action: 'disconnect' });
+      setPolyStatus('disconnected');
+      setPolyInfo(null);
+      setPolyMsg('Disconnected');
+    } catch {
+      setPolyMsg('Error disconnecting');
+    }
+  }
+
+  async function testService(name, fn) {
+    setTestResults(prev => ({ ...prev, [name]: 'testing' }));
+    try {
+      const r = await fn();
+      setTestResults(prev => ({ ...prev, [name]: r.success || r.sent ? 'ok' : 'error' }));
+    } catch {
+      setTestResults(prev => ({ ...prev, [name]: 'error' }));
+    }
+  }
+
+  async function testNWS() { await testService('nws', () => post('/weather', { action: 'test_nws' })); }
+  async function testSlack() { await testService('slack', () => post('/alerts', { action: 'test' })); }
+
+  async function runW(w) {
+    const r = await post('/workers', { action: 'run', worker: w });
+    setTestResults(prev => ({ ...prev, [w]: r.message ? 'ok' : 'error' }));
+  }
+
+  const dot = (s) => {
+    if (s === true || s === 'ok' || s === 'connected') return 'bg-green-500';
+    if (s === 'testing' || s === 'checking' || s === 'saving') return 'bg-yellow-500 animate-pulse';
+    if (s === 'error') return 'bg-red-500';
+    return 'bg-gray-600';
+  };
+
+  return (
+    <div className="space-y-6">
+      <div><h1 className="text-2xl font-bold">Settings</h1><p className="text-gray-500 text-sm">System config & Polymarket login</p></div>
+
+      {/* ── Polymarket CLOB Login ── */}
+      <div className="card border border-purple-500/20" style={{ background: 'linear-gradient(135deg, rgba(139,92,246,0.08), rgba(59,130,246,0.04))' }}>
+        <div className="flex items-center gap-3 mb-5">
+          <div className="w-12 h-12 rounded-xl flex items-center justify-center bg-purple-500/20">
+            <svg className="w-6 h-6 text-purple-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><path strokeLinecap="round" strokeLinejoin="round" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9" /></svg>
+          </div>
+          <div className="flex-1">
+            <h2 className="text-lg font-bold">Polymarket CLOB Login</h2>
+            <p className="text-xs text-gray-500">Connect with your Builder API keys for live trading on Polygon</p>
+          </div>
+          <div className="flex items-center gap-2">
+            <span className={`w-2.5 h-2.5 rounded-full ${dot(polyStatus)}`} />
+            <span className={`text-sm font-medium ${polyStatus === 'connected' ? 'text-green-400' : polyStatus === 'error' ? 'text-red-400' : 'text-gray-500'}`}>
+              {polyStatus === 'connected' ? 'Connected' : polyStatus === 'saving' ? 'Saving...' : polyStatus === 'checking' ? 'Checking...' : polyStatus === 'error' ? 'Error' : 'Not Connected'}
+            </span>
+          </div>
+        </div>
+
+        {/* Connected State */}
+        {polyStatus === 'connected' && polyInfo && (
+          <div className="bg-green-500/10 border border-green-500/20 rounded-lg p-4 mb-4">
+            <div className="grid grid-cols-3 gap-4 text-sm">
+              <div>
+                <span className="text-gray-500 text-xs block mb-0.5">API Key</span>
+                <span className="font-mono text-green-400">{polyInfo.api_key || '—'}</span>
+              </div>
+              <div>
+                <span className="text-gray-500 text-xs block mb-0.5">Wallet</span>
+                <span className="font-mono text-green-400">{polyInfo.wallet ? `${polyInfo.wallet.substring(0, 6)}...${polyInfo.wallet.slice(-4)}` : 'Not set'}</span>
+              </div>
+              <div>
+                <span className="text-gray-500 text-xs block mb-0.5">Connected</span>
+                <span className="text-green-400">{polyInfo.connected_at ? new Date(polyInfo.connected_at).toLocaleDateString() : '—'}</span>
+              </div>
+            </div>
+            <div className="flex gap-3 mt-3">
+              <button onClick={testAuth} className="btn btn-s text-xs">Test Auth</button>
+              <button onClick={testPublicClob} className={`btn btn-s text-xs ${clobOk === 'ok' ? 'text-green-400 border-green-500/30' : ''}`}>
+                {clobOk === 'testing' ? '...' : clobOk === 'ok' ? 'CLOB OK' : 'Test CLOB'}
+              </button>
+              <button onClick={disconnectPoly} className="btn text-xs text-red-400 hover:text-red-300 border border-red-500/30 hover:border-red-500/50">Disconnect</button>
+            </div>
+          </div>
+        )}
+
+        {/* Not Connected — Credential Entry */}
+        {polyStatus !== 'connected' && (
+          <div className="space-y-3">
+            <div className="bg-gray-800/40 rounded-lg p-4">
+              <p className="text-xs text-gray-400 mb-3">
+                Get your Builder API keys from{' '}
+                <span className="text-purple-400 font-mono">polymarket.com/settings</span>
+                {' '}→ Builder tab → Create New
+              </p>
+              <div className="space-y-2.5">
+                <div>
+                  <label className="text-[11px] text-gray-500 mb-1 block">API Key</label>
+                  <input type="text" value={polyKey} onChange={e => setPolyKey(e.target.value)} placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-purple-500 transition" />
+                </div>
+                <div>
+                  <label className="text-[11px] text-gray-500 mb-1 block">API Secret</label>
+                  <div className="relative">
+                    <input type={showSecret ? 'text' : 'password'} value={polySecret} onChange={e => setPolySecret(e.target.value)} placeholder="Base64 encoded secret" className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 pr-10 text-sm font-mono focus:outline-none focus:border-purple-500 transition" />
+                    <button type="button" onClick={() => setShowSecret(!showSecret)} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300">
+                      <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={showSecret ? 'M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.879L21 21' : 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z'} /></svg>
+                    </button>
+                  </div>
+                </div>
+                <div>
+                  <label className="text-[11px] text-gray-500 mb-1 block">Passphrase</label>
+                  <input type="password" value={polyPass} onChange={e => setPolyPass(e.target.value)} placeholder="Passphrase" className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-purple-500 transition" />
+                </div>
+                <div>
+                  <label className="text-[11px] text-gray-500 mb-1 block">Wallet Address <span className="text-gray-600">(optional)</span></label>
+                  <input type="text" value={polyWallet} onChange={e => setPolyWallet(e.target.value)} placeholder="0x..." className="w-full bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-sm font-mono focus:outline-none focus:border-purple-500 transition" />
+                </div>
+              </div>
+            </div>
+
+            <div className="flex gap-3">
+              <button onClick={saveKeys} disabled={!polyKey || !polySecret || !polyPass || polyStatus === 'saving'} className="btn btn-p flex items-center gap-2 disabled:opacity-50">
+                {polyStatus === 'saving' && <span className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full" />}
+                {polyStatus === 'saving' ? 'Saving...' : 'Connect'}
+              </button>
+              <button onClick={testPublicClob} className={`btn btn-s ${clobOk === 'ok' ? 'text-green-400 border-green-500/30' : ''}`}>
+                {clobOk === 'testing' ? '...' : clobOk === 'ok' ? 'CLOB Reachable' : 'Test CLOB'}
+              </button>
+            </div>
+          </div>
+        )}
+
+        {/* Status message */}
+        {polyMsg && (
+          <div className={`mt-3 text-sm p-3 rounded-lg ${polyMsg.toLowerCase().includes('error') || polyMsg.toLowerCase().includes('fail') ? 'bg-red-500/10 text-red-400' : 'bg-green-500/10 text-green-400'}`}>
+            {polyMsg}
+          </div>
+        )}
+
+        {/* API Architecture */}
+        <div className="mt-5 pt-4 border-t border-gray-800/50">
+          <p className="text-xs text-gray-500 mb-3 font-medium uppercase tracking-wider">API Endpoints</p>
+          <div className="grid grid-cols-2 md:grid-cols-4 gap-2 text-xs">
+            {[
+              { tag: 'CLOB', color: 'purple', host: 'clob.polymarket.com', desc: 'Orders & trading' },
+              { tag: 'GAMMA', color: 'blue', host: 'gamma-api.polymarket.com', desc: 'Market metadata' },
+              { tag: 'DATA', color: 'green', host: 'data-api.polymarket.com', desc: 'User positions' },
+              { tag: 'WS', color: 'yellow', host: 'ws-subscriptions-clob...', desc: 'Live orderbook' },
+            ].map(e => (
+              <div key={e.tag} className="bg-gray-800/30 rounded-lg p-2.5">
+                <span className={`inline-block px-1.5 py-0.5 rounded bg-${e.color}-500/20 text-${e.color}-400 font-mono text-[10px] mb-1`}>{e.tag}</span>
+                <p className="text-gray-400 font-mono text-[11px]">{e.host}</p>
+                <p className="text-gray-600 mt-0.5">{e.desc}</p>
+              </div>
+            ))}
+          </div>
+          <div className="mt-3 text-[11px] text-gray-600">
+            <span className="text-gray-500">Auth:</span> L2 HMAC-SHA256 — API Key + Secret + Passphrase per request
+          </div>
+        </div>
+      </div>
+
+      {/* System Status */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">System Status</h2>
+        <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
+          {[
+            { l: 'Database', s: h?.db_connected, d: 'PostgreSQL poly_betting' },
+            { l: 'NWS API', s: testResults.nws || 'unknown', d: 'api.weather.gov' },
+            { l: 'Polymarket', s: polyStatus === 'connected' ? true : clobOk || 'unknown', d: 'clob.polymarket.com' },
+            { l: 'Slack', s: h?.slack_configured ? 'ok' : testResults.slack || 'no_token', d: 'Alerts channel' },
+          ].map(s =>
+            <div key={s.l} className="bg-gray-800/30 rounded-lg p-3">
+              <div className="flex items-center gap-2 mb-1">
+                <span className={`w-2 h-2 rounded-full ${dot(s.s)}`} />
+                <span className="text-sm font-medium">{s.l}</span>
+              </div>
+              <p className="text-xs text-gray-500">{s.d}</p>
+            </div>
+          )}
+        </div>
+      </div>
+
+      {/* Connectivity Tests */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Connectivity Tests</h2>
+        <div className="flex gap-3">
+          <button onClick={testNWS} className={`btn ${testResults.nws === 'ok' ? 'btn-s' : 'btn-p'}`}>
+            {testResults.nws === 'testing' ? '...' : testResults.nws === 'ok' ? 'NWS OK' : 'Test NWS'}
+          </button>
+          <button onClick={testPublicClob} className={`btn ${clobOk === 'ok' ? 'btn-s' : 'btn-p'}`}>
+            {clobOk === 'testing' ? '...' : clobOk === 'ok' ? 'CLOB OK' : 'Test CLOB'}
+          </button>
+          <button onClick={testSlack} className={`btn ${testResults.slack === 'ok' ? 'btn-s' : 'btn-p'}`}>
+            {testResults.slack === 'testing' ? '...' : testResults.slack === 'ok' ? 'Slack OK' : 'Test Slack'}
+          </button>
+        </div>
+      </div>
+
+      {/* Workers */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Manual Workers</h2>
+        <div className="flex flex-wrap gap-3">
+          {[
+            { w: 'market_ingest', l: 'Ingest Markets' },
+            { w: 'weather_fetch', l: 'Fetch Weather' },
+            { w: 'run_predictions', l: 'Run Predictions' },
+            { w: 'risk_scan', l: 'Risk Scan' },
+            { w: 'paper_trade', l: 'Paper Trade' },
+          ].map(({ w, l }) => (
+            <button key={w} onClick={() => runW(w)} className={`btn ${testResults[w] === 'ok' ? 'btn-s' : 'btn-p'}`}>
+              {testResults[w] === 'ok' ? `${l} Done` : l}
+            </button>
+          ))}
+        </div>
+      </div>
+
+      {/* Architecture */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Architecture</h2>
+        <div className="grid grid-cols-2 gap-4 text-sm">
+          <div><p className="text-gray-500 text-xs uppercase mb-2">Data Sources</p>
+            <ul className="space-y-1">
+              <li className="flex items-center gap-2"><span className="badge bg-blue">NWS</span>Live US forecasts</li>
+              <li className="flex items-center gap-2"><span className="badge bg-purple">GEFS</span>Ensembles</li>
+              <li className="flex items-center gap-2"><span className="badge bg-green">Gamma</span>Polymarket</li>
+              <li className="flex items-center gap-2"><span className="badge bg-yellow">CLOB</span>Orderbooks</li>
+            </ul>
+          </div>
+          <div><p className="text-gray-500 text-xs uppercase mb-2">Pipeline</p>
+            <ol className="space-y-1 list-decimal list-inside text-gray-300">
+              <li>Market parse + event extraction</li>
+              <li>Ensemble exceedance probability</li>
+              <li>Climatological prior</li>
+              <li>Bayesian calibration</li>
+              <li>Edge gate + risk sizing</li>
+              <li>Paper/live execution</li>
+            </ol>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/pages/Trades.jsx b/react-dash/src/pages/Trades.jsx
new file mode 100644
index 0000000..d270c46
--- /dev/null
+++ b/react-dash/src/pages/Trades.jsx
@@ -0,0 +1,134 @@
+import React, { useState, useEffect } from 'react';
+import { api } from '../api';
+import { BarChart, Bar, AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell } from 'recharts';
+
+const ChartTooltip = ({ active, payload, label, prefix = '$' }) => {
+  if (!active || !payload?.length) return null;
+  return (
+    <div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
+      <p className="text-gray-400 mb-1">{label}</p>
+      {payload.map((p, i) => (
+        <p key={i} style={{ color: p.color }} className="font-mono">{p.name}: {prefix}{typeof p.value === 'number' ? p.value.toFixed(2) : p.value}</p>
+      ))}
+    </div>
+  );
+};
+
+export default function Trades() {
+  const [orders, setO] = useState([]);
+  const [trades, setT] = useState([]);
+  const [loading, setL] = useState(true);
+
+  useEffect(() => {
+    api('/trades').then(d => { setO(d.orders || []); setT(d.trades || []); setL(false); });
+    const i = setInterval(() => api('/trades').then(d => { setO(d.orders || []); setT(d.trades || []); }), 15000);
+    return () => clearInterval(i);
+  }, []);
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  const pnl = trades.reduce((s, t) => s + parseFloat(t.pnl || 0), 0);
+  const fees = trades.reduce((s, t) => s + parseFloat(t.fee_usd || 0), 0);
+  const wr = trades.length > 0 ? (trades.filter(t => parseFloat(t.pnl || 0) > 0).length / trades.length * 100) : 0;
+  const avgSize = trades.length > 0 ? trades.reduce((s, t) => s + parseFloat(t.size_usd || 0), 0) / trades.length : 0;
+
+  // P&L per trade chart
+  const pnlData = trades.slice().reverse().map((t, i) => ({
+    idx: i + 1,
+    time: new Date(t.match_time).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
+    pnl: parseFloat(t.pnl || 0),
+    size: parseFloat(t.size_usd || 0),
+  }));
+
+  // Cumulative P&L
+  let cum = 0;
+  const cumData = pnlData.map(d => {
+    cum += d.pnl;
+    return { ...d, cumPnl: cum };
+  });
+
+  return (
+    <div className="space-y-6">
+      <div><h1 className="text-2xl font-bold">Trades & Orders</h1><p className="text-gray-500 text-sm">Execution history & analysis</p></div>
+
+      <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Orders</p><p className="stat-v text-blue-400">{orders.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Fills</p><p className="stat-v text-green-400">{trades.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">P&L</p><p className={`stat-v ${pnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>${pnl.toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Win Rate</p><p className="stat-v text-purple-400">{wr.toFixed(0)}%</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Avg Size</p><p className="stat-v text-yellow-400">${avgSize.toFixed(2)}</p></div>
+      </div>
+
+      {/* Charts */}
+      {trades.length > 0 && (
+        <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+          <div className="card">
+            <h2 className="font-semibold mb-3">P&L Per Trade</h2>
+            <ResponsiveContainer width="100%" height={220}>
+              <BarChart data={pnlData} barSize={16}>
+                <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+                <XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} />
+                <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `$${v}`} />
+                <Tooltip content={<ChartTooltip />} />
+                <Bar dataKey="pnl" name="P&L">
+                  {pnlData.map((entry, i) => (
+                    <Cell key={i} fill={entry.pnl >= 0 ? '#10b981' : '#ef4444'} fillOpacity={0.8} />
+                  ))}
+                </Bar>
+              </BarChart>
+            </ResponsiveContainer>
+          </div>
+          <div className="card">
+            <h2 className="font-semibold mb-3">Cumulative P&L</h2>
+            <ResponsiveContainer width="100%" height={220}>
+              <AreaChart data={cumData}>
+                <defs>
+                  <linearGradient id="cumTrade" x1="0" y1="0" x2="0" y2="1">
+                    <stop offset="5%" stopColor={cum >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0.3} />
+                    <stop offset="95%" stopColor={cum >= 0 ? '#10b981' : '#ef4444'} stopOpacity={0} />
+                  </linearGradient>
+                </defs>
+                <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+                <XAxis dataKey="time" tick={{ fill: '#6b7280', fontSize: 10 }} axisLine={false} tickLine={false} />
+                <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} tickFormatter={v => `$${v}`} />
+                <Tooltip content={<ChartTooltip />} />
+                <Area type="monotone" dataKey="cumPnl" stroke={cum >= 0 ? '#10b981' : '#ef4444'} strokeWidth={2} fill="url(#cumTrade)" name="Cumulative P&L" />
+              </AreaChart>
+            </ResponsiveContainer>
+          </div>
+        </div>
+      )}
+
+      {/* Orders Table */}
+      <div className="card overflow-x-auto"><h2 className="font-semibold mb-3">Orders</h2>
+        <table><thead><tr><th>ID</th><th>Side</th><th>Outcome</th><th>Price</th><th>Size</th><th>Status</th><th>Paper</th><th>Time</th></tr></thead><tbody>
+          {orders.length === 0 ? <tr><td colSpan={8} className="text-center text-gray-500 py-8">No orders yet</td></tr>
+            : orders.map(o => <tr key={o.order_id}>
+              <td className="font-mono text-xs">{o.order_id.substring(0, 12)}</td>
+              <td><span className={`badge ${o.side === 'buy' ? 'bg-green' : 'bg-red'}`}>{o.side?.toUpperCase()}</span></td>
+              <td>{o.outcome}</td><td className="font-mono">${parseFloat(o.price).toFixed(4)}</td>
+              <td className="font-mono">${parseFloat(o.size_usd || 0).toFixed(2)}</td>
+              <td><span className={`badge ${o.status === 'filled' ? 'bg-green' : o.status === 'rejected' ? 'bg-red' : 'bg-yellow'}`}>{o.status}</span></td>
+              <td>{o.paper_trade ? <span className="badge bg-yellow">PAPER</span> : <span className="badge bg-green">LIVE</span>}</td>
+              <td className="text-gray-400 text-sm">{new Date(o.submitted_ts).toLocaleString()}</td>
+            </tr>)}
+        </tbody></table>
+      </div>
+
+      {/* Trades Table */}
+      {trades.length > 0 && (
+        <div className="card overflow-x-auto"><h2 className="font-semibold mb-3">Trade Fills</h2>
+          <table><thead><tr><th>Time</th><th>Size</th><th>Price</th><th>Fee</th><th>P&L</th></tr></thead><tbody>
+            {trades.map((t, i) => <tr key={i}>
+              <td className="text-gray-400 text-sm">{t.match_time ? new Date(t.match_time).toLocaleString() : '-'}</td>
+              <td className="font-mono">${parseFloat(t.size_usd || 0).toFixed(2)}</td>
+              <td className="font-mono">${parseFloat(t.fill_price || 0).toFixed(4)}</td>
+              <td className="font-mono text-yellow-400">${parseFloat(t.fee_usd || 0).toFixed(4)}</td>
+              <td className={`font-mono ${parseFloat(t.pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>${parseFloat(t.pnl || 0).toFixed(2)}</td>
+            </tr>)}
+          </tbody></table>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/react-dash/src/pages/Weather.jsx b/react-dash/src/pages/Weather.jsx
new file mode 100644
index 0000000..9984f2b
--- /dev/null
+++ b/react-dash/src/pages/Weather.jsx
@@ -0,0 +1,151 @@
+import React, { useState, useEffect } from 'react';
+import { api, post } from '../api';
+import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend } from 'recharts';
+
+const CITIES = ['New York', 'Chicago', 'Los Angeles', 'Miami', 'Denver', 'Seattle', 'Boston', 'San Francisco', 'Houston', 'Phoenix'];
+const COLORS = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#06b6d4', '#ec4899', '#f97316'];
+
+const ChartTooltip = ({ active, payload, label }) => {
+  if (!active || !payload?.length) return null;
+  return (
+    <div className="bg-gray-900 border border-gray-700 rounded-lg px-3 py-2 text-xs shadow-xl">
+      <p className="text-gray-400 mb-1">{label}h lead</p>
+      {payload.map((p, i) => (
+        <p key={i} style={{ color: p.color }} className="font-mono">{p.name}: {p.value?.toFixed(1)}</p>
+      ))}
+    </div>
+  );
+};
+
+export default function Weather() {
+  const [fc, setFc] = useState([]);
+  const [loading, setL] = useState(true);
+  const [busy, setB] = useState(false);
+  const [city, setCity] = useState('New York');
+  const [chartVar, setChartVar] = useState('temperature');
+
+  const load = () => api('/weather').then(d => { setFc(d.forecasts || []); setL(false); });
+  useEffect(() => { load(); }, []);
+
+  async function fetchCity() { setB(true); await post('/weather', { action: 'fetch', city }); load(); setB(false); }
+  async function fetchAll() { setB(true); await post('/weather', { action: 'fetch_all' }); load(); setB(false); }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  const byLoc = {};
+  fc.forEach(f => { const k = f.location_name || f.geom_hash; if (!byLoc[k]) byLoc[k] = []; byLoc[k].push(f); });
+
+  // Build chart data - group by lead_hours for each location
+  const variables = [...new Set(fc.map(f => f.variable))];
+  const locations = Object.keys(byLoc);
+
+  // Chart data for selected variable across locations
+  const chartData = {};
+  Object.entries(byLoc).forEach(([loc, runs]) => {
+    runs.filter(r => r.variable === chartVar).forEach(r => {
+      const lead = parseInt(r.lead_hours || 0);
+      if (!chartData[lead]) chartData[lead] = { lead };
+      chartData[lead][loc] = parseFloat(r.value_mean || 0);
+    });
+  });
+  const chartArray = Object.values(chartData).sort((a, b) => a.lead - b.lead);
+
+  // Per-location temperature range chart (mean/min/max)
+  const firstLoc = locations[0];
+  const rangeData = firstLoc ? byLoc[firstLoc]
+    .filter(r => r.variable === chartVar)
+    .sort((a, b) => (a.lead_hours || 0) - (b.lead_hours || 0))
+    .map(r => ({
+      lead: parseInt(r.lead_hours || 0),
+      mean: parseFloat(r.value_mean || 0),
+      min: parseFloat(r.value_min || 0),
+      max: parseFloat(r.value_max || 0),
+    })) : [];
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div><h1 className="text-2xl font-bold">Weather Forecasts</h1><p className="text-gray-500 text-sm">NWS forecast data + ensemble analysis</p></div>
+        <div className="flex items-center gap-2">
+          <select value={city} onChange={e => setCity(e.target.value)} className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm">
+            {CITIES.map(c => <option key={c} value={c}>{c}</option>)}
+          </select>
+          <button onClick={fetchCity} disabled={busy} className="btn btn-p">{busy ? '...' : 'Fetch City'}</button>
+          <button onClick={fetchAll} disabled={busy} className="btn btn-s">Fetch All</button>
+        </div>
+      </div>
+
+      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Data Points</p><p className="stat-v text-blue-400">{fc.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Locations</p><p className="stat-v text-green-400">{locations.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Latest Run</p><p className="text-sm text-gray-300 mt-1">{fc[0]?.run_ts ? new Date(fc[0].run_ts).toLocaleString() : 'None'}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Variables</p><p className="stat-v text-purple-400">{variables.length}</p></div>
+      </div>
+
+      {/* Variable selector for charts */}
+      <div className="flex items-center gap-2">
+        <span className="text-sm text-gray-400">Chart variable:</span>
+        {variables.slice(0, 6).map(v => (
+          <button key={v} onClick={() => setChartVar(v)} className={`px-3 py-1 text-xs rounded-full border transition ${chartVar === v ? 'bg-blue-500/20 border-blue-500/50 text-blue-400' : 'border-gray-700 text-gray-500 hover:text-gray-300'}`}>
+            {v}
+          </button>
+        ))}
+      </div>
+
+      {/* Forecast Trends Chart */}
+      {chartArray.length > 0 && (
+        <div className="card">
+          <h2 className="font-semibold mb-3">{chartVar} Forecast by Location (Lead Hours)</h2>
+          <ResponsiveContainer width="100%" height={300}>
+            <LineChart data={chartArray}>
+              <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+              <XAxis dataKey="lead" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} label={{ value: 'Lead Hours', position: 'insideBottom', offset: -5, fill: '#6b7280', fontSize: 11 }} />
+              <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
+              <Tooltip content={<ChartTooltip />} />
+              <Legend wrapperStyle={{ fontSize: 11, color: '#9ca3af' }} />
+              {locations.slice(0, 8).map((loc, i) => (
+                <Line key={loc} type="monotone" dataKey={loc} stroke={COLORS[i % COLORS.length]} strokeWidth={2} dot={false} name={loc} />
+              ))}
+            </LineChart>
+          </ResponsiveContainer>
+        </div>
+      )}
+
+      {/* Range Chart for first location */}
+      {rangeData.length > 0 && (
+        <div className="card">
+          <h2 className="font-semibold mb-3">{firstLoc}: {chartVar} Range (Mean / Min / Max)</h2>
+          <ResponsiveContainer width="100%" height={240}>
+            <LineChart data={rangeData}>
+              <CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
+              <XAxis dataKey="lead" tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
+              <YAxis tick={{ fill: '#6b7280', fontSize: 11 }} axisLine={false} tickLine={false} />
+              <Tooltip content={<ChartTooltip />} />
+              <Legend wrapperStyle={{ fontSize: 11 }} />
+              <Line type="monotone" dataKey="max" stroke="#ef4444" strokeWidth={1.5} dot={false} strokeDasharray="4 4" name="Max" />
+              <Line type="monotone" dataKey="mean" stroke="#10b981" strokeWidth={2} dot={false} name="Mean" />
+              <Line type="monotone" dataKey="min" stroke="#3b82f6" strokeWidth={1.5} dot={false} strokeDasharray="4 4" name="Min" />
+            </LineChart>
+          </ResponsiveContainer>
+        </div>
+      )}
+
+      {/* Location Data Tables */}
+      {Object.entries(byLoc).slice(0, 8).map(([loc, runs]) => (
+        <div key={loc} className="card">
+          <h3 className="font-semibold mb-3">{loc}</h3>
+          <table><thead><tr><th>Var</th><th>Lead</th><th>Mean</th><th>Min</th><th>Max</th></tr></thead><tbody>
+            {runs.slice(0, 15).map((r, i) => <tr key={i}>
+              <td className="font-medium">{r.variable}</td>
+              <td className="font-mono">{r.lead_hours}h</td>
+              <td className="font-mono text-green-400">{parseFloat(r.value_mean || 0).toFixed(1)}</td>
+              <td className="font-mono text-blue-400">{parseFloat(r.value_min || 0).toFixed(1)}</td>
+              <td className="font-mono text-red-400">{parseFloat(r.value_max || 0).toFixed(1)}</td>
+            </tr>)}
+          </tbody></table>
+        </div>
+      ))}
+      {fc.length === 0 && <div className="card text-center py-12"><p className="text-gray-500">No forecast data. Click Fetch.</p></div>}
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/SetupWizard.jsx b/react-dash/src/setup/SetupWizard.jsx
new file mode 100644
index 0000000..800a636
--- /dev/null
+++ b/react-dash/src/setup/SetupWizard.jsx
@@ -0,0 +1,150 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { checkSession, post, api } from '../api';
+import Welcome from './steps/Welcome';
+import ConnectPoly from './steps/ConnectPoly';
+import TestConnection from './steps/TestConnection';
+import IngestMarkets from './steps/IngestMarkets';
+import FetchWeather from './steps/FetchWeather';
+import RunPredictions from './steps/RunPredictions';
+import RiskProfile from './steps/RiskProfile';
+import GoLive from './steps/GoLive';
+
+const STEPS = [
+  { label: 'Welcome', icon: '1' },
+  { label: 'Connect', icon: '2' },
+  { label: 'Test', icon: '3' },
+  { label: 'Ingest', icon: '4' },
+  { label: 'Weather', icon: '5' },
+  { label: 'Predict', icon: '6' },
+  { label: 'Risk', icon: '7' },
+  { label: 'Go Live', icon: '8' },
+];
+
+export default function SetupWizard() {
+  const nav = useNavigate();
+  const [authed, setAuthed] = useState(false);
+  const [step, setStep] = useState(0);
+  const [dir, setDir] = useState('forward');
+  const [data, setData] = useState({
+    keys: null,
+    connectionOk: false,
+    marketsIngested: 0,
+    weatherFetched: false,
+    predictionsRun: false,
+    riskProfile: null,
+  });
+  const contentRef = useRef(null);
+
+  useEffect(() => {
+    checkSession().then(ok => {
+      if (!ok) { nav('/login'); return; }
+      setAuthed(true);
+      // Try to restore progress
+      api('/setup').then(r => {
+        if (r.setup_complete) { nav('/'); return; }
+        if (r.setup_progress) {
+          setStep(r.setup_progress.step || 0);
+          setData(d => ({ ...d, ...r.setup_progress.data }));
+        }
+      }).catch(() => {});
+    });
+  }, []);
+
+  useEffect(() => {
+    // Save progress on step change
+    if (authed && step > 0) {
+      post('/setup', { action: 'save_progress', progress: { step, data } }).catch(() => {});
+    }
+  }, [step]);
+
+  function next() {
+    setDir('forward');
+    if (step < STEPS.length - 1) setStep(s => s + 1);
+  }
+  function back() {
+    setDir('back');
+    if (step > 0) setStep(s => s - 1);
+  }
+  function goTo(i) {
+    if (i <= step) {
+      setDir(i < step ? 'back' : 'forward');
+      setStep(i);
+    }
+  }
+  function update(partial) {
+    setData(d => ({ ...d, ...partial }));
+  }
+  async function complete() {
+    await post('/setup', { action: 'complete', progress: { step: STEPS.length - 1, data } });
+    nav('/');
+  }
+
+  if (!authed) {
+    return <div className="flex items-center justify-center min-h-screen"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+  }
+
+  const StepComponent = [Welcome, ConnectPoly, TestConnection, IngestMarkets, FetchWeather, RunPredictions, RiskProfile, GoLive][step];
+
+  return (
+    <div className="min-h-screen flex flex-col" style={{ background: 'var(--bg)' }}>
+      {/* Header */}
+      <div className="flex items-center gap-3 px-6 py-4 border-b border-gray-800">
+        <div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0" style={{ background: 'linear-gradient(135deg,#3b82f6,#8b5cf6)' }}>
+          <span className="text-white font-bold text-sm">P</span>
+        </div>
+        <span className="font-bold text-sm wizard-gradient">POLY SETUP</span>
+      </div>
+
+      {/* Stepper */}
+      <div className="px-6 py-5">
+        <div className="max-w-3xl mx-auto flex items-center">
+          {STEPS.map((s, i) => (
+            <React.Fragment key={i}>
+              <button
+                onClick={() => goTo(i)}
+                disabled={i > step}
+                className="flex flex-col items-center gap-1.5 group"
+                style={{ minWidth: 56 }}
+              >
+                <div className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold transition-all ${
+                  i < step ? 'bg-green-500 text-white' :
+                  i === step ? 'bg-blue-500 text-white step-active-pulse' :
+                  'bg-gray-800 text-gray-500'
+                } ${i <= step ? 'cursor-pointer' : 'cursor-default'}`}>
+                  {i < step ? (
+                    <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2.5" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
+                  ) : s.icon}
+                </div>
+                <span className={`text-[10px] font-medium ${
+                  i < step ? 'text-green-400' :
+                  i === step ? 'text-blue-400' :
+                  'text-gray-600'
+                }`}>{s.label}</span>
+              </button>
+              {i < STEPS.length - 1 && (
+                <div className={`stepper-line ${i < step ? 'done' : ''}`} />
+              )}
+            </React.Fragment>
+          ))}
+        </div>
+      </div>
+
+      {/* Step Content */}
+      <div className="flex-1 flex items-start justify-center px-6 pb-10">
+        <div className="w-full max-w-2xl" ref={contentRef}>
+          <div key={step} className="step-enter step-active">
+            <StepComponent
+              data={data}
+              update={update}
+              next={next}
+              back={back}
+              complete={complete}
+              step={step}
+            />
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/steps/ConnectPoly.jsx b/react-dash/src/setup/steps/ConnectPoly.jsx
new file mode 100644
index 0000000..5f808a8
--- /dev/null
+++ b/react-dash/src/setup/steps/ConnectPoly.jsx
@@ -0,0 +1,123 @@
+import React, { useState } from 'react';
+import { post } from '../../api';
+
+export default function ConnectPoly({ data, update, next, back }) {
+  const [key, setKey] = useState(data.keys?.api_key || '');
+  const [secret, setSecret] = useState(data.keys?.api_secret || '');
+  const [passphrase, setPassphrase] = useState(data.keys?.api_passphrase || '');
+  const [wallet, setWallet] = useState(data.keys?.wallet || '');
+  const [showSecret, setShowSecret] = useState(false);
+  const [saving, setSaving] = useState(false);
+  const [error, setError] = useState(null);
+
+  async function save() {
+    setSaving(true);
+    setError(null);
+    try {
+      const r = await post('/polymarket', {
+        action: 'save_keys',
+        api_key: key,
+        api_secret: secret,
+        api_passphrase: passphrase,
+        wallet: wallet || undefined,
+      });
+      if (r.success) {
+        update({ keys: { api_key: key, api_secret: secret, api_passphrase: passphrase, wallet } });
+        next();
+      } else {
+        setError(r.error || 'Failed to save');
+      }
+    } catch {
+      setError('Connection error');
+    }
+    setSaving(false);
+  }
+
+  function skip() {
+    update({ keys: null, skippedConnect: true });
+    next();
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Connect Polymarket</h2>
+        <p className="text-gray-400 text-sm">Enter your Builder API credentials to enable live trading</p>
+      </div>
+
+      <div className="card space-y-4">
+        <div>
+          <label className="block text-xs text-gray-400 mb-1.5 font-medium">API Key</label>
+          <input
+            type="text"
+            value={key}
+            onChange={e => setKey(e.target.value)}
+            placeholder="Your Polymarket API key"
+            className="w-full px-3 py-2 bg-gray-800/50 border border-gray-700 rounded-lg text-sm focus:border-blue-500 focus:outline-none font-mono"
+          />
+        </div>
+        <div>
+          <label className="block text-xs text-gray-400 mb-1.5 font-medium">API Secret</label>
+          <div className="relative">
+            <input
+              type={showSecret ? 'text' : 'password'}
+              value={secret}
+              onChange={e => setSecret(e.target.value)}
+              placeholder="Your API secret"
+              className="w-full px-3 py-2 bg-gray-800/50 border border-gray-700 rounded-lg text-sm focus:border-blue-500 focus:outline-none font-mono pr-16"
+            />
+            <button onClick={() => setShowSecret(!showSecret)} className="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-gray-500 hover:text-gray-300">
+              {showSecret ? 'Hide' : 'Show'}
+            </button>
+          </div>
+        </div>
+        <div>
+          <label className="block text-xs text-gray-400 mb-1.5 font-medium">Passphrase</label>
+          <input
+            type="password"
+            value={passphrase}
+            onChange={e => setPassphrase(e.target.value)}
+            placeholder="Your API passphrase"
+            className="w-full px-3 py-2 bg-gray-800/50 border border-gray-700 rounded-lg text-sm focus:border-blue-500 focus:outline-none font-mono"
+          />
+        </div>
+        <div>
+          <label className="block text-xs text-gray-400 mb-1.5 font-medium">Wallet Address <span className="text-gray-600">(optional)</span></label>
+          <input
+            type="text"
+            value={wallet}
+            onChange={e => setWallet(e.target.value)}
+            placeholder="0x..."
+            className="w-full px-3 py-2 bg-gray-800/50 border border-gray-700 rounded-lg text-sm focus:border-blue-500 focus:outline-none font-mono"
+          />
+        </div>
+
+        {error && <p className="text-red-400 text-xs">{error}</p>}
+      </div>
+
+      <div className="card bg-gray-800/30">
+        <h3 className="text-sm font-medium mb-2">Where to get your keys</h3>
+        <ol className="text-xs text-gray-400 space-y-1.5 list-decimal list-inside">
+          <li>Go to <span className="text-blue-400 font-mono">polymarket.com</span></li>
+          <li>Click your profile → Settings</li>
+          <li>Navigate to the <span className="text-blue-400">Builder</span> tab</li>
+          <li>Create a new API key — save the secret immediately</li>
+        </ol>
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <div className="flex items-center gap-3">
+          <button onClick={skip} className="text-sm text-gray-500 hover:text-gray-300">Skip (paper mode)</button>
+          <button
+            onClick={save}
+            disabled={saving || (!key && !secret)}
+            className="wizard-btn"
+          >
+            {saving ? 'Saving...' : 'Save & Continue'}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/steps/FetchWeather.jsx b/react-dash/src/setup/steps/FetchWeather.jsx
new file mode 100644
index 0000000..12a0e90
--- /dev/null
+++ b/react-dash/src/setup/steps/FetchWeather.jsx
@@ -0,0 +1,85 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../../api';
+
+export default function FetchWeather({ data, update, next, back }) {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  useEffect(() => {
+    runFetch();
+  }, []);
+
+  async function runFetch() {
+    setLoading(true);
+    setError(null);
+    try {
+      const r = await post('/weather', { action: 'fetch_all' });
+      setResult(r);
+      update({ weatherFetched: true });
+    } catch {
+      setError('Failed to fetch weather data');
+    }
+    setLoading(false);
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Weather Data</h2>
+        <p className="text-gray-400 text-sm">Pulling NWS forecast data for tracked markets</p>
+      </div>
+
+      <div className="card">
+        {loading ? (
+          <div className="flex flex-col items-center justify-center py-12 gap-4">
+            <div className="animate-spin w-10 h-10 border-3 border-blue-500 border-t-transparent rounded-full" />
+            <p className="text-sm text-gray-400">Fetching NWS forecast data...</p>
+          </div>
+        ) : error ? (
+          <div className="text-center py-8">
+            <div className="w-12 h-12 rounded-full bg-red-500/10 flex items-center justify-center mx-auto mb-3">
+              <svg className="w-6 h-6 text-red-400" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
+              </svg>
+            </div>
+            <p className="text-red-400 text-sm mb-3">{error}</p>
+            <button onClick={runFetch} className="btn btn-s text-sm">Retry</button>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            <div className="flex items-center justify-center gap-3 py-4">
+              <div className="w-12 h-12 rounded-full bg-green-500/10 flex items-center justify-center">
+                <svg className="w-6 h-6 text-green-400" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
+                </svg>
+              </div>
+              <div>
+                <p className="font-semibold text-green-400">Weather Data Loaded</p>
+                <p className="text-xs text-gray-400">NWS forecasts fetched successfully</p>
+              </div>
+            </div>
+
+            <div className="grid grid-cols-2 gap-4">
+              <div className="text-center p-3 bg-gray-800/30 rounded-lg">
+                <p className="stat-v text-blue-400">{result?.cities || result?.locations || '—'}</p>
+                <p className="text-xs text-gray-500 mt-1">Cities Fetched</p>
+              </div>
+              <div className="text-center p-3 bg-gray-800/30 rounded-lg">
+                <p className="stat-v text-purple-400">{result?.data_points || result?.forecasts || '—'}</p>
+                <p className="text-xs text-gray-500 mt-1">Data Points</p>
+              </div>
+            </div>
+          </div>
+        )}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={next} disabled={loading} className="wizard-btn">
+          {loading ? 'Fetching...' : 'Continue'}
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/steps/GoLive.jsx b/react-dash/src/setup/steps/GoLive.jsx
new file mode 100644
index 0000000..23e6d97
--- /dev/null
+++ b/react-dash/src/setup/steps/GoLive.jsx
@@ -0,0 +1,115 @@
+import React, { useState } from 'react';
+
+const CHECK_ITEMS = [
+  { key: 'keys', label: 'Polymarket Connected', check: d => !!d.keys },
+  { key: 'conn', label: 'Connection Verified', check: d => !!d.connectionOk },
+  { key: 'markets', label: 'Markets Ingested', check: d => d.marketsIngested > 0 },
+  { key: 'weather', label: 'Weather Data Loaded', check: d => !!d.weatherFetched },
+  { key: 'preds', label: 'Predictions Generated', check: d => !!d.predictionsRun },
+  { key: 'risk', label: 'Risk Profile Set', check: d => !!d.riskProfile },
+];
+
+export default function GoLive({ data, update, complete, back }) {
+  const [paperMode, setPaperMode] = useState(true);
+  const [launching, setLaunching] = useState(false);
+
+  const completedSteps = CHECK_ITEMS.filter(c => c.check(data)).length;
+  const skippedPoly = data.skippedConnect;
+
+  async function launch() {
+    setLaunching(true);
+    await complete();
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Ready to Launch</h2>
+        <p className="text-gray-400 text-sm">Review your setup and start trading</p>
+      </div>
+
+      {/* Checklist */}
+      <div className="card">
+        <h3 className="font-semibold text-sm mb-3">Setup Summary</h3>
+        <div className="space-y-2">
+          {CHECK_ITEMS.map(c => {
+            const ok = c.check(data);
+            const skipped = c.key === 'keys' && skippedPoly;
+            return (
+              <div key={c.key} className="flex items-center gap-3">
+                {ok ? (
+                  <div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0">
+                    <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
+                  </div>
+                ) : skipped ? (
+                  <div className="w-5 h-5 rounded-full bg-yellow-500 flex items-center justify-center flex-shrink-0">
+                    <span className="text-white text-[10px] font-bold">—</span>
+                  </div>
+                ) : (
+                  <div className="w-5 h-5 rounded-full bg-gray-700 flex-shrink-0" />
+                )}
+                <span className={`text-sm ${ok ? 'text-gray-200' : skipped ? 'text-yellow-400' : 'text-gray-500'}`}>
+                  {c.label}{skipped ? ' (skipped)' : ''}
+                </span>
+              </div>
+            );
+          })}
+        </div>
+        <div className="mt-3 pt-3 border-t border-gray-800">
+          <p className="text-xs text-gray-500">{completedSteps}/{CHECK_ITEMS.length} steps completed</p>
+        </div>
+      </div>
+
+      {/* Stats Row */}
+      <div className="grid grid-cols-3 gap-4">
+        <div className="card text-center">
+          <p className="stat-v text-purple-400">{data.marketsIngested || 0}</p>
+          <p className="text-xs text-gray-500 mt-1">Markets</p>
+        </div>
+        <div className="card text-center">
+          <p className="stat-v text-blue-400">{data.riskProfile || '—'}</p>
+          <p className="text-xs text-gray-500 mt-1">Risk Profile</p>
+        </div>
+        <div className="card text-center">
+          <p className="stat-v text-green-400">{data.keys ? 'LIVE' : 'PAPER'}</p>
+          <p className="text-xs text-gray-500 mt-1">Mode</p>
+        </div>
+      </div>
+
+      {/* Paper/Live Toggle */}
+      <div className="card">
+        <div className="flex items-center justify-between">
+          <div>
+            <h3 className="font-semibold text-sm">Trading Mode</h3>
+            <p className="text-xs text-gray-500 mt-0.5">
+              {paperMode ? 'Simulated trades, no real funds' : 'Real trades on Polymarket'}
+            </p>
+          </div>
+          <div className="flex items-center gap-3">
+            <span className={`text-xs font-medium ${paperMode ? 'text-yellow-400' : 'text-gray-500'}`}>Paper</span>
+            <button
+              onClick={() => setPaperMode(!paperMode)}
+              disabled={!data.keys}
+              className={`w-11 h-6 rounded-full transition-colors relative ${paperMode ? 'bg-yellow-500/30' : 'bg-green-500/30'} ${!data.keys ? 'opacity-50 cursor-not-allowed' : ''}`}
+            >
+              <div className={`w-4 h-4 rounded-full absolute top-1 transition-all ${paperMode ? 'left-1 bg-yellow-400' : 'left-6 bg-green-400'}`} />
+            </button>
+            <span className={`text-xs font-medium ${!paperMode ? 'text-green-400' : 'text-gray-500'}`}>Live</span>
+          </div>
+        </div>
+        {!paperMode && (
+          <div className="mt-3 p-2 bg-red-500/10 border border-red-500/20 rounded-lg">
+            <p className="text-xs text-red-400">Live mode will use real USDC on Polygon. Make sure your wallet is funded.</p>
+          </div>
+        )}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={launch} disabled={launching} className="wizard-btn text-lg px-8">
+          {launching ? 'Launching...' : 'Launch Poly'}
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/steps/IngestMarkets.jsx b/react-dash/src/setup/steps/IngestMarkets.jsx
new file mode 100644
index 0000000..659b06f
--- /dev/null
+++ b/react-dash/src/setup/steps/IngestMarkets.jsx
@@ -0,0 +1,94 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../../api';
+
+export default function IngestMarkets({ data, update, next, back }) {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  useEffect(() => {
+    runIngest();
+  }, []);
+
+  async function runIngest() {
+    setLoading(true);
+    setError(null);
+    try {
+      const r = await post('/markets', { action: 'ingest' });
+      setResult(r);
+      update({ marketsIngested: r.ingested || r.markets?.length || 0 });
+    } catch (err) {
+      setError('Failed to ingest markets');
+    }
+    setLoading(false);
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Ingest Markets</h2>
+        <p className="text-gray-400 text-sm">Scanning Polymarket for weather prediction markets</p>
+      </div>
+
+      <div className="card">
+        {loading ? (
+          <div className="flex flex-col items-center justify-center py-12 gap-4">
+            <div className="animate-spin w-10 h-10 border-3 border-purple-500 border-t-transparent rounded-full" />
+            <p className="text-sm text-gray-400">Scanning Polymarket for weather markets...</p>
+          </div>
+        ) : error ? (
+          <div className="text-center py-8">
+            <div className="w-12 h-12 rounded-full bg-red-500/10 flex items-center justify-center mx-auto mb-3">
+              <svg className="w-6 h-6 text-red-400" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
+              </svg>
+            </div>
+            <p className="text-red-400 text-sm mb-3">{error}</p>
+            <button onClick={runIngest} className="btn btn-s text-sm">Retry</button>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            <div className="grid grid-cols-3 gap-4">
+              <div className="text-center">
+                <p className="stat-v text-purple-400">{result?.scanned || '—'}</p>
+                <p className="text-xs text-gray-500 mt-1">Scanned</p>
+              </div>
+              <div className="text-center">
+                <p className="stat-v text-green-400">{result?.ingested || result?.markets?.length || 0}</p>
+                <p className="text-xs text-gray-500 mt-1">Ingested</p>
+              </div>
+              <div className="text-center">
+                <p className="stat-v text-blue-400">{result?.skipped || 0}</p>
+                <p className="text-xs text-gray-500 mt-1">Skipped</p>
+              </div>
+            </div>
+
+            {(result?.markets || []).length > 0 && (
+              <div className="border-t border-gray-800 pt-3 mt-3">
+                <p className="text-xs text-gray-500 mb-2 uppercase font-medium">Ingested Markets</p>
+                <div className="space-y-1.5 max-h-48 overflow-y-auto">
+                  {(result.markets || []).slice(0, 10).map((m, i) => (
+                    <div key={i} className="flex items-center gap-2 text-xs">
+                      <span className="w-1.5 h-1.5 rounded-full bg-green-500 flex-shrink-0" />
+                      <span className="text-gray-300 truncate">{m.question || m}</span>
+                    </div>
+                  ))}
+                  {(result.markets || []).length > 10 && (
+                    <p className="text-xs text-gray-600">+{result.markets.length - 10} more</p>
+                  )}
+                </div>
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={next} disabled={loading} className="wizard-btn">
+          {loading ? 'Processing...' : 'Continue'}
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/steps/RiskProfile.jsx b/react-dash/src/setup/steps/RiskProfile.jsx
new file mode 100644
index 0000000..61ed378
--- /dev/null
+++ b/react-dash/src/setup/steps/RiskProfile.jsx
@@ -0,0 +1,109 @@
+import React, { useState } from 'react';
+import { post } from '../../api';
+
+const PROFILES = [
+  {
+    name: 'Conservative',
+    kelly: 0.05,
+    maxBet: 0.25,
+    survival: 0.99,
+    color: 'blue',
+    desc: 'Small bets, maximum safety. Best for learning.',
+  },
+  {
+    name: 'Moderate',
+    kelly: 0.10,
+    maxBet: 0.50,
+    survival: 0.90,
+    color: 'purple',
+    desc: 'Balanced risk/reward. Recommended for most users.',
+    recommended: true,
+  },
+  {
+    name: 'Aggressive',
+    kelly: 0.20,
+    maxBet: 1.00,
+    survival: 0.70,
+    color: 'red',
+    desc: 'Larger bets, higher variance. For experienced traders.',
+  },
+];
+
+export default function RiskProfile({ data, update, next, back }) {
+  const [selected, setSelected] = useState(data.riskProfile || 'Moderate');
+  const [saving, setSaving] = useState(false);
+
+  async function save() {
+    setSaving(true);
+    const profile = PROFILES.find(p => p.name === selected);
+    try {
+      await post('/risk', {
+        action: 'update_config',
+        kelly_fraction: profile.kelly,
+        max_bet: profile.maxBet,
+        survival_target: profile.survival,
+        risk_profile: profile.name,
+      });
+      update({ riskProfile: selected });
+      next();
+    } catch {}
+    setSaving(false);
+  }
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Risk Profile</h2>
+        <p className="text-gray-400 text-sm">Choose how aggressively Poly should trade</p>
+      </div>
+
+      <div className="grid grid-cols-3 gap-4">
+        {PROFILES.map(p => (
+          <button
+            key={p.name}
+            onClick={() => setSelected(p.name)}
+            className={`card text-left transition-all relative ${
+              selected === p.name
+                ? `ring-2 ring-${p.color === 'blue' ? 'blue' : p.color === 'purple' ? 'purple' : 'red'}-500 bg-${p.color === 'blue' ? 'blue' : p.color === 'purple' ? 'purple' : 'red'}-500/5`
+                : 'hover:border-gray-700'
+            }`}
+            style={selected === p.name ? {
+              boxShadow: `0 0 0 2px ${p.color === 'blue' ? '#3b82f6' : p.color === 'purple' ? '#8b5cf6' : '#ef4444'}`,
+            } : {}}
+          >
+            {p.recommended && (
+              <span className="absolute -top-2 right-3 text-[9px] font-bold uppercase px-2 py-0.5 rounded-full bg-purple-500 text-white">Recommended</span>
+            )}
+            <h3 className={`font-bold text-lg mb-1 ${
+              p.color === 'blue' ? 'text-blue-400' :
+              p.color === 'purple' ? 'text-purple-400' :
+              'text-red-400'
+            }`}>{p.name}</h3>
+            <p className="text-xs text-gray-500 mb-3">{p.desc}</p>
+            <div className="space-y-1.5 text-xs">
+              <div className="flex justify-between">
+                <span className="text-gray-500">Kelly Fraction</span>
+                <span className="font-mono text-gray-300">{(p.kelly * 100).toFixed(0)}%</span>
+              </div>
+              <div className="flex justify-between">
+                <span className="text-gray-500">Max Bet</span>
+                <span className="font-mono text-gray-300">${p.maxBet.toFixed(2)}</span>
+              </div>
+              <div className="flex justify-between">
+                <span className="text-gray-500">Survival Target</span>
+                <span className="font-mono text-gray-300">{(p.survival * 100).toFixed(0)}%</span>
+              </div>
+            </div>
+          </button>
+        ))}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={save} disabled={saving} className="wizard-btn">
+          {saving ? 'Saving...' : 'Save & Continue'}
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/steps/RunPredictions.jsx b/react-dash/src/setup/steps/RunPredictions.jsx
new file mode 100644
index 0000000..1300bed
--- /dev/null
+++ b/react-dash/src/setup/steps/RunPredictions.jsx
@@ -0,0 +1,118 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../../api';
+
+export default function RunPredictions({ data, update, next, back }) {
+  const [loading, setLoading] = useState(true);
+  const [result, setResult] = useState(null);
+  const [error, setError] = useState(null);
+
+  useEffect(() => {
+    runPredictions();
+  }, []);
+
+  async function runPredictions() {
+    setLoading(true);
+    setError(null);
+    try {
+      const r = await post('/models', { action: 'run_predictions' });
+      setResult(r);
+      update({ predictionsRun: true });
+    } catch {
+      setError('Failed to generate predictions');
+    }
+    setLoading(false);
+  }
+
+  const predictions = result?.predictions || [];
+  const buySignals = predictions.filter(p => p.signal === 'BUY' || p.edge > 0);
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">AI Predictions</h2>
+        <p className="text-gray-400 text-sm">Generating probability models and calculating edges</p>
+      </div>
+
+      <div className="card">
+        {loading ? (
+          <div className="flex flex-col items-center justify-center py-12 gap-4">
+            <div className="animate-spin w-10 h-10 border-3 border-green-500 border-t-transparent rounded-full" />
+            <p className="text-sm text-gray-400">Running prediction models...</p>
+          </div>
+        ) : error ? (
+          <div className="text-center py-8">
+            <div className="w-12 h-12 rounded-full bg-red-500/10 flex items-center justify-center mx-auto mb-3">
+              <svg className="w-6 h-6 text-red-400" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
+              </svg>
+            </div>
+            <p className="text-red-400 text-sm mb-3">{error}</p>
+            <button onClick={runPredictions} className="btn btn-s text-sm">Retry</button>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            <div className="grid grid-cols-3 gap-4">
+              <div className="text-center">
+                <p className="stat-v text-purple-400">{predictions.length}</p>
+                <p className="text-xs text-gray-500 mt-1">Predictions</p>
+              </div>
+              <div className="text-center">
+                <p className="stat-v text-green-400">{buySignals.length}</p>
+                <p className="text-xs text-gray-500 mt-1">Buy Signals</p>
+              </div>
+              <div className="text-center">
+                <p className="stat-v text-blue-400">{result?.edges_found || buySignals.length}</p>
+                <p className="text-xs text-gray-500 mt-1">Edges Found</p>
+              </div>
+            </div>
+
+            {predictions.length > 0 && (
+              <div className="border-t border-gray-800 pt-3">
+                <p className="text-xs text-gray-500 mb-2 uppercase font-medium">Top Predictions</p>
+                <table>
+                  <thead>
+                    <tr>
+                      <th>Market</th>
+                      <th>P(Yes)</th>
+                      <th>Edge</th>
+                      <th>Signal</th>
+                    </tr>
+                  </thead>
+                  <tbody>
+                    {predictions.slice(0, 6).map((p, i) => (
+                      <tr key={i}>
+                        <td className="max-w-[200px]"><span className="truncate block text-xs">{p.question || p.market}</span></td>
+                        <td className="font-mono text-sm">{((p.p_yes || p.probability || 0) * 100).toFixed(1)}%</td>
+                        <td className={`font-mono text-sm ${(p.edge || 0) > 0 ? 'text-green-400' : 'text-gray-500'}`}>
+                          {((p.edge || 0) * 100).toFixed(1)}%
+                        </td>
+                        <td>
+                          <span className={`badge ${p.signal === 'BUY' || (p.edge || 0) > 0 ? 'bg-green' : 'bg-red'}`}>
+                            {p.signal || ((p.edge || 0) > 0 ? 'BUY' : 'HOLD')}
+                          </span>
+                        </td>
+                      </tr>
+                    ))}
+                  </tbody>
+                </table>
+              </div>
+            )}
+
+            {predictions.length === 0 && !error && (
+              <div className="text-center py-4">
+                <p className="text-gray-500 text-sm">No predictions generated yet — models may need more market data</p>
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={next} disabled={loading} className="wizard-btn">
+          {loading ? 'Processing...' : 'Continue'}
+        </button>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/steps/TestConnection.jsx b/react-dash/src/setup/steps/TestConnection.jsx
new file mode 100644
index 0000000..dcac64d
--- /dev/null
+++ b/react-dash/src/setup/steps/TestConnection.jsx
@@ -0,0 +1,127 @@
+import React, { useState, useEffect } from 'react';
+import { post } from '../../api';
+
+export default function TestConnection({ data, update, next, back }) {
+  const [publicTest, setPublicTest] = useState(null);
+  const [authTest, setAuthTest] = useState(null);
+  const [publicLoading, setPublicLoading] = useState(true);
+  const [authLoading, setAuthLoading] = useState(false);
+
+  useEffect(() => {
+    runPublicTest();
+  }, []);
+
+  async function runPublicTest() {
+    setPublicLoading(true);
+    try {
+      const r = await post('/polymarket', { action: 'test_public' });
+      setPublicTest(r);
+      // Auto-run auth test if keys exist
+      if (r.success && data.keys) {
+        runAuthTest();
+      }
+    } catch {
+      setPublicTest({ error: true });
+    }
+    setPublicLoading(false);
+  }
+
+  async function runAuthTest() {
+    setAuthLoading(true);
+    try {
+      const r = await post('/polymarket', { action: 'auth_test' });
+      setAuthTest(r);
+      if (r.success) update({ connectionOk: true });
+    } catch {
+      setAuthTest({ error: true });
+    }
+    setAuthLoading(false);
+  }
+
+  const skipped = data.skippedConnect;
+
+  return (
+    <div className="space-y-6">
+      <div className="text-center">
+        <h2 className="text-2xl font-bold mb-2">Test Connection</h2>
+        <p className="text-gray-400 text-sm">Verifying your Polymarket API access</p>
+      </div>
+
+      <div className="grid grid-cols-2 gap-4">
+        {/* Public CLOB Test */}
+        <div className="card">
+          <div className="flex items-center gap-2 mb-3">
+            {publicLoading ? (
+              <div className="animate-spin w-5 h-5 border-2 border-blue-500 border-t-transparent rounded-full" />
+            ) : publicTest?.success ? (
+              <div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center">
+                <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
+              </div>
+            ) : (
+              <div className="w-5 h-5 rounded-full bg-red-500 flex items-center justify-center">
+                <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
+              </div>
+            )}
+            <h3 className="font-semibold text-sm">Public CLOB</h3>
+          </div>
+          {publicTest?.success ? (
+            <div className="space-y-1 text-xs text-gray-400">
+              <p>Server Time: <span className="text-green-400 font-mono">{publicTest.server_time ? new Date(parseInt(publicTest.server_time) * 1000).toLocaleTimeString() : 'OK'}</span></p>
+              <p>Endpoint: <span className="text-gray-300 font-mono">clob.polymarket.com</span></p>
+            </div>
+          ) : publicTest?.error ? (
+            <p className="text-xs text-red-400">Could not reach CLOB API</p>
+          ) : null}
+        </div>
+
+        {/* Authenticated Test */}
+        <div className="card">
+          <div className="flex items-center gap-2 mb-3">
+            {skipped ? (
+              <div className="w-5 h-5 rounded-full bg-yellow-500 flex items-center justify-center">
+                <span className="text-white text-xs font-bold">—</span>
+              </div>
+            ) : authLoading ? (
+              <div className="animate-spin w-5 h-5 border-2 border-purple-500 border-t-transparent rounded-full" />
+            ) : authTest?.success ? (
+              <div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center">
+                <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /></svg>
+              </div>
+            ) : authTest?.error ? (
+              <div className="w-5 h-5 rounded-full bg-red-500 flex items-center justify-center">
+                <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></svg>
+              </div>
+            ) : (
+              <div className="w-5 h-5 rounded-full bg-gray-600" />
+            )}
+            <h3 className="font-semibold text-sm">Authenticated</h3>
+          </div>
+          {skipped ? (
+            <p className="text-xs text-yellow-400">Skipped — paper mode only</p>
+          ) : authTest?.success ? (
+            <p className="text-xs text-green-400">HMAC-SHA256 auth verified</p>
+          ) : authTest?.error ? (
+            <p className="text-xs text-red-400">Auth failed — check credentials</p>
+          ) : !data.keys ? (
+            <p className="text-xs text-gray-500">No keys provided</p>
+          ) : (
+            <p className="text-xs text-gray-500">Waiting for public test...</p>
+          )}
+        </div>
+      </div>
+
+      {!publicLoading && !authLoading && (
+        <div className="flex justify-center">
+          <button onClick={runPublicTest} className="text-xs text-gray-500 hover:text-gray-300">
+            Retry Tests
+          </button>
+        </div>
+      )}
+
+      <div className="flex items-center justify-between">
+        <button onClick={back} className="text-sm text-gray-500 hover:text-gray-300">Back</button>
+        <button onClick={next} className="wizard-btn">Continue</button>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/src/setup/steps/Welcome.jsx b/react-dash/src/setup/steps/Welcome.jsx
new file mode 100644
index 0000000..e14f12e
--- /dev/null
+++ b/react-dash/src/setup/steps/Welcome.jsx
@@ -0,0 +1,69 @@
+import React from 'react';
+
+const FEATURES = [
+  { title: 'Market Intelligence', desc: 'Scans Polymarket for weather prediction markets and identifies profitable opportunities', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
+  { title: 'Weather Forecasting', desc: 'Pulls real NWS forecast data and builds probability models for weather outcomes', icon: 'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z' },
+  { title: 'Automated Trading', desc: 'Places bets on Polymarket when edges exceed your risk threshold, fully automated', icon: 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z' },
+];
+
+const PIPELINE = [
+  'Connect to Polymarket API',
+  'Ingest weather prediction markets',
+  'Fetch NWS forecast data',
+  'Generate AI probability models',
+  'Calculate edges & signals',
+  'Execute trades automatically',
+];
+
+export default function Welcome({ next }) {
+  return (
+    <div className="space-y-8">
+      <div className="text-center">
+        <h1 className="text-4xl font-bold wizard-gradient mb-3">Welcome to Poly</h1>
+        <p className="text-gray-400 text-lg">Weather-forecast micro-betting on Polymarket</p>
+      </div>
+
+      <div className="grid grid-cols-3 gap-4">
+        {FEATURES.map(f => (
+          <div key={f.title} className="card text-center">
+            <div className="w-10 h-10 rounded-lg bg-blue-500/10 flex items-center justify-center mx-auto mb-3">
+              <svg className="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
+                <path strokeLinecap="round" strokeLinejoin="round" d={f.icon} />
+              </svg>
+            </div>
+            <h3 className="font-semibold text-sm mb-1">{f.title}</h3>
+            <p className="text-xs text-gray-500 leading-relaxed">{f.desc}</p>
+          </div>
+        ))}
+      </div>
+
+      <div className="card">
+        <h3 className="font-semibold text-sm mb-3">How it works</h3>
+        <div className="grid grid-cols-2 gap-2">
+          {PIPELINE.map((p, i) => (
+            <div key={i} className="flex items-center gap-3 py-1.5">
+              <span className="w-6 h-6 rounded-full bg-blue-500/15 text-blue-400 flex items-center justify-center text-xs font-bold flex-shrink-0">{i + 1}</span>
+              <span className="text-sm text-gray-300">{p}</span>
+            </div>
+          ))}
+        </div>
+      </div>
+
+      <div className="card bg-yellow-500/5 border-yellow-500/20">
+        <div className="flex items-start gap-3">
+          <svg className="w-5 h-5 text-yellow-400 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
+          </svg>
+          <div>
+            <p className="text-sm font-medium text-yellow-400">You'll need Polymarket Builder API keys</p>
+            <p className="text-xs text-gray-400 mt-1">Go to polymarket.com → Settings → Builder tab to create API credentials. You can also skip this and use paper mode.</p>
+          </div>
+        </div>
+      </div>
+
+      <div className="flex justify-center">
+        <button onClick={next} className="wizard-btn">Get Started</button>
+      </div>
+    </div>
+  );
+}
diff --git a/react-dash/tailwind.config.js b/react-dash/tailwind.config.js
new file mode 100644
index 0000000..403e973
--- /dev/null
+++ b/react-dash/tailwind.config.js
@@ -0,0 +1,11 @@
+export default {
+  content: ['./index.html', './src/**/*.{js,jsx}'],
+  theme: {
+    extend: {
+      colors: {
+        b: { bg: '#0a0e1a', card: '#111827', border: '#1e293b', accent: '#3b82f6', green: '#10b981', red: '#ef4444', yellow: '#f59e0b', purple: '#8b5cf6' }
+      }
+    }
+  },
+  plugins: [],
+};
diff --git a/react-dash/vite.config.js b/react-dash/vite.config.js
new file mode 100644
index 0000000..dd2a3df
--- /dev/null
+++ b/react-dash/vite.config.js
@@ -0,0 +1,20 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+  plugins: [react()],
+  server: {
+    port: 7809,
+    host: '0.0.0.0',
+    proxy: {
+      '/api': 'http://127.0.0.1:7800'
+    }
+  },
+  preview: {
+    port: 7809,
+    host: '0.0.0.0',
+    proxy: {
+      '/api': 'http://127.0.0.1:7800'
+    }
+  }
+});
diff --git a/src/app/api/alerts/route.js b/src/app/api/alerts/route.js
new file mode 100644
index 0000000..e7ed89d
--- /dev/null
+++ b/src/app/api/alerts/route.js
@@ -0,0 +1,48 @@
+import { NextResponse } from 'next/server';
+const { query } = require('../../../lib/db');
+const { sendSlackAlert } = require('../../../lib/slack');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const res = await query('SELECT * FROM alerts_log ORDER BY ts DESC LIMIT 50');
+    return NextResponse.json({ alerts: res.rows });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
+
+export async function POST(req) {
+  const body = await req.json();
+
+  if (body.action === 'test') {
+    const result = await sendSlackAlert({
+      type: 'SYSTEM TEST',
+      severity: 'info',
+      message: 'Bertha weather betting system test alert',
+      details: { bankroll: '$50.00', mode: 'paper', status: 'operational' },
+    });
+
+    await query(`INSERT INTO alerts_log (alert_type, severity, message, slack_sent, details) VALUES ($1, $2, $3, $4, $5)`,
+      ['test', 'info', 'Test alert sent', result.ok || false, { slack_result: result }]);
+
+    return NextResponse.json({ sent: result.ok || false, result });
+  }
+
+  if (body.action === 'send') {
+    const result = await sendSlackAlert({
+      type: body.type || 'ALERT',
+      severity: body.severity || 'info',
+      message: body.message,
+      details: body.details,
+    });
+
+    await query(`INSERT INTO alerts_log (alert_type, severity, message, slack_sent, details) VALUES ($1, $2, $3, $4, $5)`,
+      [body.type || 'alert', body.severity || 'info', body.message, result.ok || false, body.details]);
+
+    return NextResponse.json({ sent: result.ok || false });
+  }
+
+  return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
+}
diff --git a/src/app/api/bankroll/route.js b/src/app/api/bankroll/route.js
new file mode 100644
index 0000000..83c8ec5
--- /dev/null
+++ b/src/app/api/bankroll/route.js
@@ -0,0 +1,26 @@
+import { NextResponse } from 'next/server';
+const { query } = require('../../../lib/db');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const [ledger, summary] = await Promise.all([
+      query('SELECT * FROM bankroll_ledger ORDER BY ts DESC LIMIT 200'),
+      query(`
+        SELECT
+          (SELECT balance_after FROM bankroll_ledger ORDER BY ts DESC LIMIT 1) as current_balance,
+          (SELECT COALESCE(SUM(amount), 0) FROM bankroll_ledger WHERE event_type = 'deposit') as total_deposits,
+          (SELECT COALESCE(SUM(amount), 0) FROM bankroll_ledger WHERE event_type = 'trade_pnl') as total_pnl,
+          (SELECT COALESCE(SUM(ABS(amount)), 0) FROM bankroll_ledger WHERE event_type = 'fee') as total_fees
+      `),
+    ]);
+
+    return NextResponse.json({
+      ledger: ledger.rows,
+      summary: summary.rows[0] || {},
+    });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
diff --git a/src/app/api/dashboard/route.js b/src/app/api/dashboard/route.js
new file mode 100644
index 0000000..4b803ff
--- /dev/null
+++ b/src/app/api/dashboard/route.js
@@ -0,0 +1,28 @@
+import { NextResponse } from 'next/server';
+const { query } = require('../../../lib/db');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const [markets, weatherMarkets, forecasts, predictions, trades, alerts] = await Promise.all([
+      query('SELECT COUNT(*) as cnt FROM markets'),
+      query("SELECT COUNT(*) as cnt FROM markets WHERE category = 'weather' OR parsed_event IS NOT NULL"),
+      query('SELECT COUNT(*) as cnt FROM forecast_runs'),
+      query('SELECT * FROM predictions ORDER BY asof_ts DESC LIMIT 10'),
+      query('SELECT t.*, o.side, o.outcome FROM trades t LEFT JOIN orders o ON t.order_id = o.order_id ORDER BY t.match_time DESC NULLS LAST LIMIT 10'),
+      query("SELECT * FROM alerts_log ORDER BY ts DESC LIMIT 5"),
+    ]);
+
+    return NextResponse.json({
+      market_count: parseInt(markets.rows[0]?.cnt || 0),
+      weather_market_count: parseInt(weatherMarkets.rows[0]?.cnt || 0),
+      forecast_count: parseInt(forecasts.rows[0]?.cnt || 0),
+      recent_predictions: predictions.rows,
+      recent_trades: trades.rows,
+      recent_alerts: alerts.rows,
+    });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
diff --git a/src/app/api/health/route.js b/src/app/api/health/route.js
new file mode 100644
index 0000000..c7b90ec
--- /dev/null
+++ b/src/app/api/health/route.js
@@ -0,0 +1,40 @@
+import { NextResponse } from 'next/server';
+const { pool } = require('../../../lib/db');
+const { authenticateRequest, createToken, VALID_USER, VALID_PASS } = require('../../../lib/auth');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET(req) {
+  const user = authenticateRequest(req);
+  let dbOk = false;
+  try { await pool.query('SELECT 1'); dbOk = true; } catch {}
+
+  let riskState = {};
+  try {
+    const res = await pool.query('SELECT * FROM risk_state ORDER BY id DESC LIMIT 1');
+    riskState = res.rows[0] || {};
+  } catch {}
+
+  return NextResponse.json({
+    service: 'bertha-weather-betting',
+    version: '1.0.0',
+    status: 'running',
+    authenticated: !!user,
+    db_connected: dbOk,
+    paper_mode: riskState.config?.paper_mode !== false,
+    kill_switch: riskState.kill_switch_active || false,
+    bankroll: parseFloat(riskState.bankroll || 50),
+    uptime: process.uptime(),
+    timestamp: new Date().toISOString(),
+    slack_configured: !!process.env.SLACK_BOT_TOKEN,
+  });
+}
+
+export async function POST(req) {
+  const body = await req.json();
+  if (body.username === VALID_USER && body.password === VALID_PASS) {
+    const token = createToken(body.username);
+    return NextResponse.json({ token, user: body.username });
+  }
+  return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
+}
diff --git a/src/app/api/markets/route.js b/src/app/api/markets/route.js
new file mode 100644
index 0000000..2f63b4e
--- /dev/null
+++ b/src/app/api/markets/route.js
@@ -0,0 +1,108 @@
+import { NextResponse } from 'next/server';
+const { query } = require('../../../lib/db');
+const { getMarkets, isWeatherMarket, parseWeatherEvent } = require('../../../lib/polymarket');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const res = await query(`
+      SELECT m.*,
+        (SELECT COUNT(*) FROM predictions p WHERE p.condition_id = m.condition_id) as prediction_count
+      FROM markets m
+      ORDER BY m.created_at DESC
+      LIMIT 100
+    `);
+    return NextResponse.json({ markets: res.rows });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
+
+export async function POST(req) {
+  const body = await req.json();
+
+  if (body.action === 'test_api') {
+    try {
+      const markets = await getMarkets({ limit: 5 });
+      return NextResponse.json({ success: true, market_count: Array.isArray(markets) ? markets.length : 0 });
+    } catch (err) {
+      return NextResponse.json({ success: false, error: err.message });
+    }
+  }
+
+  if (body.action === 'ingest') {
+    try {
+      let allMarkets = [];
+      let offset = 0;
+      const limit = 100;
+
+      // Paginate through Gamma API
+      for (let page = 0; page < 10; page++) {
+        const batch = await getMarkets({ limit, offset: offset + page * limit });
+        if (!Array.isArray(batch) || batch.length === 0) break;
+        allMarkets = allMarkets.concat(batch);
+        if (batch.length < limit) break;
+        await new Promise(r => setTimeout(r, 600)); // rate limit
+      }
+
+      // Filter weather markets
+      const weatherMarkets = allMarkets.filter(isWeatherMarket);
+      let ingested = 0;
+
+      for (const m of weatherMarkets) {
+        const parsed = parseWeatherEvent(m);
+        try {
+          await query(`
+            INSERT INTO markets (condition_id, slug, question, description, resolution_source, end_ts, category, liquidity, parsed_event, raw_gamma)
+            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
+            ON CONFLICT (condition_id) DO UPDATE SET
+              liquidity = EXCLUDED.liquidity, raw_gamma = EXCLUDED.raw_gamma,
+              parsed_event = EXCLUDED.parsed_event, updated_at = NOW()
+          `, [
+            m.conditionId || m.id, m.slug, m.question, m.description,
+            m.resolutionSource, m.endDate, 'weather',
+            m.liquidity || 0, parsed, m
+          ]);
+
+          // Insert tokens if available
+          if (m.tokens && Array.isArray(m.tokens)) {
+            for (const t of m.tokens) {
+              await query(`
+                INSERT INTO tokens (asset_id, condition_id, outcome, price, raw_gamma)
+                VALUES ($1, $2, $3, $4, $5)
+                ON CONFLICT (asset_id) DO UPDATE SET price = EXCLUDED.price, raw_gamma = EXCLUDED.raw_gamma, updated_at = NOW()
+              `, [t.token_id || t.asset_id || `${m.conditionId}_${t.outcome}`, m.conditionId || m.id, t.outcome, t.price || 0, t]);
+            }
+          }
+          ingested++;
+        } catch (err) {
+          console.error(`[Markets] Ingest error for ${m.conditionId}:`, err.message);
+        }
+      }
+
+      // Log alert
+      await query(`INSERT INTO alerts_log (alert_type, severity, message, details) VALUES ($1, $2, $3, $4)`,
+        ['market_ingest', 'info', `Ingested ${ingested} weather markets from ${allMarkets.length} total`, { total_scanned: allMarkets.length, weather_found: weatherMarkets.length, ingested }]);
+
+      return NextResponse.json({ ingested, total_scanned: allMarkets.length, weather_candidates: weatherMarkets.length });
+    } catch (err) {
+      return NextResponse.json({ error: err.message }, { status: 500 });
+    }
+  }
+
+  if (body.action === 'parse') {
+    try {
+      const res = await query('SELECT * FROM markets WHERE condition_id = $1', [body.condition_id]);
+      if (!res.rows[0]) return NextResponse.json({ error: 'Market not found' }, { status: 404 });
+      const market = res.rows[0];
+      const parsed = parseWeatherEvent(market.raw_gamma || market);
+      await query('UPDATE markets SET parsed_event = $1, updated_at = NOW() WHERE condition_id = $2', [parsed, body.condition_id]);
+      return NextResponse.json({ parsed, condition_id: body.condition_id });
+    } catch (err) {
+      return NextResponse.json({ error: err.message }, { status: 500 });
+    }
+  }
+
+  return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
+}
diff --git a/src/app/api/models/route.js b/src/app/api/models/route.js
new file mode 100644
index 0000000..4994828
--- /dev/null
+++ b/src/app/api/models/route.js
@@ -0,0 +1,108 @@
+import { NextResponse } from 'next/server';
+const { query } = require('../../../lib/db');
+const { generatePrediction, storePrediction } = require('../../../lib/model');
+const { calculateEdge } = require('../../../lib/risk');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const [models, predictions, calibration] = await Promise.all([
+      query('SELECT * FROM model_versions ORDER BY id DESC'),
+      query('SELECT * FROM predictions ORDER BY asof_ts DESC LIMIT 50'),
+      query('SELECT * FROM calibration_log ORDER BY eval_ts DESC LIMIT 20'),
+    ]);
+    return NextResponse.json({
+      models: models.rows,
+      predictions: predictions.rows,
+      calibration: calibration.rows,
+    });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
+
+export async function POST(req) {
+  const body = await req.json();
+
+  if (body.action === 'run_predictions') {
+    try {
+      // Get all active markets with parsed events
+      const marketsRes = await query(`
+        SELECT m.*, t.asset_id, t.price as token_price
+        FROM markets m
+        LEFT JOIN tokens t ON t.condition_id = m.condition_id AND t.outcome = 'Yes'
+        WHERE m.active = TRUE AND m.parsed_event IS NOT NULL
+      `);
+
+      let predictionsMade = 0;
+      const now = new Date();
+      const month = now.getMonth() + 1;
+
+      for (const market of marketsRes.rows) {
+        const event = market.parsed_event;
+        if (!event?.variable || !event?.threshold) continue;
+
+        // Get latest forecast data for this market's location
+        let forecastData = null;
+        if (event.location) {
+          const forecastRes = await query(`
+            SELECT * FROM forecast_runs
+            WHERE location_name = $1 AND variable = $2
+            ORDER BY run_ts DESC LIMIT 20
+          `, [event.location, event.variable === 'temperature' ? 'temperature' : event.variable]);
+
+          if (forecastRes.rows.length > 0) {
+            forecastData = {
+              values: forecastRes.rows.map(r => parseFloat(r.value_mean)),
+              timestamps: forecastRes.rows.map(r => r.run_ts),
+            };
+          }
+        }
+
+        // Generate prediction
+        const prediction = await generatePrediction({
+          conditionId: market.condition_id,
+          forecastFeatures: forecastData,
+          eventSpec: event,
+          month,
+        });
+
+        if (prediction.p_yes === null) continue;
+
+        // Calculate edge vs market
+        const marketMid = market.token_price ? parseFloat(market.token_price) : 0.50;
+        const { bestEdge, bestSide } = calculateEdge(prediction.p_yes, marketMid);
+
+        // Determine recommendation
+        let recommendation = 'HOLD';
+        if (bestEdge >= 0.03) recommendation = bestSide === 'YES' ? 'BUY_YES' : 'BUY_NO';
+        else if (bestEdge < -0.05) recommendation = 'SKIP';
+
+        // Store
+        await storePrediction({
+          conditionId: market.condition_id,
+          asofTs: now,
+          modelVersionId: 1,
+          pYes: prediction.p_yes,
+          confidence: prediction.confidence,
+          edge: bestEdge,
+          marketMid,
+          recommendation,
+          reasoning: { ...prediction.components, event_spec: event },
+        });
+
+        predictionsMade++;
+      }
+
+      await query(`INSERT INTO alerts_log (alert_type, severity, message, details) VALUES ($1, $2, $3, $4)`,
+        ['predictions', 'info', `Generated ${predictionsMade} predictions`, { predictions_made: predictionsMade, markets_checked: marketsRes.rows.length }]);
+
+      return NextResponse.json({ predictions_made: predictionsMade, markets_checked: marketsRes.rows.length });
+    } catch (err) {
+      return NextResponse.json({ error: err.message }, { status: 500 });
+    }
+  }
+
+  return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
+}
diff --git a/src/app/api/risk/route.js b/src/app/api/risk/route.js
new file mode 100644
index 0000000..40a8b8f
--- /dev/null
+++ b/src/app/api/risk/route.js
@@ -0,0 +1,52 @@
+import { NextResponse } from 'next/server';
+const { query } = require('../../../lib/db');
+const { getRiskState, updateRiskState, evaluateTrade, activateKillSwitch, deactivateKillSwitch } = require('../../../lib/risk');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const state = await getRiskState();
+    return NextResponse.json({ state });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
+
+export async function POST(req) {
+  const body = await req.json();
+
+  if (body.action === 'kill_switch_activate') {
+    const result = await activateKillSwitch(body.reason || 'Manual activation');
+    await query(`INSERT INTO alerts_log (alert_type, severity, message, details) VALUES ($1, $2, $3, $4)`,
+      ['kill_switch', 'critical', `Kill switch ACTIVATED: ${body.reason}`, result]);
+    return NextResponse.json(result);
+  }
+
+  if (body.action === 'kill_switch_deactivate') {
+    const result = await deactivateKillSwitch();
+    await query(`INSERT INTO alerts_log (alert_type, severity, message, details) VALUES ($1, $2, $3, $4)`,
+      ['kill_switch', 'warning', 'Kill switch deactivated', result]);
+    return NextResponse.json(result);
+  }
+
+  if (body.action === 'evaluate') {
+    const evaluation = await evaluateTrade({
+      pYes: body.pYes,
+      marketMid: body.marketMid,
+      spread: body.spread,
+      liquidity: body.liquidity,
+      conditionId: body.conditionId || 'simulation',
+    });
+    return NextResponse.json({ evaluation });
+  }
+
+  if (body.action === 'update_config') {
+    const state = await getRiskState();
+    const newConfig = { ...state.config, ...body.config };
+    await query('UPDATE risk_state SET config = $1, updated_at = NOW() WHERE id = $2', [newConfig, state.id]);
+    return NextResponse.json({ updated: true, config: newConfig });
+  }
+
+  return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
+}
diff --git a/src/app/api/trades/route.js b/src/app/api/trades/route.js
new file mode 100644
index 0000000..0342602
--- /dev/null
+++ b/src/app/api/trades/route.js
@@ -0,0 +1,100 @@
+import { NextResponse } from 'next/server';
+const { query } = require('../../../lib/db');
+const crypto = require('crypto');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const [orders, trades] = await Promise.all([
+      query('SELECT * FROM orders ORDER BY submitted_ts DESC LIMIT 100'),
+      query('SELECT * FROM trades ORDER BY match_time DESC NULLS LAST LIMIT 100'),
+    ]);
+    return NextResponse.json({ orders: orders.rows, trades: trades.rows });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
+
+export async function POST(req) {
+  const body = await req.json();
+
+  if (body.action === 'paper_order') {
+    try {
+      const orderId = `paper_${crypto.randomUUID().substring(0, 12)}`;
+      const { asset_id, condition_id, side, outcome, price, size_usd, prediction_id } = body;
+
+      // Insert order
+      await query(`
+        INSERT INTO orders (order_id, asset_id, condition_id, side, outcome, price, size, size_usd, status, paper_trade, prediction_id)
+        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'filled', TRUE, $9)
+      `, [orderId, asset_id, condition_id, side, outcome, price, size_usd / price, size_usd, prediction_id]);
+
+      // Simulate instant fill (paper trading)
+      const tradeId = `ptrade_${crypto.randomUUID().substring(0, 12)}`;
+      const feeRate = 0; // Paper trades have no fees
+      const feeUsd = 0;
+
+      await query(`
+        INSERT INTO trades (trade_id, order_id, match_time, price, size, size_usd, fee_rate_bps, fee_usd, pnl, paper_trade)
+        VALUES ($1, $2, NOW(), $3, $4, $5, $6, $7, 0, TRUE)
+      `, [tradeId, orderId, price, size_usd / price, size_usd, feeRate, feeUsd]);
+
+      // Update order status
+      await query("UPDATE orders SET status = 'filled', filled_ts = NOW() WHERE order_id = $1", [orderId]);
+
+      // Log bankroll change
+      const lastBal = await query('SELECT balance_after FROM bankroll_ledger ORDER BY ts DESC LIMIT 1');
+      const currentBal = parseFloat(lastBal.rows[0]?.balance_after || 50);
+      const newBal = currentBal - size_usd; // Debit for position entry
+
+      await query(`
+        INSERT INTO bankroll_ledger (event_type, amount, balance_after, trade_id, note)
+        VALUES ('trade_pnl', $1, $2, $3, $4)
+      `, [-size_usd, newBal, tradeId, `Paper ${side} ${outcome} @ ${price}`]);
+
+      // Update risk state
+      await query('UPDATE risk_state SET open_exposure = open_exposure + $1, updated_at = NOW() WHERE id = (SELECT id FROM risk_state ORDER BY id DESC LIMIT 1)', [size_usd]);
+
+      return NextResponse.json({
+        order_id: orderId,
+        trade_id: tradeId,
+        status: 'filled',
+        paper: true,
+        size_usd,
+        price,
+      });
+    } catch (err) {
+      return NextResponse.json({ error: err.message }, { status: 500 });
+    }
+  }
+
+  if (body.action === 'settle') {
+    // Settle a paper trade (mark outcome)
+    try {
+      const { trade_id, outcome_yes } = body;
+      const tradeRes = await query('SELECT t.*, o.outcome, o.price as order_price, o.size_usd FROM trades t JOIN orders o ON t.order_id = o.order_id WHERE t.trade_id = $1', [trade_id]);
+      if (!tradeRes.rows[0]) return NextResponse.json({ error: 'Trade not found' }, { status: 404 });
+
+      const trade = tradeRes.rows[0];
+      const won = (trade.outcome === 'YES' && outcome_yes) || (trade.outcome === 'NO' && !outcome_yes);
+      const pnl = won ? parseFloat(trade.size_usd) * (1 / parseFloat(trade.order_price) - 1) : -parseFloat(trade.size_usd);
+
+      await query('UPDATE trades SET pnl = $1 WHERE trade_id = $2', [pnl, trade_id]);
+
+      // Update bankroll
+      const lastBal = await query('SELECT balance_after FROM bankroll_ledger ORDER BY ts DESC LIMIT 1');
+      const currentBal = parseFloat(lastBal.rows[0]?.balance_after || 50);
+      const returnAmount = won ? parseFloat(trade.size_usd) + pnl : 0;
+
+      await query(`INSERT INTO bankroll_ledger (event_type, amount, balance_after, trade_id, note) VALUES ('trade_pnl', $1, $2, $3, $4)`,
+        [returnAmount, currentBal + returnAmount, trade_id, `Settlement: ${won ? 'WIN' : 'LOSS'} ${pnl >= 0 ? '+' : ''}$${pnl.toFixed(4)}`]);
+
+      return NextResponse.json({ settled: true, trade_id, won, pnl });
+    } catch (err) {
+      return NextResponse.json({ error: err.message }, { status: 500 });
+    }
+  }
+
+  return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
+}
diff --git a/src/app/api/weather/route.js b/src/app/api/weather/route.js
new file mode 100644
index 0000000..b7855e0
--- /dev/null
+++ b/src/app/api/weather/route.js
@@ -0,0 +1,120 @@
+import { NextResponse } from 'next/server';
+const { query } = require('../../../lib/db');
+const { getGridPoint, getForecast, getHourlyForecast, getCityCoords, geomHash, extractForecastFeatures, CITY_COORDS } = require('../../../lib/weather');
+
+export const dynamic = 'force-dynamic';
+
+export async function GET() {
+  try {
+    const res = await query('SELECT * FROM forecast_runs ORDER BY run_ts DESC LIMIT 200');
+    return NextResponse.json({ forecasts: res.rows });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
+
+async function fetchCityForecast(cityName) {
+  const coords = getCityCoords(cityName);
+  if (!coords) return { error: `Unknown city: ${cityName}` };
+
+  try {
+    // Step 1: Get grid mapping
+    const point = await getGridPoint(coords.lat, coords.lon);
+    const props = point.properties;
+    const office = props.gridId;
+    const gridX = props.gridX;
+    const gridY = props.gridY;
+
+    // Step 2: Get forecast
+    const forecast = await getForecast(office, gridX, gridY);
+    const periods = forecast?.properties?.periods || [];
+    const gHash = geomHash(coords.lat, coords.lon);
+    const now = new Date();
+
+    let stored = 0;
+    for (const period of periods) {
+      const temp = period.temperature;
+      const windMatch = period.windSpeed?.match(/(\d+)/);
+      const windSpeed = windMatch ? parseInt(windMatch[1]) : null;
+      const precipProb = period.probabilityOfPrecipitation?.value || 0;
+      const startTime = new Date(period.startTime);
+      const leadHours = Math.round((startTime - now) / 3600000);
+
+      // Store temperature
+      if (temp != null) {
+        await query(`
+          INSERT INTO forecast_runs (source, run_ts, geom_hash, location_name, lat, lon, lead_hours, variable, value_mean, value_min, value_max, raw_meta)
+          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9, $9, $10)
+          ON CONFLICT (source, run_ts, geom_hash, lead_hours, variable) DO UPDATE SET
+            value_mean = EXCLUDED.value_mean, raw_meta = EXCLUDED.raw_meta
+        `, ['nws', now, gHash, cityName, coords.lat, coords.lon, leadHours, 'temperature', temp, period]);
+        stored++;
+      }
+
+      // Store wind
+      if (windSpeed != null) {
+        await query(`
+          INSERT INTO forecast_runs (source, run_ts, geom_hash, location_name, lat, lon, lead_hours, variable, value_mean, value_min, value_max, raw_meta)
+          VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9, $9, $10)
+          ON CONFLICT (source, run_ts, geom_hash, lead_hours, variable) DO UPDATE SET
+            value_mean = EXCLUDED.value_mean, raw_meta = EXCLUDED.raw_meta
+        `, ['nws', now, gHash, cityName, coords.lat, coords.lon, leadHours, 'wind_speed', windSpeed, period]);
+        stored++;
+      }
+
+      // Store precip probability
+      await query(`
+        INSERT INTO forecast_runs (source, run_ts, geom_hash, location_name, lat, lon, lead_hours, variable, value_mean, value_min, value_max, raw_meta)
+        VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9, $9, $10)
+        ON CONFLICT (source, run_ts, geom_hash, lead_hours, variable) DO UPDATE SET
+          value_mean = EXCLUDED.value_mean, raw_meta = EXCLUDED.raw_meta
+      `, ['nws', now, gHash, cityName, coords.lat, coords.lon, leadHours, 'precip_probability', precipProb, period]);
+      stored++;
+    }
+
+    return { city: cityName, periods: periods.length, stored, office, gridX, gridY };
+  } catch (err) {
+    return { city: cityName, error: err.message };
+  }
+}
+
+export async function POST(req) {
+  const body = await req.json();
+
+  if (body.action === 'test_nws') {
+    try {
+      const point = await getGridPoint(40.7128, -74.0060); // NYC
+      return NextResponse.json({ success: true, office: point.properties?.gridId });
+    } catch (err) {
+      return NextResponse.json({ success: false, error: err.message });
+    }
+  }
+
+  if (body.action === 'fetch') {
+    const result = await fetchCityForecast(body.city || 'New York');
+    if (result.error) return NextResponse.json({ error: result.error }, { status: 400 });
+
+    await query(`INSERT INTO alerts_log (alert_type, severity, message, details) VALUES ($1, $2, $3, $4)`,
+      ['weather_fetch', 'info', `Fetched ${result.periods} forecast periods for ${result.city}`, result]);
+
+    return NextResponse.json(result);
+  }
+
+  if (body.action === 'fetch_all') {
+    const cities = Object.keys(CITY_COORDS);
+    const results = [];
+    for (const city of cities) {
+      const result = await fetchCityForecast(city);
+      results.push(result);
+      await new Promise(r => setTimeout(r, 300)); // rate limit NWS
+    }
+
+    const success = results.filter(r => !r.error);
+    await query(`INSERT INTO alerts_log (alert_type, severity, message, details) VALUES ($1, $2, $3, $4)`,
+      ['weather_fetch_all', 'info', `Fetched forecasts for ${success.length}/${cities.length} cities`, { results: results.map(r => ({ city: r.city, ok: !r.error, periods: r.periods })) }]);
+
+    return NextResponse.json({ cities_fetched: success.length, total: cities.length, results });
+  }
+
+  return NextResponse.json({ error: 'Unknown action' }, { status: 400 });
+}
diff --git a/src/app/api/workers/route.js b/src/app/api/workers/route.js
new file mode 100644
index 0000000..5f8bfa3
--- /dev/null
+++ b/src/app/api/workers/route.js
@@ -0,0 +1,31 @@
+import { NextResponse } from 'next/server';
+
+export const dynamic = 'force-dynamic';
+
+export async function POST(req) {
+  const body = await req.json();
+  const baseUrl = 'http://127.0.0.1:7800';
+
+  const workerMap = {
+    market_ingest: { url: `${baseUrl}/api/markets`, body: { action: 'ingest' } },
+    weather_fetch: { url: `${baseUrl}/api/weather`, body: { action: 'fetch_all' } },
+    run_predictions: { url: `${baseUrl}/api/models`, body: { action: 'run_predictions' } },
+    risk_scan: { url: `${baseUrl}/api/risk`, body: { action: 'evaluate', pYes: 0.5, marketMid: 0.5, spread: 0.05, liquidity: 1000 } },
+    paper_trade: { url: `${baseUrl}/api/trades`, body: { action: 'paper_cycle' } },
+  };
+
+  const worker = workerMap[body.worker];
+  if (!worker) return NextResponse.json({ error: `Unknown worker: ${body.worker}` }, { status: 400 });
+
+  try {
+    const res = await fetch(worker.url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(worker.body),
+    });
+    const data = await res.json();
+    return NextResponse.json({ message: `Worker ${body.worker} completed`, result: data });
+  } catch (err) {
+    return NextResponse.json({ error: err.message }, { status: 500 });
+  }
+}
diff --git a/src/app/dashboard/bankroll/page.js b/src/app/dashboard/bankroll/page.js
new file mode 100644
index 0000000..65061ba
--- /dev/null
+++ b/src/app/dashboard/bankroll/page.js
@@ -0,0 +1,104 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+export default function BankrollPage() {
+  const [ledger, setLedger] = useState([]);
+  const [summary, setSummary] = useState({});
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    async function load() {
+      try {
+        const res = await fetch('/api/bankroll');
+        const data = await res.json();
+        setLedger(data.ledger || []);
+        setSummary(data.summary || {});
+      } catch (err) { console.error(err); }
+      setLoading(false);
+    }
+    load();
+  }, []);
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  const eventColors = {
+    deposit: 'badge-green',
+    withdrawal: 'badge-red',
+    trade_pnl: 'badge-blue',
+    fee: 'badge-yellow',
+    adjustment: 'badge-purple'
+  };
+
+  return (
+    <div className="space-y-6">
+      <div>
+        <h1 className="text-2xl font-bold">Bankroll</h1>
+        <p className="text-gray-500 text-sm">Ledger tracking every balance change</p>
+      </div>
+
+      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Current Balance</p>
+          <p className="stat-value text-green-400">${parseFloat(summary.current_balance || 50).toFixed(2)}</p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Total Deposits</p>
+          <p className="stat-value text-blue-400">${parseFloat(summary.total_deposits || 50).toFixed(2)}</p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Total P&L</p>
+          <p className={`stat-value ${parseFloat(summary.total_pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
+            ${parseFloat(summary.total_pnl || 0).toFixed(2)}
+          </p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Total Fees</p>
+          <p className="stat-value text-yellow-400">${parseFloat(summary.total_fees || 0).toFixed(2)}</p>
+        </div>
+      </div>
+
+      {/* Bankroll Chart Placeholder */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Balance History</h2>
+        {ledger.length > 1 ? (
+          <div className="h-48 flex items-end gap-1">
+            {ledger.slice(-60).map((entry, i) => {
+              const bal = parseFloat(entry.balance_after);
+              const maxBal = Math.max(...ledger.slice(-60).map(e => parseFloat(e.balance_after)));
+              const minBal = Math.min(...ledger.slice(-60).map(e => parseFloat(e.balance_after)));
+              const range = maxBal - minBal || 1;
+              const height = ((bal - minBal) / range) * 160 + 20;
+              return (
+                <div key={i} className="flex-1 rounded-t transition-all hover:opacity-80" title={`$${bal.toFixed(2)} - ${entry.event_type}`}
+                  style={{ height: `${height}px`, background: bal >= 50 ? 'linear-gradient(to top, #10b981, #059669)' : 'linear-gradient(to top, #ef4444, #dc2626)', minWidth: '3px' }} />
+              );
+            })}
+          </div>
+        ) : (
+          <p className="text-gray-500 text-sm text-center py-8">Balance chart will appear after trades begin.</p>
+        )}
+      </div>
+
+      {/* Ledger Table */}
+      <div className="card overflow-x-auto">
+        <h2 className="font-semibold mb-3">Ledger Entries</h2>
+        <table>
+          <thead><tr><th>Time</th><th>Event</th><th>Amount</th><th>Balance After</th><th>Note</th></tr></thead>
+          <tbody>
+            {ledger.map((entry, i) => (
+              <tr key={i}>
+                <td className="text-gray-400 text-sm">{new Date(entry.ts).toLocaleString()}</td>
+                <td><span className={`badge ${eventColors[entry.event_type] || 'badge-blue'}`}>{entry.event_type}</span></td>
+                <td className={`font-mono ${parseFloat(entry.amount) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
+                  {parseFloat(entry.amount) >= 0 ? '+' : ''}${parseFloat(entry.amount).toFixed(2)}
+                </td>
+                <td className="font-mono font-medium">${parseFloat(entry.balance_after).toFixed(2)}</td>
+                <td className="text-gray-400 text-sm max-w-[200px] truncate">{entry.note || '-'}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/dashboard/layout.js b/src/app/dashboard/layout.js
new file mode 100644
index 0000000..cef0d5c
--- /dev/null
+++ b/src/app/dashboard/layout.js
@@ -0,0 +1,96 @@
+'use client';
+import { useState, useEffect } from 'react';
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+
+const NAV_ITEMS = [
+  { href: '/dashboard', label: 'Overview', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
+  { href: '/dashboard/markets', label: 'Markets', icon: 'M13 7h8m0 0v8m0-8l-8 8-4-4-6 6' },
+  { href: '/dashboard/weather', label: 'Weather', icon: 'M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z' },
+  { href: '/dashboard/models', label: 'Models', icon: 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z' },
+  { href: '/dashboard/risk', label: 'Risk', icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z' },
+  { href: '/dashboard/trades', label: 'Trades', icon: 'M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4' },
+  { href: '/dashboard/bankroll', label: 'Bankroll', icon: 'M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z' },
+  { href: '/dashboard/settings', label: 'Settings', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z' },
+];
+
+export default function DashboardLayout({ children }) {
+  const pathname = usePathname();
+  const [collapsed, setCollapsed] = useState(false);
+  const [health, setHealth] = useState(null);
+
+  useEffect(() => {
+    fetch('/api/health').then(r => r.json()).then(d => {
+      if (!d.authenticated) window.location.href = '/';
+      setHealth(d);
+    }).catch(() => window.location.href = '/');
+
+    const interval = setInterval(() => {
+      fetch('/api/health').then(r => r.json()).then(setHealth).catch(() => {});
+    }, 30000);
+    return () => clearInterval(interval);
+  }, []);
+
+  return (
+    <div className="flex min-h-screen">
+      {/* Sidebar */}
+      <aside className={`${collapsed ? 'w-16' : 'w-56'} bg-gray-900/50 border-r border-gray-800 flex flex-col transition-all duration-200`}>
+        <div className="p-4 border-b border-gray-800 flex items-center gap-3">
+          <div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0" style={{ background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)' }}>
+            <span className="text-white font-bold text-sm">B</span>
+          </div>
+          {!collapsed && <span className="font-bold text-sm bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">BERTHA</span>}
+        </div>
+
+        <nav className="flex-1 p-2 space-y-0.5">
+          {NAV_ITEMS.map(item => {
+            const active = pathname === item.href || (item.href !== '/dashboard' && pathname?.startsWith(item.href));
+            return (
+              <Link key={item.href} href={item.href}
+                className={`flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all ${
+                  active ? 'bg-blue-500/15 text-blue-400 font-medium' : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/50'
+                }`}>
+                <svg className="w-4.5 h-4.5 flex-shrink-0" style={{width:'18px',height:'18px'}} fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
+                  <path strokeLinecap="round" strokeLinejoin="round" d={item.icon} />
+                </svg>
+                {!collapsed && <span>{item.label}</span>}
+              </Link>
+            );
+          })}
+        </nav>
+
+        <div className="p-3 border-t border-gray-800">
+          {!collapsed && health && (
+            <div className="space-y-1.5 text-[11px]">
+              <div className="flex justify-between">
+                <span className="text-gray-500">Mode</span>
+                <span className={health.paper_mode ? 'text-yellow-400' : 'text-green-400'}>
+                  {health.paper_mode ? 'PAPER' : 'LIVE'}
+                </span>
+              </div>
+              <div className="flex justify-between">
+                <span className="text-gray-500">Kill Switch</span>
+                <span className={health.kill_switch ? 'text-red-400' : 'text-green-400'}>
+                  {health.kill_switch ? 'ACTIVE' : 'OFF'}
+                </span>
+              </div>
+            </div>
+          )}
+          <button onClick={() => setCollapsed(!collapsed)}
+            className="w-full mt-2 flex items-center justify-center py-1.5 text-gray-500 hover:text-gray-300 transition">
+            <svg className={`w-4 h-4 transition-transform ${collapsed ? '' : 'rotate-180'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
+            </svg>
+          </button>
+        </div>
+      </aside>
+
+      {/* Main content */}
+      <main className="flex-1 overflow-auto">
+        <div className="p-6 max-w-[1600px] mx-auto">
+          {children}
+        </div>
+      </main>
+    </div>
+  );
+}
diff --git a/src/app/dashboard/markets/page.js b/src/app/dashboard/markets/page.js
new file mode 100644
index 0000000..0bfbc51
--- /dev/null
+++ b/src/app/dashboard/markets/page.js
@@ -0,0 +1,101 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+export default function MarketsPage() {
+  const [markets, setMarkets] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [ingesting, setIngesting] = useState(false);
+
+  async function loadMarkets() {
+    try {
+      const res = await fetch('/api/markets');
+      const data = await res.json();
+      setMarkets(data.markets || []);
+    } catch (err) { console.error(err); }
+    setLoading(false);
+  }
+
+  useEffect(() => { loadMarkets(); }, []);
+
+  async function ingestMarkets() {
+    setIngesting(true);
+    try {
+      const res = await fetch('/api/markets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'ingest' }) });
+      const data = await res.json();
+      alert(`Ingested ${data.ingested || 0} weather markets`);
+      loadMarkets();
+    } catch (err) { alert('Ingest error: ' + err.message); }
+    setIngesting(false);
+  }
+
+  async function parseMarket(conditionId) {
+    try {
+      const res = await fetch('/api/markets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'parse', condition_id: conditionId }) });
+      const data = await res.json();
+      alert(JSON.stringify(data.parsed, null, 2));
+      loadMarkets();
+    } catch (err) { alert('Parse error: ' + err.message); }
+  }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-2xl font-bold">Markets</h1>
+          <p className="text-gray-500 text-sm">Polymarket weather-related markets</p>
+        </div>
+        <button onClick={ingestMarkets} disabled={ingesting} className="btn btn-primary">
+          {ingesting ? 'Ingesting...' : 'Ingest Markets'}
+        </button>
+      </div>
+
+      <div className="grid grid-cols-3 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Total Markets</p><p className="stat-value text-blue-400">{markets.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Active</p><p className="stat-value text-green-400">{markets.filter(m => m.active).length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">With Predictions</p><p className="stat-value text-purple-400">{markets.filter(m => m.prediction_count > 0).length}</p></div>
+      </div>
+
+      <div className="card overflow-x-auto">
+        <table>
+          <thead>
+            <tr>
+              <th>Question</th>
+              <th>Category</th>
+              <th>Liquidity</th>
+              <th>End Date</th>
+              <th>Parsed</th>
+              <th>Actions</th>
+            </tr>
+          </thead>
+          <tbody>
+            {markets.length === 0 ? (
+              <tr><td colSpan={6} className="text-center text-gray-500 py-8">No markets yet. Click "Ingest Markets" to scan Polymarket.</td></tr>
+            ) : markets.map(m => (
+              <tr key={m.condition_id}>
+                <td className="max-w-[300px]">
+                  <p className="truncate font-medium">{m.question}</p>
+                  <p className="text-xs text-gray-500 truncate">{m.condition_id}</p>
+                </td>
+                <td><span className="badge badge-blue">{m.category || 'weather'}</span></td>
+                <td className="font-mono">${parseFloat(m.liquidity || 0).toFixed(0)}</td>
+                <td className="text-gray-400 text-sm">{m.end_ts ? new Date(m.end_ts).toLocaleDateString() : '-'}</td>
+                <td>
+                  {m.parsed_event ? (
+                    <span className="badge badge-green">Yes</span>
+                  ) : (
+                    <span className="badge badge-yellow">No</span>
+                  )}
+                </td>
+                <td>
+                  <button onClick={() => parseMarket(m.condition_id)} className="text-xs text-blue-400 hover:text-blue-300">Parse</button>
+                </td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/dashboard/models/page.js b/src/app/dashboard/models/page.js
new file mode 100644
index 0000000..178811f
--- /dev/null
+++ b/src/app/dashboard/models/page.js
@@ -0,0 +1,96 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+export default function ModelsPage() {
+  const [models, setModels] = useState([]);
+  const [predictions, setPredictions] = useState([]);
+  const [calibration, setCalibration] = useState([]);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    async function load() {
+      try {
+        const res = await fetch('/api/models');
+        const data = await res.json();
+        setModels(data.models || []);
+        setPredictions(data.predictions || []);
+        setCalibration(data.calibration || []);
+      } catch (err) { console.error(err); }
+      setLoading(false);
+    }
+    load();
+  }, []);
+
+  async function runPredictions() {
+    try {
+      const res = await fetch('/api/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'run_predictions' }) });
+      const data = await res.json();
+      alert(`Generated ${data.predictions_made || 0} predictions`);
+      window.location.reload();
+    } catch (err) { alert('Error: ' + err.message); }
+  }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-2xl font-bold">Models & Predictions</h1>
+          <p className="text-gray-500 text-sm">Probabilistic forecasting and calibration</p>
+        </div>
+        <button onClick={runPredictions} className="btn btn-primary">Run Predictions</button>
+      </div>
+
+      <div className="grid grid-cols-4 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Model Versions</p><p className="stat-value text-blue-400">{models.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Total Predictions</p><p className="stat-value text-purple-400">{predictions.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Avg Brier Score</p><p className="stat-value text-green-400">{calibration.length > 0 ? (calibration.reduce((a, c) => a + (c.brier_score || 0), 0) / calibration.length).toFixed(4) : 'N/A'}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Active Models</p><p className="stat-value text-yellow-400">{models.filter(m => m.active).length}</p></div>
+      </div>
+
+      {/* Model Versions */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Model Versions</h2>
+        <table>
+          <thead><tr><th>ID</th><th>Type</th><th>Version</th><th>Status</th><th>Metrics</th><th>Created</th></tr></thead>
+          <tbody>
+            {models.map(m => (
+              <tr key={m.id}>
+                <td className="font-mono">#{m.id}</td>
+                <td className="font-medium">{m.model_type}</td>
+                <td>{m.version_tag}</td>
+                <td><span className={`badge ${m.active ? 'badge-green' : 'badge-yellow'}`}>{m.active ? 'Active' : 'Inactive'}</span></td>
+                <td className="text-xs text-gray-400 font-mono">{JSON.stringify(m.metrics || {}).substring(0, 40)}</td>
+                <td className="text-gray-400 text-sm">{new Date(m.created_at).toLocaleDateString()}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+
+      {/* Predictions */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Recent Predictions</h2>
+        <table>
+          <thead><tr><th>Market</th><th>P(Yes)</th><th>Confidence</th><th>Edge</th><th>Mid</th><th>Signal</th><th>Time</th></tr></thead>
+          <tbody>
+            {predictions.length === 0 ? (
+              <tr><td colSpan={7} className="text-center text-gray-500 py-8">No predictions yet. Ingest markets and forecasts first.</td></tr>
+            ) : predictions.map((p, i) => (
+              <tr key={i}>
+                <td className="font-mono text-xs">{p.condition_id?.substring(0, 16)}...</td>
+                <td className="font-mono text-green-400">{(p.p_yes * 100).toFixed(1)}%</td>
+                <td className="font-mono">{p.confidence ? (p.confidence * 100).toFixed(0) + '%' : '-'}</td>
+                <td className={`font-mono ${(p.edge || 0) > 0 ? 'text-green-400' : 'text-red-400'}`}>{p.edge ? (p.edge * 100).toFixed(2) + '%' : '-'}</td>
+                <td className="font-mono">{p.market_mid ? (p.market_mid * 100).toFixed(1) + '%' : '-'}</td>
+                <td><span className={`badge ${p.recommendation?.startsWith('BUY') ? 'badge-green' : p.recommendation === 'SKIP' ? 'badge-red' : 'badge-yellow'}`}>{p.recommendation || '-'}</span></td>
+                <td className="text-gray-400 text-sm">{new Date(p.asof_ts).toLocaleString()}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/dashboard/page.js b/src/app/dashboard/page.js
new file mode 100644
index 0000000..4d88a60
--- /dev/null
+++ b/src/app/dashboard/page.js
@@ -0,0 +1,217 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+function StatCard({ label, value, sub, color, icon }) {
+  return (
+    <div className="card">
+      <div className="flex items-start justify-between">
+        <div>
+          <p className="text-xs text-gray-500 uppercase tracking-wide mb-1">{label}</p>
+          <p className={`stat-value ${color || 'text-white'}`}>{value}</p>
+          {sub && <p className="text-xs text-gray-500 mt-1">{sub}</p>}
+        </div>
+        {icon && <div className="text-gray-600">{icon}</div>}
+      </div>
+    </div>
+  );
+}
+
+export default function DashboardOverview() {
+  const [data, setData] = useState(null);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    async function load() {
+      try {
+        const [dash, risk, bankroll] = await Promise.all([
+          fetch('/api/dashboard').then(r => r.json()),
+          fetch('/api/risk').then(r => r.json()),
+          fetch('/api/bankroll').then(r => r.json()),
+        ]);
+        setData({ dash, risk, bankroll });
+      } catch (err) {
+        console.error('Dashboard load error:', err);
+      }
+      setLoading(false);
+    }
+    load();
+    const interval = setInterval(load, 15000);
+    return () => clearInterval(interval);
+  }, []);
+
+  if (loading) return (
+    <div className="flex items-center justify-center h-64">
+      <div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" />
+    </div>
+  );
+
+  const d = data?.dash || {};
+  const r = data?.risk?.state || {};
+  const config = r.config || {};
+  const b = data?.bankroll || {};
+
+  return (
+    <div className="space-y-6">
+      {/* Header */}
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-2xl font-bold">Dashboard</h1>
+          <p className="text-gray-500 text-sm mt-0.5">Weather micro-betting system overview</p>
+        </div>
+        <div className="flex items-center gap-3">
+          <span className={`badge ${config.paper_mode !== false ? 'badge-yellow' : 'badge-green'}`}>
+            {config.paper_mode !== false ? 'PAPER MODE' : 'LIVE MODE'}
+          </span>
+          {r.kill_switch_active && (
+            <span className="badge badge-red">KILL SWITCH ON</span>
+          )}
+          <span className={`w-2.5 h-2.5 rounded-full ${r.kill_switch_active ? 'bg-red-500' : 'bg-green-500 pulse-green'}`} />
+        </div>
+      </div>
+
+      {/* Stats Grid */}
+      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <StatCard label="Bankroll" value={`$${parseFloat(r.bankroll || 50).toFixed(2)}`} color="text-green-400" sub="Starting: $50.00" />
+        <StatCard label="Daily P&L" value={`$${parseFloat(r.daily_pnl || 0).toFixed(2)}`} color={parseFloat(r.daily_pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'} sub={`Limit: -$${config.max_daily_loss_usd || 5}`} />
+        <StatCard label="Open Exposure" value={`$${parseFloat(r.open_exposure || 0).toFixed(2)}`} color="text-blue-400" sub={`Max: $${config.max_open_exposure_usd || 5}`} />
+        <StatCard label="Max Drawdown" value={`${parseFloat(r.max_drawdown_pct || 0).toFixed(1)}%`} color="text-purple-400" sub={`Limit: ${config.max_drawdown_pct || 20}%`} />
+      </div>
+
+      {/* Trading + Markets Row */}
+      <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
+        {/* Recent Predictions */}
+        <div className="card">
+          <h2 className="font-semibold mb-3 flex items-center gap-2">
+            <svg className="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
+            </svg>
+            Recent Predictions
+          </h2>
+          {d.recent_predictions?.length > 0 ? (
+            <table>
+              <thead>
+                <tr>
+                  <th>Market</th>
+                  <th>P(Yes)</th>
+                  <th>Mid</th>
+                  <th>Edge</th>
+                  <th>Signal</th>
+                </tr>
+              </thead>
+              <tbody>
+                {d.recent_predictions.map((p, i) => (
+                  <tr key={i}>
+                    <td className="max-w-[180px] truncate">{p.condition_id?.substring(0, 12)}...</td>
+                    <td className="font-mono">{(p.p_yes * 100).toFixed(1)}%</td>
+                    <td className="font-mono">{p.market_mid ? (p.market_mid * 100).toFixed(1) + '%' : '-'}</td>
+                    <td className={`font-mono ${p.edge > 0 ? 'text-green-400' : 'text-red-400'}`}>
+                      {p.edge ? (p.edge * 100).toFixed(1) + '%' : '-'}
+                    </td>
+                    <td>
+                      <span className={`badge ${p.recommendation === 'BUY_YES' || p.recommendation === 'BUY_NO' ? 'badge-green' : 'badge-yellow'}`}>
+                        {p.recommendation || 'HOLD'}
+                      </span>
+                    </td>
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+          ) : (
+            <p className="text-gray-500 text-sm">No predictions yet. Ingest markets to begin.</p>
+          )}
+        </div>
+
+        {/* Recent Trades */}
+        <div className="card">
+          <h2 className="font-semibold mb-3 flex items-center gap-2">
+            <svg className="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+              <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
+            </svg>
+            Recent Trades
+          </h2>
+          {d.recent_trades?.length > 0 ? (
+            <table>
+              <thead>
+                <tr>
+                  <th>Time</th>
+                  <th>Side</th>
+                  <th>Price</th>
+                  <th>Size</th>
+                  <th>P&L</th>
+                </tr>
+              </thead>
+              <tbody>
+                {d.recent_trades.map((t, i) => (
+                  <tr key={i}>
+                    <td className="text-gray-400">{new Date(t.match_time).toLocaleString()}</td>
+                    <td><span className={`badge ${t.side === 'buy' ? 'badge-green' : 'badge-red'}`}>{t.side?.toUpperCase()}</span></td>
+                    <td className="font-mono">${parseFloat(t.price).toFixed(4)}</td>
+                    <td className="font-mono">${parseFloat(t.size_usd || 0).toFixed(2)}</td>
+                    <td className={`font-mono ${parseFloat(t.pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>
+                      ${parseFloat(t.pnl || 0).toFixed(2)}
+                    </td>
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+          ) : (
+            <p className="text-gray-500 text-sm">No trades yet. System is in paper mode.</p>
+          )}
+        </div>
+      </div>
+
+      {/* System Info Row */}
+      <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
+        <div className="card">
+          <h3 className="text-xs text-gray-500 uppercase mb-2">Markets Tracked</h3>
+          <p className="stat-value text-blue-400">{d.market_count || 0}</p>
+          <p className="text-xs text-gray-500 mt-1">{d.weather_market_count || 0} weather markets</p>
+        </div>
+        <div className="card">
+          <h3 className="text-xs text-gray-500 uppercase mb-2">Forecast Runs</h3>
+          <p className="stat-value text-purple-400">{d.forecast_count || 0}</p>
+          <p className="text-xs text-gray-500 mt-1">NWS + GEFS sources</p>
+        </div>
+        <div className="card">
+          <h3 className="text-xs text-gray-500 uppercase mb-2">Risk Config</h3>
+          <div className="space-y-1 text-sm">
+            <div className="flex justify-between"><span className="text-gray-500">Max Bet</span><span>${config.max_bet_usd || 0.50}</span></div>
+            <div className="flex justify-between"><span className="text-gray-500">Min Edge</span><span>{((config.min_edge_threshold || 0.03) * 100).toFixed(0)}%</span></div>
+            <div className="flex justify-between"><span className="text-gray-500">Kelly Fraction</span><span>{config.fractional_kelly || 0.10}x</span></div>
+          </div>
+        </div>
+      </div>
+
+      {/* Alerts */}
+      <div className="card">
+        <h2 className="font-semibold mb-3 flex items-center gap-2">
+          <svg className="w-4 h-4 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
+          </svg>
+          System Alerts
+        </h2>
+        {d.recent_alerts?.length > 0 ? (
+          <div className="space-y-2">
+            {d.recent_alerts.map((a, i) => (
+              <div key={i} className={`flex items-start gap-3 p-3 rounded-lg ${
+                a.severity === 'critical' ? 'bg-red-500/10' :
+                a.severity === 'warning' ? 'bg-yellow-500/10' : 'bg-blue-500/10'
+              }`}>
+                <span className={`badge ${
+                  a.severity === 'critical' ? 'badge-red' :
+                  a.severity === 'warning' ? 'badge-yellow' : 'badge-blue'
+                }`}>{a.severity}</span>
+                <div>
+                  <p className="text-sm">{a.message}</p>
+                  <p className="text-xs text-gray-500 mt-0.5">{new Date(a.ts).toLocaleString()}</p>
+                </div>
+              </div>
+            ))}
+          </div>
+        ) : (
+          <p className="text-gray-500 text-sm">No alerts. System is healthy.</p>
+        )}
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/dashboard/risk/page.js b/src/app/dashboard/risk/page.js
new file mode 100644
index 0000000..11e2b42
--- /dev/null
+++ b/src/app/dashboard/risk/page.js
@@ -0,0 +1,141 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+export default function RiskPage() {
+  const [risk, setRisk] = useState(null);
+  const [loading, setLoading] = useState(true);
+
+  async function loadRisk() {
+    try {
+      const res = await fetch('/api/risk');
+      const data = await res.json();
+      setRisk(data);
+    } catch (err) { console.error(err); }
+    setLoading(false);
+  }
+
+  useEffect(() => { loadRisk(); const i = setInterval(loadRisk, 10000); return () => clearInterval(i); }, []);
+
+  async function toggleKillSwitch() {
+    const action = risk?.state?.kill_switch_active ? 'deactivate' : 'activate';
+    const reason = action === 'activate' ? prompt('Kill switch reason:') : null;
+    if (action === 'activate' && !reason) return;
+    try {
+      await fetch('/api/risk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: `kill_switch_${action}`, reason }) });
+      loadRisk();
+    } catch (err) { alert('Error: ' + err.message); }
+  }
+
+  async function updateConfig(key, value) {
+    try {
+      const config = { ...risk.state.config, [key]: value };
+      await fetch('/api/risk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update_config', config }) });
+      loadRisk();
+    } catch (err) { alert('Error: ' + err.message); }
+  }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  const s = risk?.state || {};
+  const c = s.config || {};
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-2xl font-bold">Risk Management</h1>
+          <p className="text-gray-500 text-sm">Exposure limits, kill switches, and sizing controls</p>
+        </div>
+        <button onClick={toggleKillSwitch} className={`btn ${s.kill_switch_active ? 'btn-success' : 'btn-danger'}`}>
+          {s.kill_switch_active ? 'Deactivate Kill Switch' : 'Activate Kill Switch'}
+        </button>
+      </div>
+
+      {s.kill_switch_active && (
+        <div className="bg-red-500/10 border border-red-500/30 rounded-xl p-4 flex items-center gap-3">
+          <span className="text-2xl">&#128680;</span>
+          <div>
+            <p className="font-semibold text-red-400">KILL SWITCH ACTIVE</p>
+            <p className="text-sm text-gray-400">{s.kill_reason || 'No reason specified'}</p>
+          </div>
+        </div>
+      )}
+
+      <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Bankroll</p>
+          <p className="stat-value text-green-400">${parseFloat(s.bankroll || 50).toFixed(2)}</p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Daily P&L</p>
+          <p className={`stat-value ${parseFloat(s.daily_pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>${parseFloat(s.daily_pnl || 0).toFixed(2)}</p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Open Exposure</p>
+          <p className="stat-value text-blue-400">${parseFloat(s.open_exposure || 0).toFixed(2)}</p>
+        </div>
+        <div className="card">
+          <p className="text-xs text-gray-500 uppercase mb-1">Max Drawdown</p>
+          <p className="stat-value text-purple-400">{parseFloat(s.max_drawdown_pct || 0).toFixed(1)}%</p>
+        </div>
+      </div>
+
+      {/* Risk Configuration */}
+      <div className="card">
+        <h2 className="font-semibold mb-4">Risk Configuration</h2>
+        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
+          {[
+            { key: 'paper_mode', label: 'Paper Trading Mode', type: 'toggle', desc: 'When ON, no real orders are placed' },
+            { key: 'max_bet_usd', label: 'Max Bet Size ($)', type: 'number', step: 0.05 },
+            { key: 'max_daily_loss_usd', label: 'Max Daily Loss ($)', type: 'number', step: 0.50 },
+            { key: 'max_daily_loss_pct', label: 'Max Daily Loss (%)', type: 'number', step: 1 },
+            { key: 'max_open_exposure_usd', label: 'Max Open Exposure ($)', type: 'number', step: 0.50 },
+            { key: 'max_drawdown_pct', label: 'Max Drawdown (%)', type: 'number', step: 1 },
+            { key: 'min_edge_threshold', label: 'Min Edge Threshold', type: 'number', step: 0.005 },
+            { key: 'min_liquidity', label: 'Min Market Liquidity ($)', type: 'number', step: 10 },
+            { key: 'fractional_kelly', label: 'Kelly Fraction', type: 'number', step: 0.05 },
+          ].map(item => (
+            <div key={item.key} className="flex items-center justify-between bg-gray-800/30 rounded-lg p-3">
+              <div>
+                <p className="text-sm font-medium">{item.label}</p>
+                {item.desc && <p className="text-xs text-gray-500">{item.desc}</p>}
+              </div>
+              {item.type === 'toggle' ? (
+                <button onClick={() => updateConfig(item.key, !c[item.key])}
+                  className={`w-12 h-6 rounded-full transition-colors ${c[item.key] ? 'bg-green-500' : 'bg-gray-600'} relative`}>
+                  <span className={`absolute top-0.5 w-5 h-5 bg-white rounded-full transition-transform ${c[item.key] ? 'left-6' : 'left-0.5'}`} />
+                </button>
+              ) : (
+                <input type="number" step={item.step} value={c[item.key] ?? ''} onChange={e => updateConfig(item.key, parseFloat(e.target.value))}
+                  className="w-24 bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-right font-mono" />
+              )}
+            </div>
+          ))}
+        </div>
+      </div>
+
+      {/* Edge Gate Simulation */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Edge Gate Simulator</h2>
+        <p className="text-gray-500 text-sm mb-4">Test if a hypothetical trade would pass risk gates</p>
+        <div className="grid grid-cols-4 gap-3">
+          <div><label className="text-xs text-gray-500">P(Yes)</label><input id="sim-pyes" type="number" step="0.01" defaultValue="0.65" className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm font-mono" /></div>
+          <div><label className="text-xs text-gray-500">Market Mid</label><input id="sim-mid" type="number" step="0.01" defaultValue="0.55" className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm font-mono" /></div>
+          <div><label className="text-xs text-gray-500">Spread</label><input id="sim-spread" type="number" step="0.01" defaultValue="0.04" className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm font-mono" /></div>
+          <div><label className="text-xs text-gray-500">Liquidity ($)</label><input id="sim-liq" type="number" step="10" defaultValue="500" className="w-full bg-gray-800 border border-gray-700 rounded px-2 py-1.5 text-sm font-mono" /></div>
+        </div>
+        <button onClick={async () => {
+          const res = await fetch('/api/risk', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
+            action: 'evaluate',
+            pYes: parseFloat(document.getElementById('sim-pyes').value),
+            marketMid: parseFloat(document.getElementById('sim-mid').value),
+            spread: parseFloat(document.getElementById('sim-spread').value),
+            liquidity: parseFloat(document.getElementById('sim-liq').value),
+          })});
+          const data = await res.json();
+          alert(JSON.stringify(data.evaluation, null, 2));
+        }} className="btn btn-primary mt-3">Simulate</button>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/dashboard/settings/page.js b/src/app/dashboard/settings/page.js
new file mode 100644
index 0000000..e5440d8
--- /dev/null
+++ b/src/app/dashboard/settings/page.js
@@ -0,0 +1,130 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+export default function SettingsPage() {
+  const [health, setHealth] = useState(null);
+  const [testResult, setTestResult] = useState(null);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    fetch('/api/health').then(r => r.json()).then(d => { setHealth(d); setLoading(false); }).catch(() => setLoading(false));
+  }, []);
+
+  async function testSlack() {
+    try {
+      const res = await fetch('/api/alerts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'test' }) });
+      const data = await res.json();
+      setTestResult(data);
+    } catch (err) { setTestResult({ error: err.message }); }
+  }
+
+  async function testNWS() {
+    try {
+      const res = await fetch('/api/weather', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'test_nws' }) });
+      const data = await res.json();
+      alert(data.success ? 'NWS API connected successfully!' : 'NWS API error: ' + (data.error || 'Unknown'));
+    } catch (err) { alert('Error: ' + err.message); }
+  }
+
+  async function testPolymarket() {
+    try {
+      const res = await fetch('/api/markets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'test_api' }) });
+      const data = await res.json();
+      alert(data.success ? `Polymarket API connected! Found ${data.market_count} markets.` : 'API error: ' + (data.error || 'Unknown'));
+    } catch (err) { alert('Error: ' + err.message); }
+  }
+
+  async function runWorker(worker) {
+    try {
+      const res = await fetch('/api/workers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'run', worker }) });
+      const data = await res.json();
+      alert(data.message || JSON.stringify(data));
+    } catch (err) { alert('Error: ' + err.message); }
+  }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  return (
+    <div className="space-y-6">
+      <div>
+        <h1 className="text-2xl font-bold">Settings</h1>
+        <p className="text-gray-500 text-sm">System configuration and connectivity tests</p>
+      </div>
+
+      {/* System Status */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">System Status</h2>
+        <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
+          {[
+            { label: 'Database', status: health?.db_connected, detail: 'PostgreSQL' },
+            { label: 'NWS API', status: 'unknown', detail: 'api.weather.gov' },
+            { label: 'Polymarket', status: 'unknown', detail: 'gamma-api.polymarket.com' },
+            { label: 'Slack', status: health?.slack_configured ? 'ok' : 'no_token', detail: 'U03TV3UC2V7' },
+          ].map(s => (
+            <div key={s.label} className="bg-gray-800/30 rounded-lg p-3">
+              <div className="flex items-center gap-2 mb-1">
+                <span className={`w-2 h-2 rounded-full ${s.status === true || s.status === 'ok' ? 'bg-green-500' : s.status === 'unknown' ? 'bg-yellow-500' : 'bg-red-500'}`} />
+                <span className="text-sm font-medium">{s.label}</span>
+              </div>
+              <p className="text-xs text-gray-500">{s.detail}</p>
+            </div>
+          ))}
+        </div>
+      </div>
+
+      {/* Connectivity Tests */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Connectivity Tests</h2>
+        <div className="flex gap-3">
+          <button onClick={testNWS} className="btn btn-primary">Test NWS API</button>
+          <button onClick={testPolymarket} className="btn btn-primary">Test Polymarket</button>
+          <button onClick={testSlack} className="btn btn-primary">Test Slack Alert</button>
+        </div>
+        {testResult && (
+          <pre className="mt-3 bg-gray-900 rounded-lg p-3 text-xs text-gray-300 overflow-auto max-h-48">
+            {JSON.stringify(testResult, null, 2)}
+          </pre>
+        )}
+      </div>
+
+      {/* Manual Worker Triggers */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Manual Worker Triggers</h2>
+        <div className="flex flex-wrap gap-3">
+          <button onClick={() => runWorker('market_ingest')} className="btn btn-primary">Ingest Markets</button>
+          <button onClick={() => runWorker('weather_fetch')} className="btn btn-primary">Fetch Weather</button>
+          <button onClick={() => runWorker('run_predictions')} className="btn btn-primary">Run Predictions</button>
+          <button onClick={() => runWorker('risk_scan')} className="btn btn-primary">Risk Scan</button>
+          <button onClick={() => runWorker('paper_trade')} className="btn btn-primary">Paper Trade Cycle</button>
+        </div>
+      </div>
+
+      {/* Architecture Info */}
+      <div className="card">
+        <h2 className="font-semibold mb-3">Architecture</h2>
+        <div className="grid grid-cols-2 gap-4 text-sm">
+          <div>
+            <p className="text-gray-500 text-xs uppercase mb-2">Data Sources</p>
+            <ul className="space-y-1">
+              <li className="flex items-center gap-2"><span className="badge badge-blue">NWS</span> Live US forecasts (api.weather.gov)</li>
+              <li className="flex items-center gap-2"><span className="badge badge-purple">GEFS</span> Ensemble forecasts (AWS)</li>
+              <li className="flex items-center gap-2"><span className="badge badge-green">Gamma</span> Polymarket markets</li>
+              <li className="flex items-center gap-2"><span className="badge badge-yellow">CLOB</span> Orderbook data</li>
+            </ul>
+          </div>
+          <div>
+            <p className="text-gray-500 text-xs uppercase mb-2">Model Pipeline</p>
+            <ul className="space-y-1">
+              <li>1. Market parsing + event extraction</li>
+              <li>2. Ensemble exceedance probability</li>
+              <li>3. Climatological prior</li>
+              <li>4. Bayesian combination + calibration</li>
+              <li>5. Edge gate + risk sizing</li>
+              <li>6. Paper/live execution</li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/dashboard/trades/page.js b/src/app/dashboard/trades/page.js
new file mode 100644
index 0000000..7f4b6ba
--- /dev/null
+++ b/src/app/dashboard/trades/page.js
@@ -0,0 +1,96 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+export default function TradesPage() {
+  const [orders, setOrders] = useState([]);
+  const [trades, setTrades] = useState([]);
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    async function load() {
+      try {
+        const res = await fetch('/api/trades');
+        const data = await res.json();
+        setOrders(data.orders || []);
+        setTrades(data.trades || []);
+      } catch (err) { console.error(err); }
+      setLoading(false);
+    }
+    load();
+    const i = setInterval(load, 15000);
+    return () => clearInterval(i);
+  }, []);
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  const totalPnl = trades.reduce((s, t) => s + parseFloat(t.pnl || 0), 0);
+  const totalFees = trades.reduce((s, t) => s + parseFloat(t.fee_usd || 0), 0);
+  const winRate = trades.length > 0 ? (trades.filter(t => parseFloat(t.pnl || 0) > 0).length / trades.length * 100) : 0;
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-2xl font-bold">Trades & Orders</h1>
+          <p className="text-gray-500 text-sm">Execution history and paper trade log</p>
+        </div>
+      </div>
+
+      <div className="grid grid-cols-2 md:grid-cols-5 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Orders</p><p className="stat-value text-blue-400">{orders.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Fills</p><p className="stat-value text-green-400">{trades.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Total P&L</p><p className={`stat-value ${totalPnl >= 0 ? 'text-green-400' : 'text-red-400'}`}>${totalPnl.toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Total Fees</p><p className="stat-value text-yellow-400">${totalFees.toFixed(2)}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Win Rate</p><p className="stat-value text-purple-400">{winRate.toFixed(0)}%</p></div>
+      </div>
+
+      {/* Orders */}
+      <div className="card overflow-x-auto">
+        <h2 className="font-semibold mb-3">Orders</h2>
+        <table>
+          <thead><tr><th>ID</th><th>Market</th><th>Side</th><th>Outcome</th><th>Price</th><th>Size ($)</th><th>Status</th><th>Paper</th><th>Time</th></tr></thead>
+          <tbody>
+            {orders.length === 0 ? (
+              <tr><td colSpan={9} className="text-center text-gray-500 py-8">No orders yet</td></tr>
+            ) : orders.map(o => (
+              <tr key={o.order_id}>
+                <td className="font-mono text-xs">{o.order_id.substring(0, 10)}...</td>
+                <td className="font-mono text-xs max-w-[120px] truncate">{o.condition_id?.substring(0, 12)}</td>
+                <td><span className={`badge ${o.side === 'buy' ? 'badge-green' : 'badge-red'}`}>{o.side?.toUpperCase()}</span></td>
+                <td className="font-medium">{o.outcome}</td>
+                <td className="font-mono">${parseFloat(o.price).toFixed(4)}</td>
+                <td className="font-mono">${parseFloat(o.size_usd || 0).toFixed(2)}</td>
+                <td><span className={`badge ${o.status === 'filled' ? 'badge-green' : o.status === 'rejected' ? 'badge-red' : 'badge-yellow'}`}>{o.status}</span></td>
+                <td>{o.paper_trade ? <span className="badge badge-yellow">PAPER</span> : <span className="badge badge-green">LIVE</span>}</td>
+                <td className="text-gray-400 text-sm">{new Date(o.submitted_ts).toLocaleString()}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+
+      {/* Trades */}
+      <div className="card overflow-x-auto">
+        <h2 className="font-semibold mb-3">Trade Fills</h2>
+        <table>
+          <thead><tr><th>ID</th><th>Price</th><th>Size ($)</th><th>Fee</th><th>P&L</th><th>Paper</th><th>Time</th></tr></thead>
+          <tbody>
+            {trades.length === 0 ? (
+              <tr><td colSpan={7} className="text-center text-gray-500 py-8">No trade fills yet</td></tr>
+            ) : trades.map(t => (
+              <tr key={t.trade_id}>
+                <td className="font-mono text-xs">{t.trade_id.substring(0, 10)}...</td>
+                <td className="font-mono">${parseFloat(t.price).toFixed(4)}</td>
+                <td className="font-mono">${parseFloat(t.size_usd || 0).toFixed(2)}</td>
+                <td className="font-mono text-yellow-400">${parseFloat(t.fee_usd || 0).toFixed(4)}</td>
+                <td className={`font-mono ${parseFloat(t.pnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}`}>${parseFloat(t.pnl || 0).toFixed(4)}</td>
+                <td>{t.paper_trade ? <span className="badge badge-yellow">PAPER</span> : <span className="badge badge-green">LIVE</span>}</td>
+                <td className="text-gray-400 text-sm">{t.match_time ? new Date(t.match_time).toLocaleString() : '-'}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      </div>
+    </div>
+  );
+}
diff --git a/src/app/dashboard/weather/page.js b/src/app/dashboard/weather/page.js
new file mode 100644
index 0000000..bcb9267
--- /dev/null
+++ b/src/app/dashboard/weather/page.js
@@ -0,0 +1,124 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+const DEMO_CITIES = ['New York', 'Chicago', 'Los Angeles', 'Miami', 'Denver', 'Seattle'];
+
+export default function WeatherPage() {
+  const [forecasts, setForecasts] = useState([]);
+  const [loading, setLoading] = useState(true);
+  const [fetching, setFetching] = useState(false);
+  const [selectedCity, setSelectedCity] = useState('New York');
+
+  async function loadForecasts() {
+    try {
+      const res = await fetch('/api/weather');
+      const data = await res.json();
+      setForecasts(data.forecasts || []);
+    } catch (err) { console.error(err); }
+    setLoading(false);
+  }
+
+  useEffect(() => { loadForecasts(); }, []);
+
+  async function fetchWeather() {
+    setFetching(true);
+    try {
+      const res = await fetch('/api/weather', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ action: 'fetch', city: selectedCity })
+      });
+      const data = await res.json();
+      if (data.error) alert('Error: ' + data.error);
+      else alert(`Fetched ${data.periods || 0} forecast periods for ${selectedCity}`);
+      loadForecasts();
+    } catch (err) { alert('Fetch error: ' + err.message); }
+    setFetching(false);
+  }
+
+  async function fetchAllCities() {
+    setFetching(true);
+    try {
+      const res = await fetch('/api/weather', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ action: 'fetch_all' })
+      });
+      const data = await res.json();
+      alert(`Fetched forecasts for ${data.cities_fetched || 0} cities`);
+      loadForecasts();
+    } catch (err) { alert('Fetch error: ' + err.message); }
+    setFetching(false);
+  }
+
+  if (loading) return <div className="flex items-center justify-center h-64"><div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
+
+  // Group by location
+  const byLocation = {};
+  forecasts.forEach(f => {
+    const key = f.location_name || f.geom_hash;
+    if (!byLocation[key]) byLocation[key] = [];
+    byLocation[key].push(f);
+  });
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-2xl font-bold">Weather Forecasts</h1>
+          <p className="text-gray-500 text-sm">NWS forecast data for tracked locations</p>
+        </div>
+        <div className="flex items-center gap-2">
+          <select value={selectedCity} onChange={e => setSelectedCity(e.target.value)}
+            className="bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm">
+            {DEMO_CITIES.map(c => <option key={c} value={c}>{c}</option>)}
+          </select>
+          <button onClick={fetchWeather} disabled={fetching} className="btn btn-primary">
+            {fetching ? 'Fetching...' : 'Fetch City'}
+          </button>
+          <button onClick={fetchAllCities} disabled={fetching} className="btn btn-success">
+            Fetch All
+          </button>
+        </div>
+      </div>
+
+      <div className="grid grid-cols-4 gap-4">
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Forecast Runs</p><p className="stat-value text-blue-400">{forecasts.length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Locations</p><p className="stat-value text-green-400">{Object.keys(byLocation).length}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Latest</p><p className="text-sm text-gray-300 mt-1">{forecasts[0]?.run_ts ? new Date(forecasts[0].run_ts).toLocaleString() : 'None'}</p></div>
+        <div className="card"><p className="text-xs text-gray-500 uppercase mb-1">Sources</p><p className="stat-value text-purple-400">{[...new Set(forecasts.map(f => f.source))].length}</p></div>
+      </div>
+
+      {Object.entries(byLocation).map(([location, runs]) => (
+        <div key={location} className="card">
+          <h3 className="font-semibold mb-3">{location}</h3>
+          <table>
+            <thead>
+              <tr><th>Source</th><th>Run Time</th><th>Variable</th><th>Lead (h)</th><th>Mean</th><th>Min</th><th>Max</th><th>Spread</th></tr>
+            </thead>
+            <tbody>
+              {runs.slice(0, 20).map((r, i) => (
+                <tr key={i}>
+                  <td><span className="badge badge-blue">{r.source}</span></td>
+                  <td className="text-gray-400 text-sm">{new Date(r.run_ts).toLocaleString()}</td>
+                  <td className="font-medium">{r.variable}</td>
+                  <td className="font-mono">{r.lead_hours}h</td>
+                  <td className="font-mono text-green-400">{parseFloat(r.value_mean || 0).toFixed(1)}</td>
+                  <td className="font-mono text-blue-400">{parseFloat(r.value_min || 0).toFixed(1)}</td>
+                  <td className="font-mono text-red-400">{parseFloat(r.value_max || 0).toFixed(1)}</td>
+                  <td className="font-mono text-purple-400">{parseFloat(r.ensemble_spread || 0).toFixed(1)}</td>
+                </tr>
+              ))}
+            </tbody>
+          </table>
+        </div>
+      ))}
+
+      {forecasts.length === 0 && (
+        <div className="card text-center py-12">
+          <p className="text-gray-500">No forecast data yet. Click "Fetch City" or "Fetch All" to begin.</p>
+        </div>
+      )}
+    </div>
+  );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
new file mode 100644
index 0000000..d2a4f95
--- /dev/null
+++ b/src/app/globals.css
@@ -0,0 +1,81 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+:root {
+  --bg: #0a0e1a;
+  --card: #111827;
+  --border: #1e293b;
+  --accent: #3b82f6;
+  --green: #10b981;
+  --red: #ef4444;
+  --yellow: #f59e0b;
+  --purple: #8b5cf6;
+}
+
+body {
+  background: var(--bg);
+  color: #e2e8f0;
+  font-family: 'Inter', -apple-system, sans-serif;
+}
+
+::-webkit-scrollbar { width: 6px; height: 6px; }
+::-webkit-scrollbar-track { background: var(--bg); }
+::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
+::-webkit-scrollbar-thumb:hover { background: #4b5563; }
+
+.card {
+  background: var(--card);
+  border: 1px solid var(--border);
+  border-radius: 12px;
+  padding: 1.25rem;
+}
+
+.stat-value {
+  font-size: 1.75rem;
+  font-weight: 700;
+  line-height: 1;
+}
+
+.badge {
+  display: inline-flex;
+  align-items: center;
+  padding: 2px 8px;
+  border-radius: 9999px;
+  font-size: 0.75rem;
+  font-weight: 600;
+}
+
+.badge-green { background: rgba(16, 185, 129, 0.15); color: #10b981; }
+.badge-red { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
+.badge-yellow { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
+.badge-blue { background: rgba(59, 130, 246, 0.15); color: #3b82f6; }
+.badge-purple { background: rgba(139, 92, 246, 0.15); color: #8b5cf6; }
+
+.pulse-green { animation: pulse-green 2s infinite; }
+@keyframes pulse-green {
+  0%, 100% { box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.4); }
+  50% { box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
+}
+
+.btn {
+  padding: 0.5rem 1rem;
+  border-radius: 8px;
+  font-weight: 600;
+  font-size: 0.875rem;
+  transition: all 0.15s;
+  cursor: pointer;
+  border: none;
+}
+
+.btn-primary { background: var(--accent); color: white; }
+.btn-primary:hover { background: #2563eb; }
+.btn-danger { background: var(--red); color: white; }
+.btn-danger:hover { background: #dc2626; }
+.btn-success { background: var(--green); color: white; }
+.btn-success:hover { background: #059669; }
+
+table { width: 100%; border-collapse: collapse; }
+th { text-align: left; padding: 0.75rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; border-bottom: 1px solid var(--border); }
+td { padding: 0.75rem; border-bottom: 1px solid rgba(30, 41, 59, 0.5); font-size: 0.875rem; }
+tr:hover td { background: rgba(59, 130, 246, 0.03); }
diff --git a/src/app/layout.js b/src/app/layout.js
new file mode 100644
index 0000000..a3fa422
--- /dev/null
+++ b/src/app/layout.js
@@ -0,0 +1,18 @@
+import './globals.css';
+
+export const metadata = {
+  title: 'Bertha - Weather Micro-Betting System',
+  description: 'Probabilistic weather forecasting + Polymarket execution',
+};
+
+export default function RootLayout({ children }) {
+  return (
+    <html lang="en">
+      <head>
+        <link rel="preconnect" href="https://fonts.googleapis.com" />
+        <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
+      </head>
+      <body>{children}</body>
+    </html>
+  );
+}
diff --git a/src/app/page.js b/src/app/page.js
new file mode 100644
index 0000000..dbb9695
--- /dev/null
+++ b/src/app/page.js
@@ -0,0 +1,85 @@
+'use client';
+import { useState, useEffect } from 'react';
+
+export default function LoginPage() {
+  const [username, setUsername] = useState('');
+  const [password, setPassword] = useState('');
+  const [error, setError] = useState('');
+  const [loading, setLoading] = useState(true);
+
+  useEffect(() => {
+    fetch('/api/health').then(r => r.json()).then(d => {
+      if (d.authenticated) window.location.href = '/dashboard';
+      else setLoading(false);
+    }).catch(() => setLoading(false));
+  }, []);
+
+  async function handleLogin(e) {
+    e.preventDefault();
+    setError('');
+    const res = await fetch('/api/health', {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ username, password })
+    });
+    const data = await res.json();
+    if (data.token) {
+      document.cookie = `bertha_token=${data.token}; path=/; max-age=${7*86400}`;
+      window.location.href = '/dashboard';
+    } else {
+      setError(data.error || 'Login failed');
+    }
+  }
+
+  if (loading) return (
+    <div className="min-h-screen flex items-center justify-center">
+      <div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full" />
+    </div>
+  );
+
+  return (
+    <div className="min-h-screen flex items-center justify-center p-4">
+      <div className="w-full max-w-md">
+        <div className="text-center mb-8">
+          <div className="inline-flex items-center justify-center w-20 h-20 rounded-2xl mb-4" style={{ background: 'linear-gradient(135deg, #3b82f6, #8b5cf6)' }}>
+            <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
+              <path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/>
+              <circle cx="12" cy="12" r="3"/>
+            </svg>
+          </div>
+          <h1 className="text-3xl font-extrabold bg-gradient-to-r from-blue-400 to-purple-400 bg-clip-text text-transparent">BERTHA</h1>
+          <p className="text-gray-400 mt-2 text-sm">Weather Forecast Micro-Betting System</p>
+          <p className="text-gray-500 text-xs mt-1">Polymarket Edge Detection + Risk Management</p>
+        </div>
+
+        <form onSubmit={handleLogin} className="card space-y-4">
+          {error && <div className="text-red-400 text-sm bg-red-500/10 rounded-lg p-3">{error}</div>}
+          <div>
+            <label className="block text-xs font-medium text-gray-400 mb-1.5">Username</label>
+            <input type="text" value={username} onChange={e => setUsername(e.target.value)} required
+              className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-blue-500 transition" placeholder="admin" />
+          </div>
+          <div>
+            <label className="block text-xs font-medium text-gray-400 mb-1.5">Password</label>
+            <input type="password" value={password} onChange={e => setPassword(e.target.value)} required
+              className="w-full bg-gray-800/50 border border-gray-700 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-blue-500 transition" placeholder="Enter password" />
+          </div>
+          <button type="submit" className="btn btn-primary w-full py-2.5">Sign In</button>
+        </form>
+
+        <div className="mt-6 grid grid-cols-3 gap-2 text-center">
+          {[
+            { label: 'Bankroll', value: '$50', color: 'text-green-400' },
+            { label: 'Mode', value: 'Paper', color: 'text-yellow-400' },
+            { label: 'Status', value: 'Active', color: 'text-blue-400' }
+          ].map(s => (
+            <div key={s.label} className="card !p-3">
+              <div className={`text-lg font-bold ${s.color}`}>{s.value}</div>
+              <div className="text-[10px] text-gray-500 uppercase">{s.label}</div>
+            </div>
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/lib/auth.js b/src/lib/auth.js
new file mode 100644
index 0000000..25fb736
--- /dev/null
+++ b/src/lib/auth.js
@@ -0,0 +1,42 @@
+const jwt = require('jsonwebtoken');
+const cookie = require('cookie');
+
+function requiredEnv(name) {
+  const value = process.env[name];
+  if (!value) {
+    throw new Error(`Missing required environment variable: ${name}`);
+  }
+  return value;
+}
+
+const JWT_SECRET = requiredEnv('JWT_SECRET');
+const VALID_USER = process.env.ADMIN_USER || 'admin';
+const VALID_PASS = requiredEnv('ADMIN_PASSWORD');
+
+function createToken(username) {
+  return jwt.sign({ username, role: 'admin' }, JWT_SECRET, { expiresIn: '7d' });
+}
+
+function verifyToken(token) {
+  try {
+    return jwt.verify(token, JWT_SECRET);
+  } catch {
+    return null;
+  }
+}
+
+function getTokenFromRequest(req) {
+  const cookies = cookie.parse(req.headers.get?.('cookie') || req.headers?.cookie || '');
+  if (cookies.bertha_token) return cookies.bertha_token;
+  const auth = req.headers.get?.('authorization') || req.headers?.authorization || '';
+  if (auth.startsWith('Bearer ')) return auth.slice(7);
+  return null;
+}
+
+function authenticateRequest(req) {
+  const token = getTokenFromRequest(req);
+  if (!token) return null;
+  return verifyToken(token);
+}
+
+module.exports = { createToken, verifyToken, authenticateRequest, VALID_USER, VALID_PASS, JWT_SECRET };
diff --git a/src/lib/db.js b/src/lib/db.js
new file mode 100644
index 0000000..aedba2b
--- /dev/null
+++ b/src/lib/db.js
@@ -0,0 +1,32 @@
+const { Pool } = require('pg');
+
+function requiredEnv(name) {
+  const value = process.env[name];
+  if (!value) {
+    throw new Error(`Missing required environment variable: ${name}`);
+  }
+  return value;
+}
+
+const pool = new Pool({
+  connectionString: requiredEnv('DATABASE_URL'),
+  max: 10,
+  idleTimeoutMillis: 30000,
+  connectionTimeoutMillis: 5000,
+});
+
+pool.on('error', (err) => {
+  console.error('[DB] Unexpected error on idle client:', err.message);
+});
+
+async function query(text, params) {
+  const start = Date.now();
+  const res = await pool.query(text, params);
+  const duration = Date.now() - start;
+  if (duration > 1000) {
+    console.warn(`[DB] Slow query (${duration}ms):`, text.substring(0, 80));
+  }
+  return res;
+}
+
+module.exports = { pool, query };
diff --git a/src/lib/model.js b/src/lib/model.js
new file mode 100644
index 0000000..d00373f
--- /dev/null
+++ b/src/lib/model.js
@@ -0,0 +1,139 @@
+const { query } = require('./db');
+
+// Calibrated ensemble probability estimation
+// P(event) = fraction of "forecast scenarios" exceeding threshold
+function ensembleExceedance(forecastFeatures, threshold, operator = '>=') {
+  const values = forecastFeatures?.values || [];
+  if (!values.length) return null;
+
+  let count = 0;
+  for (const v of values) {
+    if (operator === '>=' && v >= threshold) count++;
+    else if (operator === '<=' && v <= threshold) count++;
+    else if (operator === '>' && v > threshold) count++;
+    else if (operator === '<' && v < threshold) count++;
+  }
+
+  return count / values.length;
+}
+
+// Isotonic-style calibration (simple bin-based)
+function calibrate(rawProb, calibrationBins) {
+  if (!calibrationBins || !calibrationBins.length) return rawProb;
+
+  // Find the closest bin
+  let closestBin = calibrationBins[0];
+  let minDist = Math.abs(rawProb - closestBin.center);
+
+  for (const bin of calibrationBins) {
+    const dist = Math.abs(rawProb - bin.center);
+    if (dist < minDist) {
+      minDist = dist;
+      closestBin = bin;
+    }
+  }
+
+  return closestBin.calibrated;
+}
+
+// Simple climatological prior (historical base rate)
+function climatologicalPrior(variable, threshold, month, location) {
+  // Simplified priors based on US weather patterns
+  const priors = {
+    temperature: {
+      // P(temp >= threshold) by month range (rough US averages)
+      summer: (t) => t > 100 ? 0.05 : t > 90 ? 0.35 : t > 80 ? 0.65 : 0.85,
+      winter: (t) => t > 60 ? 0.15 : t > 40 ? 0.50 : t > 20 ? 0.75 : 0.90,
+      spring: (t) => t > 80 ? 0.20 : t > 60 ? 0.55 : t > 40 ? 0.80 : 0.95,
+      fall: (t) => t > 80 ? 0.25 : t > 60 ? 0.50 : t > 40 ? 0.75 : 0.90,
+    },
+    precipitation: {
+      // P(rain >= threshold inches)
+      any: (t) => t > 2 ? 0.10 : t > 1 ? 0.20 : t > 0.5 ? 0.35 : 0.50,
+    },
+    snow: {
+      winter: (t) => t > 12 ? 0.05 : t > 6 ? 0.15 : t > 3 ? 0.25 : 0.40,
+      any: () => 0.10,
+    }
+  };
+
+  const season = month >= 6 && month <= 8 ? 'summer' :
+                 month >= 12 || month <= 2 ? 'winter' :
+                 month >= 3 && month <= 5 ? 'spring' : 'fall';
+
+  const varPriors = priors[variable];
+  if (!varPriors) return 0.50; // uninformative
+  const fn = varPriors[season] || varPriors.any;
+  if (!fn) return 0.50;
+  return fn(threshold);
+}
+
+// Bayesian update: combine prior with forecast evidence
+function bayesianUpdate(prior, forecastProb, forecastWeight = 0.7) {
+  // Weighted combination (simplified)
+  return prior * (1 - forecastWeight) + forecastProb * forecastWeight;
+}
+
+// Compute Brier score for a set of predictions
+function brierScore(predictions) {
+  if (!predictions.length) return null;
+  const sum = predictions.reduce((acc, p) => {
+    const outcome = p.resolved_yes ? 1 : 0;
+    return acc + Math.pow(p.p_yes - outcome, 2);
+  }, 0);
+  return sum / predictions.length;
+}
+
+// Full prediction pipeline
+async function generatePrediction({ conditionId, forecastFeatures, eventSpec, month }) {
+  const { variable, threshold, operator } = eventSpec;
+
+  // Step 1: Ensemble exceedance probability
+  const rawProb = ensembleExceedance(forecastFeatures, threshold, operator);
+  if (rawProb === null) {
+    return { p_yes: null, confidence: 0, method: 'no_data' };
+  }
+
+  // Step 2: Climatological prior
+  const prior = climatologicalPrior(variable, threshold, month);
+
+  // Step 3: Bayesian update
+  const combined = bayesianUpdate(prior, rawProb, 0.7);
+
+  // Step 4: Clamp to [0.01, 0.99] to avoid extreme confidence
+  const pYes = Math.max(0.01, Math.min(0.99, combined));
+
+  // Step 5: Confidence based on data quality
+  const dataPoints = forecastFeatures?.values?.length || 0;
+  const confidence = Math.min(0.95, dataPoints / 20); // more data = more confidence
+
+  return {
+    p_yes: Math.round(pYes * 1000) / 1000,
+    confidence: Math.round(confidence * 100) / 100,
+    method: 'calibrated_ensemble_v1',
+    components: {
+      raw_exceedance: rawProb,
+      climatological_prior: prior,
+      bayesian_combined: combined,
+      data_points: dataPoints,
+    }
+  };
+}
+
+// Store prediction in DB
+async function storePrediction({ conditionId, asofTs, modelVersionId, pYes, confidence, edge, marketMid, recommendation, reasoning }) {
+  const res = await query(`
+    INSERT INTO predictions (condition_id, asof_ts, model_version_id, p_yes, confidence, edge, market_mid, recommendation, reasoning)
+    VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+    ON CONFLICT (condition_id, asof_ts, model_version_id) DO UPDATE SET
+      p_yes = EXCLUDED.p_yes, confidence = EXCLUDED.confidence, edge = EXCLUDED.edge,
+      market_mid = EXCLUDED.market_mid, recommendation = EXCLUDED.recommendation, reasoning = EXCLUDED.reasoning
+    RETURNING *
+  `, [conditionId, asofTs || new Date(), modelVersionId || 1, pYes, confidence, edge, marketMid, recommendation, reasoning]);
+  return res.rows[0];
+}
+
+module.exports = {
+  ensembleExceedance, calibrate, climatologicalPrior, bayesianUpdate,
+  brierScore, generatePrediction, storePrediction
+};
diff --git a/src/lib/polymarket.js b/src/lib/polymarket.js
new file mode 100644
index 0000000..efa42c7
--- /dev/null
+++ b/src/lib/polymarket.js
@@ -0,0 +1,140 @@
+const GAMMA_BASE = 'https://gamma-api.polymarket.com';
+const CLOB_BASE = 'https://clob.polymarket.com';
+const DATA_BASE = 'https://data-api.polymarket.com';
+
+const WEATHER_KEYWORDS = [
+  'temperature', 'weather', 'rain', 'snow', 'hurricane', 'tornado',
+  'heat wave', 'cold', 'frost', 'wind', 'storm', 'precipitation',
+  'drought', 'flood', 'celsius', 'fahrenheit', 'degrees', 'inches',
+  'snowfall', 'rainfall', 'heatwave', 'blizzard', 'record high',
+  'record low', 'above average', 'below average', 'warmest', 'coldest'
+];
+
+async function fetchWithRetry(url, options = {}, retries = 3) {
+  for (let i = 0; i < retries; i++) {
+    try {
+      const res = await fetch(url, {
+        ...options,
+        headers: {
+          'Accept': 'application/json',
+          'User-Agent': 'Bertha-Weather-Bot/1.0 (steve@designerwallcoverings.com)',
+          ...options.headers
+        }
+      });
+      if (res.status === 429) {
+        const wait = Math.pow(2, i) * 1000 + Math.random() * 500;
+        console.log(`[Polymarket] Rate limited, waiting ${Math.round(wait)}ms...`);
+        await new Promise(r => setTimeout(r, wait));
+        continue;
+      }
+      if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
+      return await res.json();
+    } catch (err) {
+      if (i === retries - 1) throw err;
+      await new Promise(r => setTimeout(r, 1000 * (i + 1)));
+    }
+  }
+}
+
+async function getMarkets({ limit = 100, offset = 0, active = true, closed = false } = {}) {
+  const params = new URLSearchParams({
+    limit: String(limit),
+    offset: String(offset),
+    active: String(active),
+    closed: String(closed),
+  });
+  return fetchWithRetry(`${GAMMA_BASE}/markets?${params}`);
+}
+
+async function getMarketBySlug(slug) {
+  return fetchWithRetry(`${GAMMA_BASE}/markets?slug=${encodeURIComponent(slug)}`);
+}
+
+async function getMarketByCondition(conditionId) {
+  return fetchWithRetry(`${GAMMA_BASE}/markets?id=${encodeURIComponent(conditionId)}`);
+}
+
+function isWeatherMarket(market) {
+  const text = `${market.question || ''} ${market.description || ''} ${market.category || ''}`.toLowerCase();
+  return WEATHER_KEYWORDS.some(kw => text.includes(kw));
+}
+
+function parseWeatherEvent(market) {
+  const text = `${market.question || ''} ${market.description || ''}`;
+  const parsed = {
+    raw_question: market.question,
+    variable: null,
+    operator: null,
+    threshold: null,
+    threshold_unit: null,
+    location: null,
+    time_window: null,
+    confidence: 0
+  };
+
+  // Temperature patterns
+  const tempMatch = text.match(/(\d+)\s*(?:degrees?\s*)?(?:°?\s*)?(fahrenheit|celsius|F|C)/i);
+  if (tempMatch) {
+    parsed.variable = 'temperature';
+    parsed.threshold = parseFloat(tempMatch[1]);
+    parsed.threshold_unit = tempMatch[2].toLowerCase().startsWith('f') ? 'F' : 'C';
+    parsed.confidence = 0.8;
+  }
+
+  // Precipitation patterns
+  const precipMatch = text.match(/(\d+(?:\.\d+)?)\s*(?:inches?|in\.?|mm|cm)\s*(?:of\s+)?(rain|snow|precipitation)/i);
+  if (precipMatch) {
+    parsed.variable = precipMatch[2].toLowerCase();
+    parsed.threshold = parseFloat(precipMatch[1]);
+    parsed.threshold_unit = precipMatch[0].match(/mm/i) ? 'mm' : precipMatch[0].match(/cm/i) ? 'cm' : 'inches';
+    parsed.confidence = 0.7;
+  }
+
+  // Operator extraction
+  if (/above|over|exceed|more than|higher than|at least|≥|>=|>/i.test(text)) {
+    parsed.operator = '>=';
+  } else if (/below|under|less than|lower than|at most|≤|<=|</i.test(text)) {
+    parsed.operator = '<=';
+  } else if (/between/i.test(text)) {
+    parsed.operator = 'between';
+  } else if (/reach|hit/i.test(text)) {
+    parsed.operator = '>=';
+  }
+
+  // Location extraction (US cities)
+  const cities = [
+    'New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix', 'Philadelphia',
+    'San Antonio', 'San Diego', 'Dallas', 'San Jose', 'Austin', 'Jacksonville',
+    'Fort Worth', 'Columbus', 'Charlotte', 'Indianapolis', 'San Francisco',
+    'Seattle', 'Denver', 'Washington', 'Nashville', 'Oklahoma City', 'El Paso',
+    'Boston', 'Portland', 'Las Vegas', 'Memphis', 'Louisville', 'Baltimore',
+    'Milwaukee', 'Albuquerque', 'Tucson', 'Fresno', 'Sacramento', 'Kansas City',
+    'Mesa', 'Atlanta', 'Omaha', 'Colorado Springs', 'Raleigh', 'Long Beach',
+    'Virginia Beach', 'Miami', 'Oakland', 'Minneapolis', 'Tampa', 'Tulsa',
+    'Arlington', 'New Orleans', 'Detroit', 'Cleveland', 'Pittsburgh', 'St. Louis'
+  ];
+  for (const city of cities) {
+    if (text.includes(city)) {
+      parsed.location = city;
+      parsed.confidence = Math.min(parsed.confidence + 0.1, 1.0);
+      break;
+    }
+  }
+
+  return parsed;
+}
+
+async function getOrderBook(tokenId) {
+  return fetchWithRetry(`${CLOB_BASE}/book?token_id=${tokenId}`);
+}
+
+async function getMarketTrades(conditionId, limit = 50) {
+  return fetchWithRetry(`${DATA_BASE}/trades?market=${conditionId}&limit=${limit}`);
+}
+
+module.exports = {
+  getMarkets, getMarketBySlug, getMarketByCondition,
+  isWeatherMarket, parseWeatherEvent,
+  getOrderBook, getMarketTrades,
+  GAMMA_BASE, CLOB_BASE, DATA_BASE, WEATHER_KEYWORDS
+};
diff --git a/src/lib/risk.js b/src/lib/risk.js
new file mode 100644
index 0000000..ec859f4
--- /dev/null
+++ b/src/lib/risk.js
@@ -0,0 +1,156 @@
+const { query } = require('./db');
+
+async function getRiskState() {
+  const res = await query('SELECT * FROM risk_state ORDER BY id DESC LIMIT 1');
+  return res.rows[0] || null;
+}
+
+async function updateRiskState(updates) {
+  const state = await getRiskState();
+  if (!state) return null;
+
+  const fields = [];
+  const values = [];
+  let idx = 1;
+
+  for (const [key, val] of Object.entries(updates)) {
+    fields.push(`${key} = $${idx}`);
+    values.push(val);
+    idx++;
+  }
+  fields.push(`updated_at = NOW()`);
+  values.push(state.id);
+
+  await query(`UPDATE risk_state SET ${fields.join(', ')} WHERE id = $${idx}`, values);
+  return getRiskState();
+}
+
+async function getBankroll() {
+  const res = await query('SELECT balance_after FROM bankroll_ledger ORDER BY ts DESC LIMIT 1');
+  return res.rows[0]?.balance_after || 50;
+}
+
+async function getDailyPnL() {
+  const res = await query(`
+    SELECT COALESCE(SUM(pnl), 0) as daily_pnl
+    FROM trades
+    WHERE match_time >= CURRENT_DATE
+    AND match_time < CURRENT_DATE + INTERVAL '1 day'
+  `);
+  return parseFloat(res.rows[0]?.daily_pnl || 0);
+}
+
+async function getOpenExposure() {
+  const res = await query(`
+    SELECT COALESCE(SUM(size_usd), 0) as exposure
+    FROM orders
+    WHERE status IN ('pending', 'open', 'partial')
+  `);
+  return parseFloat(res.rows[0]?.exposure || 0);
+}
+
+function calculateEdge(pYes, marketMid, feeBps = 0) {
+  const feeAdjust = feeBps / 10000;
+  const edgeYes = pYes - marketMid - feeAdjust;
+  const edgeNo = (1 - pYes) - (1 - marketMid) - feeAdjust;
+  return { edgeYes, edgeNo, bestSide: edgeYes > edgeNo ? 'YES' : 'NO', bestEdge: Math.max(edgeYes, edgeNo) };
+}
+
+function kellySize(edge, odds, fraction = 0.1) {
+  // Fractional Kelly: f* = fraction * (p*b - q) / b
+  // where p = prob of winning, b = odds, q = 1-p
+  if (edge <= 0) return 0;
+  const p = 0.5 + edge / 2; // approximate
+  const b = odds;
+  const q = 1 - p;
+  const fullKelly = (p * b - q) / b;
+  return Math.max(0, fullKelly * fraction);
+}
+
+async function evaluateTrade({ pYes, marketMid, spread, liquidity, conditionId }) {
+  const state = await getRiskState();
+  if (!state) return { approved: false, reason: 'no_risk_state' };
+
+  const config = state.config;
+  const bankroll = parseFloat(state.bankroll);
+  const dailyPnl = await getDailyPnL();
+  const openExposure = await getOpenExposure();
+
+  // Kill switch check
+  if (state.kill_switch_active) {
+    return { approved: false, reason: `kill_switch: ${state.kill_reason}` };
+  }
+
+  // Paper mode check
+  const paperMode = config.paper_mode !== false;
+
+  // Edge calculation
+  const { bestEdge, bestSide } = calculateEdge(pYes, marketMid);
+
+  // Edge threshold
+  if (bestEdge < config.min_edge_threshold) {
+    return { approved: false, reason: `insufficient_edge: ${bestEdge.toFixed(4)} < ${config.min_edge_threshold}`, edge: bestEdge };
+  }
+
+  // Liquidity check
+  if (liquidity < config.min_liquidity) {
+    return { approved: false, reason: `low_liquidity: ${liquidity} < ${config.min_liquidity}`, edge: bestEdge };
+  }
+
+  // Daily loss limit
+  if (dailyPnl < -(config.max_daily_loss_usd || 5)) {
+    return { approved: false, reason: `daily_loss_limit: ${dailyPnl.toFixed(2)}`, edge: bestEdge };
+  }
+
+  // Open exposure limit
+  if (openExposure >= (config.max_open_exposure_usd || 5)) {
+    return { approved: false, reason: `exposure_limit: ${openExposure.toFixed(2)}`, edge: bestEdge };
+  }
+
+  // Drawdown limit
+  if (state.max_drawdown_pct >= (config.max_drawdown_pct || 20)) {
+    return { approved: false, reason: `drawdown_limit: ${state.max_drawdown_pct.toFixed(1)}%`, edge: bestEdge };
+  }
+
+  // Sizing
+  const kellyFraction = kellySize(bestEdge, 1 / marketMid, config.fractional_kelly || 0.1);
+  const kellyBet = kellyFraction * bankroll;
+  const fixedBet = Math.min(0.25, bankroll * 0.005);
+  const betSize = Math.min(kellyBet || fixedBet, fixedBet, config.max_bet_usd || 0.50);
+  const finalBet = Math.max(0.01, Math.round(betSize * 100) / 100);
+
+  return {
+    approved: true,
+    paper_mode: paperMode,
+    side: bestSide,
+    edge: bestEdge,
+    bet_usd: finalBet,
+    kelly_fraction: kellyFraction,
+    bankroll,
+    daily_pnl: dailyPnl,
+    open_exposure: openExposure,
+    reasoning: {
+      edge_threshold: config.min_edge_threshold,
+      edge_actual: bestEdge,
+      spread,
+      liquidity,
+      kelly_raw: kellyFraction,
+      bet_capped: finalBet
+    }
+  };
+}
+
+async function activateKillSwitch(reason) {
+  await updateRiskState({ kill_switch_active: true, kill_reason: reason });
+  return { activated: true, reason };
+}
+
+async function deactivateKillSwitch() {
+  await updateRiskState({ kill_switch_active: false, kill_reason: null });
+  return { deactivated: true };
+}
+
+module.exports = {
+  getRiskState, updateRiskState, getBankroll, getDailyPnL, getOpenExposure,
+  calculateEdge, kellySize, evaluateTrade, activateKillSwitch, deactivateKillSwitch
+};
diff --git a/src/lib/slack.js b/src/lib/slack.js
new file mode 100644
index 0000000..02c6db6
--- /dev/null
+++ b/src/lib/slack.js
@@ -0,0 +1,59 @@
+const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN || '';
+const STEVE_USER_ID = 'U03TV3UC2V7';
+
+async function sendSlackAlert({ type, severity, message, details, channel }) {
+  const target = channel || STEVE_USER_ID;
+
+  const severityEmoji = {
+    info: ':information_source:',
+    warning: ':warning:',
+    critical: ':rotating_light:',
+    emergency: ':fire:'
+  };
+
+  const blocks = [
+    {
+      type: 'section',
+      text: {
+        type: 'mrkdwn',
+        text: `${severityEmoji[severity] || ':robot_face:'} *[Bertha] ${type}*\n${message}`
+      }
+    }
+  ];
+
+  if (details) {
+    blocks.push({
+      type: 'context',
+      elements: [{
+        type: 'mrkdwn',
+        text: typeof details === 'string' ? details : Object.entries(details).map(([k, v]) => `*${k}:* ${v}`).join(' | ')
+      }]
+    });
+  }
+
+  if (!SLACK_BOT_TOKEN) {
+    console.log('[Slack] No token configured - would send:', JSON.stringify({ target, blocks }));
+    return { ok: false, error: 'no_token' };
+  }
+
+  try {
+    const res = await fetch('https://slack.com/api/chat.postMessage', {
+      method: 'POST',
+      headers: {
+        'Authorization': `Bearer ${SLACK_BOT_TOKEN}`,
+        'Content-Type': 'application/json'
+      },
+      body: JSON.stringify({
+        channel: target,
+        text: `[Bertha] ${type}: ${message}`,
+        blocks
+      })
+    });
+    return await res.json();
+  } catch (err) {
+    console.error('[Slack] Send error:', err.message);
+    return { ok: false, error: err.message };
+  }
+}
+
+module.exports = { sendSlackAlert, STEVE_USER_ID };
diff --git a/src/lib/weather.js b/src/lib/weather.js
new file mode 100644
index 0000000..a4eb7b2
--- /dev/null
+++ b/src/lib/weather.js
@@ -0,0 +1,140 @@
+const NWS_BASE = 'https://api.weather.gov';
+const USER_AGENT = 'Bertha-Weather-Bot/1.0 (steve@designerwallcoverings.com)';
+
+async function nwsFetch(url) {
+  const res = await fetch(url, {
+    headers: {
+      'User-Agent': USER_AGENT,
+      'Accept': 'application/geo+json'
+    }
+  });
+  if (!res.ok) throw new Error(`NWS API ${res.status}: ${res.statusText} for ${url}`);
+  return res.json();
+}
+
+// Get gridpoint mapping for a lat/lon
+async function getGridPoint(lat, lon) {
+  return nwsFetch(`${NWS_BASE}/points/${lat},${lon}`);
+}
+
+// Get forecast for a gridpoint
+async function getForecast(office, gridX, gridY) {
+  return nwsFetch(`${NWS_BASE}/gridpoints/${office}/${gridX},${gridY}/forecast`);
+}
+
+// Get hourly forecast for a gridpoint
+async function getHourlyForecast(office, gridX, gridY) {
+  return nwsFetch(`${NWS_BASE}/gridpoints/${office}/${gridX},${gridY}/forecast/hourly`);
+}
+
+// Get raw gridpoint data (quantitative)
+async function getGridData(office, gridX, gridY) {
+  return nwsFetch(`${NWS_BASE}/gridpoints/${office}/${gridX},${gridY}`);
+}
+
+// Get active alerts for a point
+async function getAlerts(lat, lon) {
+  return nwsFetch(`${NWS_BASE}/alerts/active?point=${lat},${lon}`);
+}
+
+// Get station observations
+async function getStationObs(stationId) {
+  return nwsFetch(`${NWS_BASE}/stations/${stationId}/observations/latest`);
+}
+
+// Get list of stations near a point
+async function getNearbyStations(office, gridX, gridY) {
+  return nwsFetch(`${NWS_BASE}/gridpoints/${office}/${gridX},${gridY}/stations`);
+}
+
+// City coordinate lookup
+const CITY_COORDS = {
+  'New York': { lat: 40.7128, lon: -74.0060 },
+  'Los Angeles': { lat: 34.0522, lon: -118.2437 },
+  'Chicago': { lat: 41.8781, lon: -87.6298 },
+  'Houston': { lat: 29.7604, lon: -95.3698 },
+  'Phoenix': { lat: 33.4484, lon: -112.0740 },
+  'Philadelphia': { lat: 39.9526, lon: -75.1652 },
+  'San Antonio': { lat: 29.4241, lon: -98.4936 },
+  'San Diego': { lat: 32.7157, lon: -117.1611 },
+  'Dallas': { lat: 32.7767, lon: -96.7970 },
+  'San Francisco': { lat: 37.7749, lon: -122.4194 },
+  'Seattle': { lat: 47.6062, lon: -122.3321 },
+  'Denver': { lat: 39.7392, lon: -104.9903 },
+  'Boston': { lat: 42.3601, lon: -71.0589 },
+  'Miami': { lat: 25.7617, lon: -80.1918 },
+  'Atlanta': { lat: 33.7490, lon: -84.3880 },
+  'Minneapolis': { lat: 44.9778, lon: -93.2650 },
+  'Detroit': { lat: 42.3314, lon: -83.0458 },
+  'Portland': { lat: 45.5051, lon: -122.6750 },
+  'Las Vegas': { lat: 36.1699, lon: -115.1398 },
+  'Nashville': { lat: 36.1627, lon: -86.7816 },
+  'Washington': { lat: 38.9072, lon: -77.0369 },
+  'Cleveland': { lat: 41.4993, lon: -81.6944 },
+  'Pittsburgh': { lat: 40.4406, lon: -79.9959 },
+  'St. Louis': { lat: 38.6270, lon: -90.1994 },
+  'Kansas City': { lat: 39.0997, lon: -94.5786 },
+  'Tampa': { lat: 27.9506, lon: -82.4572 },
+  'New Orleans': { lat: 29.9511, lon: -90.0715 },
+  'Oklahoma City': { lat: 35.4676, lon: -97.5164 },
+};
+
+function getCityCoords(cityName) {
+  return CITY_COORDS[cityName] || null;
+}
+
+// Generate geom_hash for consistent location keys
+function geomHash(lat, lon) {
+  return `${Math.round(lat * 100)}_${Math.round(lon * 100)}`;
+}
+
+// Extract forecast features for a specific variable and time
+function extractForecastFeatures(forecastData, variable, targetTime) {
+  const periods = forecastData?.properties?.periods || [];
+  if (!periods.length) return null;
+
+  const features = {
+    variable,
+    forecast_count: periods.length,
+    values: [],
+    timestamps: []
+  };
+
+  for (const period of periods) {
+    let value = null;
+    if (variable === 'temperature') {
+      value = period.temperature;
+      features.unit = period.temperatureUnit;
+    } else if (variable === 'wind') {
+      const match = period.windSpeed?.match(/(\d+)/);
+      if (match) value = parseInt(match[1]);
+      features.unit = 'mph';
+    } else if (variable === 'precipitation') {
+      const prob = period.probabilityOfPrecipitation?.value;
+      value = prob != null ? prob : 0;
+      features.unit = 'percent';
+    }
+
+    if (value !== null) {
+      features.values.push(value);
+      features.timestamps.push(period.startTime);
+    }
+  }
+
+  if (features.values.length > 0) {
+    features.mean = features.values.reduce((a, b) => a + b, 0) / features.values.length;
+    features.min = Math.min(...features.values);
+    features.max = Math.max(...features.values);
+    features.range = features.max - features.min;
+    features.latest = features.values[0];
+  }
+
+  return features;
+}
+
+module.exports = {
+  getGridPoint, getForecast, getHourlyForecast, getGridData,
+  getAlerts, getStationObs, getNearbyStations,
+  getCityCoords, geomHash, extractForecastFeatures,
+  CITY_COORDS, NWS_BASE
+};
diff --git a/src/workers/cron-runner.js b/src/workers/cron-runner.js
new file mode 100644
index 0000000..a152afb
--- /dev/null
+++ b/src/workers/cron-runner.js
@@ -0,0 +1,61 @@
+const cron = require('node-cron');
+
+const BASE_URL = 'http://127.0.0.1:7800';
+
+async function runWorker(name, url, body) {
+  const start = Date.now();
+  console.log(`[Cron] Starting ${name}...`);
+  try {
+    const res = await fetch(url, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(body),
+    });
+    const data = await res.json();
+    console.log(`[Cron] ${name} completed in ${Date.now() - start}ms:`, JSON.stringify(data).substring(0, 200));
+    return data;
+  } catch (err) {
+    console.error(`[Cron] ${name} error:`, err.message);
+    return { error: err.message };
+  }
+}
+
+// Market ingest: every 30 minutes
+cron.schedule('*/30 * * * *', () => {
+  runWorker('market_ingest', `${BASE_URL}/api/markets`, { action: 'ingest' });
+});
+
+// Weather fetch all cities: every 2 hours
+cron.schedule('0 */2 * * *', () => {
+  runWorker('weather_fetch_all', `${BASE_URL}/api/weather`, { action: 'fetch_all' });
+});
+
+// Run predictions: every hour
+cron.schedule('0 * * * *', () => {
+  runWorker('run_predictions', `${BASE_URL}/api/models`, { action: 'run_predictions' });
+});
+
+// Daily risk reset at midnight UTC
+cron.schedule('0 0 * * *', async () => {
+  console.log('[Cron] Daily risk reset');
+  try {
+    const { query } = require('../lib/db');
+    await query("UPDATE risk_state SET daily_pnl = 0, updated_at = NOW() WHERE id = (SELECT id FROM risk_state ORDER BY id DESC LIMIT 1)");
+    console.log('[Cron] Daily P&L reset to 0');
+  } catch (err) {
+    console.error('[Cron] Risk reset error:', err.message);
+  }
+});
+
+// Health check ping: every 5 minutes
+cron.schedule('*/5 * * * *', async () => {
+  try {
+    const res = await fetch(`${BASE_URL}/api/health`);
+    if (!res.ok) console.warn('[Cron] Health check failed:', res.status);
+  } catch (err) {
+    console.error('[Cron] Health check error:', err.message);
+  }
+});
+
+console.log('[Cron] Bertha cron runner started');
+console.log('[Cron] Schedules: market_ingest(30m), weather_fetch(2h), predictions(1h), risk_reset(midnight), health(5m)');
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..35c6d46
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,21 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+  content: ['./src/**/*.{js,jsx}'],
+  theme: {
+    extend: {
+      colors: {
+        bertha: {
+          bg: '#0a0e1a',
+          card: '#111827',
+          border: '#1e293b',
+          accent: '#3b82f6',
+          green: '#10b981',
+          red: '#ef4444',
+          yellow: '#f59e0b',
+          purple: '#8b5cf6',
+        }
+      }
+    }
+  },
+  plugins: [],
+};

(oldest)  ·  back to Ken  ·  tighten .gitignore: add missing standing-rule patterns (.env a27e0fd →