diff --git a/.github/workflows/daveai-production-e2e.yml b/.github/workflows/daveai-production-e2e.yml
new file mode 100644
index 00000000000..b02bcc8bed5
--- /dev/null
+++ b/.github/workflows/daveai-production-e2e.yml
@@ -0,0 +1,43 @@
+name: DaveAI Production E2E
+
+on:
+ workflow_dispatch:
+ inputs:
+ base_url:
+ description: "DaveAI base URL"
+ required: false
+ default: "https://daveai.tech"
+ schedule:
+ - cron: "17 9 * * *"
+
+jobs:
+ production-e2e:
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ env:
+ DAVEAI_BASE_URL: ${{ github.event.inputs.base_url || 'https://daveai.tech' }}
+ DAVEAI_E2E_EMAIL: ${{ secrets.DAVEAI_E2E_EMAIL }}
+ DAVEAI_E2E_PASSWORD: ${{ secrets.DAVEAI_E2E_PASSWORD }}
+ DAVEAI_PROOF_DIR: proofs/daveai-production-e2e
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+
+ - name: Install Playwright runtime
+ run: |
+ npm install --no-save playwright
+ npx playwright install --with-deps chromium
+
+ - name: Run DaveAI production E2E proof pack
+ run: node vps/production-e2e-runner.cjs
+
+ - name: Upload E2E proofs
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: daveai-production-e2e-proofs
+ path: proofs/daveai-production-e2e
+ if-no-files-found: warn
diff --git a/.gitignore b/.gitignore
index 4bc03e175d1..be31bc1678a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
+__pycache__/
dist
dist-ssr
*.local
@@ -45,4 +46,8 @@ changelogUI.md
docs/instructions/Roadmap.md
.cursorrules
*.md
+!docs/DAVEAI_PRODUCTION_E2E_AUDIT_20260731.md
+!docs/DAVEAI_RC1_TO_RC2_HANDOFF_20260801.md
+!docs/DAVEAI_RC2_E2E_ROADMAP_20260801.md
+private/
.qodo
diff --git a/docs/DAVEAI_PRODUCTION_E2E_AUDIT_20260731.md b/docs/DAVEAI_PRODUCTION_E2E_AUDIT_20260731.md
new file mode 100644
index 00000000000..e6199d958a2
--- /dev/null
+++ b/docs/DAVEAI_PRODUCTION_E2E_AUDIT_20260731.md
@@ -0,0 +1,122 @@
+# DaveAI.tech Production E2E Audit and Roadmap — 2026-07-31
+
+This document records the production truth from the July 31, 2026 DaveAI.tech audit. It separates proven-working behavior from accepted/auth-gated behavior and unresolved platform debt.
+
+Proof artifact root:
+
+`G:\Github\Bolt.DIY\proofs\daveai-prod-audit-20260731`
+
+## Current production status
+
+| Area | Status | Proof / fact |
+| --- | --- | --- |
+| Main DaveAI chat unauthenticated state | Working as gated | No-token and malformed-token browser proofs show auth modal, no `/api/stream` call, and no protected API storm: `09-final-no-token-auth-gate.png`, `10-final-malformed-token-auth-gate.png`. |
+| Main DaveAI authenticated chat | Working | Signed-in browser proof completed with AI reply, status `Ready`, no bad API responses, no page errors: `13-final-browser-auth-chat-no429.json`, `13-final-browser-auth-chat-no429.png`. |
+| Main health endpoint | Working | `https://daveai.tech/api/health` returned HTTP 200 in `29-health-endpoints.json`. |
+| Catalog HTTP inventory | Working at URL level | 55 catalog entries returned HTTP 200 after login-page rate-limit fix: `15-catalog-http-audit-after-login-guard.json`. |
+| Game carousel card activation | Working | Carousel cards launch selected iframe targets for Siege TD, TD2, and Asteroids: `20-carousel-iframe-proof.json`, `20-carousel-iframe-0.png`, `20-carousel-iframe-1.png`, `20-carousel-iframe-2.png`. |
+| DaveAI Asteroids gameplay smoke | Working | Start/gameplay proof changed score and had no bad responses or page errors: `22-gameplay-core-proof.json`, `22-gameplay-2.png`. |
+| Dave's Siege TD smoke | Working to menu | Browser proof reaches saved-name menu / campaign controls with no bad responses or page errors: `22-gameplay-core-proof.json`, `22-gameplay-0.png`. |
+| DaveAI TD2 | Working after repair | Missing VFX/audio assets were repaired, stale cached asset references were cache-busted, and the final browser proof had no 404/502 responses, no request failures, no page errors, no actionable console errors, and a canvas: `24-td2-cachebust-gameplay.json`, `24-td2-cachebust-gameplay.png`. |
+| Checkers Crowning | Corrected at HTTP/title level | Catalog audit shows title/H1 `Checkers Crowning`; it no longer resolves to Wheel of Fortune in the HTTP audit. |
+| DaveAI Studio | Working at page-smoke level | Correct catalog URL `https://daveai.tech/studio/index.html` renders with no bad responses or console/page errors: `26-catalog-non-game-pages-smoke.json`, `26-catalog-page-studio.png`. |
+| DaveAI Web Pages | Working at page-smoke level | `https://daveai.tech/web-pages.html` renders cleanly: `26-catalog-non-game-pages-smoke.json`, `26-catalog-page-web-pages.png`. |
+| DaveAI Voice / Voice Landing | Working at page-smoke level | `https://voice.daveai.tech/` and `https://daveai.tech/voice-landing.html` render cleanly: `26-catalog-non-game-pages-smoke.json`, `26-catalog-page-voice.png`, `26-catalog-page-voice-landing.png`. |
+| StreamHub IPTV public page | Working after repair | Public StreamHub now renders with no bad responses, no request failures, no page errors, no console errors, and a video element: `28-streamhub-after-provider-empty.json`, `28-streamhub-after-provider-empty.png`. Final combined non-game proof: `34-final-non-game-pages-smoke.json`, `34-page-streamhub.png`. |
+| Windsurf / Ops page | Working as auth-gated | Catalog marks it `auth`. Public browser proof now shows an explicit auth gate and makes no protected admin API calls: `33-windsurf-auth-gate.json`, `33-windsurf-auth-gate.png`. Final combined non-game proof: `34-final-non-game-pages-smoke.json`, `34-page-windsurf.png`. |
+| Provider/IPTV private provider vault | Auth-gated | Public StreamHub now gets `{"providers":[]}` at `/api/provider-vault/providers`; protected provider catalog/stream APIs still require auth. |
+| `brain.daveai.tech/health` | Working after repair | Added the missing `brain.daveai.tech` Nginx vhost with the Cloudflare Origin wildcard certificate; public health now returns HTTP 200: `32-health-endpoints-after-api-fix.json`. |
+| `api.daveai.tech/health` | Working after repair | Added a narrow public `/health` response while leaving the rest of the API protected; public health now returns HTTP 200: `32-health-endpoints-after-api-fix.json`. |
+| `voice.daveai.tech/health` | Working after repair | Added exact `/health` proxy to the Edge TTS backend; public health now returns HTTP 200: `32-health-endpoints-after-api-fix.json`. |
+| LiteLLM router | Functional; bare `/health` probe is bad | `127.0.0.1:4000/health` and model-scoped `/health?model=...` hang, but `/health/readiness`, `/health/liveliness`, `/v1/models`, and real `/v1/chat/completions` probes for `fast-agent`, `local-fallback`, and `heavy-coder` return HTTP 200. Use `vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh` for functional health until the bare LiteLLM `/health` route is fixed upstream/configured differently. Proof: `36-litellm-endpoints.txt`, `37-litellm-model-health.txt`, `38-litellm-chat-probe.txt`. |
+| VPS system health | Needs maintenance | SSH banner reported high swap use, 32 zombie processes, pending updates, and restart required in `30-vps-process-health.txt`. |
+
+## Repeatable E2E coverage added
+
+Source-controlled runner:
+
+`vps/production-e2e-runner.cjs`
+
+NPM entry:
+
+`npm run test:daveai:prod`
+
+GitHub Actions workflow:
+
+`.github/workflows/daveai-production-e2e.yml`
+
+The runner covers:
+
+- public health endpoints,
+- unauthenticated chat auth gate,
+- authenticated chat reply when `DAVEAI_E2E_EMAIL` and `DAVEAI_E2E_PASSWORD` are supplied,
+- carousel card launch,
+- TD2/Asteroids/Siege runtime smoke,
+- Studio/Web Pages/Windsurf/Voice/Voice Landing/StreamHub smoke,
+- StreamHub no-error render.
+
+Latest public run without auth credentials: 6 passed, 0 failed, 1 skipped (`authenticated chat reply` skipped because no credentials were supplied). Proof root:
+
+`G:\Github\Bolt.DIY\proofs\daveai-prod-e2e-runner-20260731-rerun`
+
+## Repairs completed during this audit
+
+1. Fixed frontend auth gating and stale `Thinking...` state for unauthenticated / malformed-token chat attempts.
+2. Fixed signed-in chat browser behavior by:
+ - using a direct native Ollama chat path in the backend,
+ - streaming token chunks into the existing SSE queue,
+ - preserving token spacing in the frontend SSE parser,
+ - disabling automatic chat TTS narration by default to avoid post-reply 429s.
+3. Raised API and login-page rate limits enough for real browser/catalog audits without weakening credential POST throttles.
+4. Fixed production game/card activation by aligning local source with production card-as-button launcher behavior.
+5. Repaired DaveAI TD2 missing assets:
+ - 140 VFX PNG placeholders,
+ - 10 silent MP3 placeholders,
+ - cache-busted unversioned TD2 JS asset references to bypass stale CDN/origin cache.
+6. Fixed StreamHub public-page provider-vault behavior by making unauthenticated `/api/provider-vault/providers` return `200 {"providers":[]}` instead of redirecting an XHR to the login domain.
+7. Added public health coverage for `brain.daveai.tech`, `voice.daveai.tech`, and `api.daveai.tech`.
+8. Added Windsurf/Ops auth gating so public users see a sign-in/launcher message and the page does not poll protected admin APIs until auth is present.
+
+## Remaining work to finish DaveAI.tech E2E
+
+### P0 — must finish before calling the whole platform complete
+
+1. Make the production changes durable in source control.
+ - Current VPS-only changes include backend `brain_llm.py`, Nginx configs, generated TD2 assets, TD2 built JS cache-bust, and the StreamHub Nginx provider-vault rule.
+ - Local repo currently includes DaveAI UI source edits and this proof/doc set, but not all VPS-only runtime changes.
+2. Add repeatable E2E tests to CI or a production smoke runner.
+ - Chat unauthenticated gate.
+ - Authenticated chat reply and `Ready` status.
+ - Carousel card launches.
+ - TD2/Asteroids/Siege canvas or menu state.
+ - Catalog URL sweep.
+ - StreamHub public no-error render.
+
+### P1 — product quality / proof expansion
+
+1. Add gameplay-specific assertions for all 17 public games. Current coverage is a mix of direct smoke and deeper checks for the main three carousel games.
+2. Replace TD2 placeholder VFX/audio assets with real intended assets if brand polish matters.
+3. Clean up Nginx `listen 443 ssl http2` protocol-option warnings across vhosts.
+4. Track LiteLLM bare `/health` separately from functional health. The router and completion path work, but bare `/health` hangs and should not be used as the production liveness check.
+5. Reduce VPS operational pressure: updates, reboot window, swap pressure, zombie processes.
+ - Safe diagnostic script added: `vps/vps-maintenance-check.sh`.
+ - Do not run apt upgrades or reboot outside an explicit maintenance window.
+
+### P2 — catalog/status polish
+
+1. Define catalog status meanings:
+ - `live`: public route should render with no auth, no console/request failures.
+ - `auth`: route may require login but should not throw noisy unauthenticated browser errors.
+ - `historical`: retained route, not active product surface.
+2. Add stale-route redirects for common wrong paths if users have already seen them:
+ - `/studio.html` -> `/studio/index.html`
+ - `/voice.html` -> `/voice-landing.html` or `https://voice.daveai.tech/`
+ - `/streamhub.html` -> `https://iptv.daveai.tech/`
+3. Keep screenshots and JSON proofs versioned or archived with each production revision.
+
+## Next execution order
+
+1. Persist VPS-only runtime fixes into the deploy/source-of-truth repository or an explicit deployment patch directory.
+2. Add the repeatable E2E smoke runner to CI or a production-proof script.
+3. Run the full proof pack again from the durable source deployment path.
+4. Commit, push, and open/update the PR only after the durable source matches production behavior.
diff --git a/docs/DAVEAI_RC1_TO_RC2_HANDOFF_20260801.md b/docs/DAVEAI_RC1_TO_RC2_HANDOFF_20260801.md
new file mode 100644
index 00000000000..dc0bb17d630
--- /dev/null
+++ b/docs/DAVEAI_RC1_TO_RC2_HANDOFF_20260801.md
@@ -0,0 +1,235 @@
+# DaveAI.tech RC1 → RC2 Handoff — 2026-08-01
+
+This is the handoff for continuing DaveAI.tech work on a more stable coding PC with Codex.
+
+## Executive truth
+
+RC1 is not “everything forever complete,” but the production repair work is now source-controlled and reproducible.
+
+RC1 completed:
+
+- main DaveAI chat works again through the native Ollama backend path;
+- unauthenticated chat attempts are cleanly auth-gated;
+- TD2 missing assets and stale asset cache issue are repaired;
+- game carousel launches the selected game;
+- StreamHub public page no longer throws provider-vault XHR errors;
+- Windsurf/Ops is now properly auth-gated and does not poll protected APIs publicly;
+- public health endpoints are green;
+- production runtime fixes are persisted in source under `vps/patches/daveai-production-runtime-20260731/`;
+- repeatable production E2E runner exists at `vps/production-e2e-runner.cjs`;
+- draft PR exists: https://github.com/stackblitz-labs/bolt.diy/pull/2186.
+
+RC1 remaining / RC2 target:
+
+- run authenticated-chat E2E with a real test account or GitHub Actions secrets;
+- move from “runtime patch bundle” to the long-term canonical deployment source;
+- complete deeper gameplay assertions for all public games, not just smoke/runtime checks;
+- run VPS maintenance in a planned window;
+- decide how to archive or publish proof artifacts without bloating the repo;
+- harden LiteLLM health semantics: functional health works, bare `/health` still hangs.
+
+## Current branches and PRs
+
+Local branch:
+
+```text
+chore/daveai-a11y-source-truth-20260725
+```
+
+Remote branch:
+
+```text
+origin/chore/daveai-a11y-source-truth-20260725
+```
+
+Draft PR:
+
+```text
+https://github.com/stackblitz-labs/bolt.diy/pull/2186
+```
+
+Latest commit:
+
+```text
+41e8b61 Persist DaveAI production runtime repairs
+```
+
+## Source map
+
+```mermaid
+flowchart TD
+ A["Production VPS 187.77.30.206"] --> B["Runtime truth captured 2026-07-31 / 2026-08-01"]
+ B --> C["Source-controlled patch bundle vps/patches/daveai-production-runtime-20260731"]
+ C --> D["Apply script apply-production-runtime-fixes.sh"]
+ C --> E["LiteLLM functional health script check-litellm-health.sh"]
+ C --> F["TD2 asset manifest + patched bundle"]
+ C --> G["Nginx vhost/rate-limit/health configs"]
+ C --> H["Windsurf auth-gated HTML"]
+ C --> I["brain_llm.py native streaming path"]
+ J["E2E runner vps/production-e2e-runner.cjs"] --> K["Local proof artifacts proofs/"]
+ J --> L["GitHub Actions daveai-production-e2e.yml"]
+```
+
+## Production service map
+
+```mermaid
+flowchart LR
+ User["Browser / user"] --> CF["Cloudflare"]
+ CF --> Nginx["VPS Nginx"]
+
+ Nginx --> Main["daveai.tech /var/www/agentic-website"]
+ Main --> Brain["agent-brain 127.0.0.1:8888"]
+ Brain --> Ollama["Ollama 127.0.0.1:11434"]
+
+ Nginx --> Voice["voice.daveai.tech edge-tts 127.0.0.1:5050"]
+ Nginx --> API["api.daveai.tech public /health only; rest protected/upstream"]
+ Nginx --> BrainHost["brain.daveai.tech proxy to 127.0.0.1:8888"]
+ Nginx --> IPTV["iptv.daveai.tech StreamHub Docker :3103"]
+ IPTV --> ProviderVault["provider-vault API protected except public empty providers list"]
+ Main --> Games["/games/* TD2 / Asteroids / Siege / Arcade"]
+```
+
+## RC1 production proof summary
+
+Primary proof roots on the original PC:
+
+```text
+G:\Github\Bolt.DIY\proofs\daveai-prod-audit-20260731
+G:\Github\Bolt.DIY\proofs\daveai-prod-e2e-runner-20260731-rerun
+```
+
+Latest repeatable public E2E result:
+
+```text
+6 passed, 0 failed, 1 skipped
+```
+
+Skipped:
+
+```text
+authenticated chat reply
+```
+
+Reason:
+
+```text
+DAVEAI_E2E_EMAIL and DAVEAI_E2E_PASSWORD were not supplied locally.
+```
+
+## How to resume on another PC
+
+1. Copy or clone the repo.
+
+ Preferred if using the prepared copy:
+
+ ```powershell
+ cd I:\Github\Bolt.DIY
+ git status --short --branch
+ ```
+
+ If cloning fresh:
+
+ ```powershell
+ cd I:\Github
+ git clone https://github.com/Ghenghis/bolt.diy.git Bolt.DIY
+ cd I:\Github\Bolt.DIY
+ git checkout chore/daveai-a11y-source-truth-20260725
+ git pull
+ ```
+
+2. Confirm GitHub CLI auth.
+
+ ```powershell
+ gh auth status
+ gh pr view 2186 --repo stackblitz-labs/bolt.diy
+ ```
+
+3. Install/use Node dependencies.
+
+ If `node_modules` was copied, first try:
+
+ ```powershell
+ npm run test:daveai:prod
+ ```
+
+ If dependencies are missing or stale:
+
+ ```powershell
+ pnpm install
+ ```
+
+4. Run public production E2E.
+
+ ```powershell
+ $env:DAVEAI_PROOF_DIR="I:\Github\Bolt.DIY\proofs\daveai-prod-e2e-rc2"
+ npm run test:daveai:prod
+ ```
+
+5. Run authenticated chat E2E when a safe test account exists.
+
+ ```powershell
+ $env:DAVEAI_E2E_EMAIL="test-account@example.com"
+ $env:DAVEAI_E2E_PASSWORD="replace-with-private-value"
+ $env:DAVEAI_PROOF_DIR="I:\Github\Bolt.DIY\proofs\daveai-prod-e2e-rc2-auth"
+ npm run test:daveai:prod
+ ```
+
+Do not commit real credentials.
+
+## RC2 acceptance gates
+
+RC2 should not be called complete until all of these are true:
+
+| Gate | Required proof |
+| --- | --- |
+| Public E2E | `npm run test:daveai:prod` passes with 0 failed. |
+| Authenticated chat E2E | Same runner passes authenticated chat with supplied test account. |
+| Durable deployment source | VPS patch bundle is either accepted as the deployment source or migrated into the canonical deploy repo/process. |
+| Full game catalog | All public games have at least load/render assertions; top games have input/state assertions. |
+| Health | `daveai.tech`, `brain.daveai.tech`, `voice.daveai.tech`, `api.daveai.tech`, and StreamHub provider list health checks are 200. |
+| LiteLLM | `check-litellm-health.sh` passes; bare `/health` behavior documented or fixed. |
+| VPS maintenance | Maintenance window run completed, reboot done if required, post-reboot smoke green. |
+| Proof handling | Proof artifacts copied/archived without bloating Git history unintentionally. |
+
+## What not to do blindly
+
+- Do not run `git reset --hard` on either PC unless the target branch and local user work are explicitly confirmed.
+- Do not commit `proofs/` unless intentionally creating a proof archive.
+- Do not commit SSH keys, `.env`, cookies, tokens, or private credentials.
+- Do not reboot or upgrade the VPS during active user traffic without a maintenance window.
+- Do not replace production Nginx configs without `nginx -t` and a backup.
+
+## First RC2 work order
+
+1. On the stable PC, verify the copied checkout:
+
+ ```powershell
+ git status --short --branch
+ npm run test:daveai:prod
+ ```
+
+2. Add real test credentials locally or as GitHub Actions secrets:
+
+ ```text
+ DAVEAI_E2E_EMAIL
+ DAVEAI_E2E_PASSWORD
+ ```
+
+3. Re-run the full runner and confirm 7 passed, 0 failed, 0 skipped.
+4. Run the VPS maintenance diagnostic:
+
+ ```bash
+ bash vps/vps-maintenance-check.sh
+ ```
+
+5. Schedule maintenance window for apt updates/reboot.
+6. After reboot, run:
+
+ ```bash
+ nginx -t
+ systemctl is-active nginx litellm
+ pm2 status
+ bash vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh
+ ```
+
+7. Run `npm run test:daveai:prod` again from the stable PC.
diff --git a/docs/DAVEAI_RC2_E2E_ROADMAP_20260801.md b/docs/DAVEAI_RC2_E2E_ROADMAP_20260801.md
new file mode 100644
index 00000000000..0bbaa9c4332
--- /dev/null
+++ b/docs/DAVEAI_RC2_E2E_ROADMAP_20260801.md
@@ -0,0 +1,125 @@
+# DaveAI.tech RC2 E2E Roadmap — 2026-08-01
+
+RC2 is the “prove it repeatedly” milestone. RC1 repaired production and captured the repair as source. RC2 should convert that repair into a stable, repeatable, maintenance-safe release flow.
+
+## RC2 completion matrix
+
+| Area | RC1 state | RC2 requirement | Owner action |
+| --- | --- | --- | --- |
+| Public E2E | 6 passed, 0 failed, 1 auth skipped | 7 passed, 0 failed, 0 skipped | Provide test credentials and rerun. |
+| Chat | Public gate + signed-in manual proof works | Automated auth-chat proof in runner/CI | Add `DAVEAI_E2E_EMAIL` and `DAVEAI_E2E_PASSWORD`. |
+| Games | Carousel + TD2/Asteroids/Siege smoke green | All public games catalog-smoked; primary games have input/state assertions | Extend `vps/production-e2e-runner.cjs`. |
+| TD2 | Runtime repaired with placeholders and cache-bust | Replace placeholders or formally accept placeholders | Decide product-quality bar. |
+| StreamHub | Public render clean | Provider-auth tests with test provider account, if available | Add private provider credentials only in secure env. |
+| Windsurf | Public auth gate clean | Authenticated admin flow proof, if safe | Use admin/test token only locally. |
+| LiteLLM | Functional completion works; bare `/health` hangs | Use functional probe or fix upstream health route | Keep `check-litellm-health.sh`; investigate `/health` separately. |
+| VPS | Needs maintenance | Reboot/updates completed in maintenance window; post-reboot E2E green | Schedule downtime, snapshot, update, reboot, verify. |
+| Docs | RC1 audit + handoff docs exist | Docs updated after every RC2 proof run | Append proof run IDs and status. |
+
+## E2E architecture
+
+```mermaid
+flowchart TD
+ Runner["vps/production-e2e-runner.cjs"] --> Health["Health endpoints"]
+ Runner --> PublicChat["Unauth chat gate"]
+ Runner --> AuthChat["Authenticated chat reply requires env credentials"]
+ Runner --> Carousel["Carousel launch"]
+ Runner --> Games["TD2 / Asteroids / Siege runtime"]
+ Runner --> Pages["Studio / Web Pages / Windsurf / Voice / StreamHub"]
+ Runner --> StreamHub["StreamHub no-error render"]
+
+ Health --> Proofs["JSON + PNG proofs"]
+ PublicChat --> Proofs
+ AuthChat --> Proofs
+ Carousel --> Proofs
+ Games --> Proofs
+ Pages --> Proofs
+ StreamHub --> Proofs
+
+ Proofs --> LocalArchive["Local proofs directory not committed by default"]
+ Proofs --> ActionsArtifact["GitHub Actions artifact when workflow runs"]
+```
+
+## RC2 test commands
+
+Public:
+
+```powershell
+$env:DAVEAI_PROOF_DIR="I:\Github\Bolt.DIY\proofs\daveai-prod-e2e-rc2-public"
+npm run test:daveai:prod
+```
+
+Authenticated:
+
+```powershell
+$env:DAVEAI_E2E_EMAIL="private-test-user"
+$env:DAVEAI_E2E_PASSWORD="private-test-password"
+$env:DAVEAI_PROOF_DIR="I:\Github\Bolt.DIY\proofs\daveai-prod-e2e-rc2-auth"
+npm run test:daveai:prod
+```
+
+LiteLLM:
+
+```bash
+bash vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh
+```
+
+VPS maintenance diagnostic:
+
+```bash
+bash vps/vps-maintenance-check.sh
+```
+
+## RC2 game proof expansion
+
+Minimum public game checks:
+
+| Game group | Minimum RC2 proof |
+| --- | --- |
+| Carousel games | iframe launch + direct page render + no bad responses. |
+| TD2 | canvas present, no failed assets, click Play if stable, screenshot menu/game state. |
+| Asteroids | click Start mission, fire/move input, score/lives/wave state remains readable. |
+| Siege TD | enter name, reach menu/campaign controls, no stuck loading. |
+| Older arcade games | page render, controls visible, no bad responses/page errors. |
+| Checkers Crowning | verify title/H1 and not Wheel of Fortune. |
+
+## RC2 maintenance flow
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant StablePC as Stable coding PC
+ participant VPS
+ participant GitHub
+
+ User->>StablePC: Pull branch / PR 2186
+ StablePC->>GitHub: Verify branch and PR
+ StablePC->>VPS: Run read-only maintenance diagnostic
+ StablePC->>StablePC: Run public E2E
+ User->>StablePC: Provide test auth credentials
+ StablePC->>StablePC: Run authenticated E2E
+ User->>VPS: Approve maintenance window
+ VPS->>VPS: Snapshot, apt upgrade, reboot
+ StablePC->>VPS: Verify nginx, pm2, litellm, health
+ StablePC->>StablePC: Run full E2E again
+ StablePC->>GitHub: Push RC2 docs/test updates
+```
+
+## Known RC2 risks
+
+- The VPS has high swap use and a reboot-required marker. Maintenance may briefly disturb services.
+- Authenticated E2E needs a safe test account; using a real admin/user account in CI is risky unless scoped and rotated.
+- TD2 placeholder assets are functional but may not be final product art/audio.
+- `proofs/` can grow quickly. Archive intentionally; do not blindly commit.
+- Bare LiteLLM `/health` hangs even though readiness/liveliness/completions pass.
+
+## Definition of done for RC2
+
+RC2 is done when:
+
+1. `npm run test:daveai:prod` returns 7 passed, 0 failed, 0 skipped with test credentials.
+2. Public GitHub Actions run uploads proof artifacts.
+3. VPS maintenance is completed or explicitly deferred with owner/date.
+4. Post-maintenance public health and E2E are green.
+5. Docs are updated with latest proof paths, dates, and exact remaining items.
+6. PR is updated and ready to move from draft to review.
diff --git a/package.json b/package.json
index 5c8c2a977f2..ca97270e17d 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"build": "remix vite:build",
"dev": "node pre-start.cjs && remix vite:dev",
"test": "vitest --run",
+ "test:daveai:prod": "node vps/production-e2e-runner.cjs",
"test:watch": "vitest",
"lint": "eslint --cache --cache-location ./node_modules/.cache/eslint app",
"lint:fix": "npm run lint -- --fix && prettier app --write",
diff --git a/vps/DAVEAI_SOURCE_OF_TRUTH.md b/vps/DAVEAI_SOURCE_OF_TRUTH.md
new file mode 100644
index 00000000000..28624bff863
--- /dev/null
+++ b/vps/DAVEAI_SOURCE_OF_TRUTH.md
@@ -0,0 +1,62 @@
+# DaveAI Deployment Source of Truth
+
+The authoritative deployment inputs are:
+
+- `daveai-ui-v6.html` for both the production root page and V6 page.
+- `assets/daveai-v6.css` for the production workspace, responsive device
+ layouts, and structural layout-preset styling.
+- `patches/agent-runtime-completion-20260724-src/` for the Agent Brain runtime,
+ Nginx route, and LiteLLM configuration.
+- `daveai-sites-config.json` for the 22-site route inventory.
+- `daveai-project-catalog.json` for the 55-entry project catalog.
+- `services/daveai-subdomain-suite/` for DIY, Dev, Staging, Fleet, WebSocket,
+ and Hermes3D.
+- `services/gitea/`, `services/adminer/`, and `services/monitoring/` for the
+ three containerized production services.
+
+`daveai-ui-v6.html` must remain byte-for-byte identical to the copy in the
+Agent Brain runtime bundle. Its stylesheet reference must match the canonical
+asset under `assets/`. Run this before any deployment:
+
+```powershell
+python vps/verify-source-of-truth.py
+node vps/verify-daveai-ui-contracts.cjs vps/daveai-ui-v6.html
+node vps/verify-feature-completion-state-actions.cjs vps/daveai-ui-v6.html
+```
+
+The UI contract distinguishes static IDs from IDs explicitly assigned to
+runtime-created nodes. It fails on unresolved references and on clickable
+`div`/`span` controls that lack a role, keyboard focus, or a keyboard handler.
+Dialog backdrops are exempt only when their click handler dismisses on the
+backdrop itself; every dialog retains a keyboard-operable close control.
+
+The verified route truth is:
+
+- 14 `live` routes returning HTTP 200.
+- 8 `auth` routes whose protected root returns the Authelia redirect.
+- 0 `soon` routes.
+
+Do not infer `live` from the existence of a hostname or a configured port.
+Update the JSON source only after an HTTP route sweep and product-specific
+proof.
+
+The nine services completed on 2026-07-25 were accepted individually before
+their labels changed. Their product evidence is recorded under task
+`daveai-nine-services-completion-20260725`; the final per-service entry is
+`ev_1b374693551fbcd1`.
+
+The canvas Code/Preview switch is a workspace-surface control. It must not
+overwrite the selected global DaveAI mode. Sites `live` and `auth` labels must
+also remain visibly distinct, not only present in the DOM.
+
+Production copies:
+
+- `/var/www/agentic-website/index.html`
+- `/var/www/agentic-website/daveai-ui-v6.html`
+- `/var/www/agentic-website/assets/daveai-v6.css`
+- `/var/www/agentic-website/daveai-project-catalog.json`
+- `/opt/daveai/daveai-sites-config.json`
+- `/opt/daveai-subdomain-suite/`
+- `/opt/daveai/services/{gitea,adminer,monitoring}/`
+
+Agent runtime files are installed under `/opt/agent-brain/`.
diff --git a/vps/assets/daveai-v6.css b/vps/assets/daveai-v6.css
new file mode 100644
index 00000000000..a149d70541c
--- /dev/null
+++ b/vps/assets/daveai-v6.css
@@ -0,0 +1,8382 @@
+:root {
+ --bg-base: #020810;
+ --bg-panel: rgba(8, 14, 28, 0.8);
+ --bg-elevated: rgba(12, 18, 36, 0.97);
+ --border: rgba(255, 255, 255, 0.07);
+ --border-hi: rgba(255, 255, 255, 0.13);
+ --sv: #9333EA;
+ --sv-g: rgba(147, 51, 234, .4);
+ --cd: #2563EB;
+ --cd-g: rgba(37, 99, 235, .4);
+ --qa: #059669;
+ --qa-g: rgba(5, 150, 105, .4);
+ --as: #D97706;
+ --as-g: rgba(217, 119, 6, .4);
+ --accent: #8B5CF6;
+ --grad: linear-gradient(135deg, #9333EA, #2563EB);
+ --workspace-primary: #9333EA;
+ --workspace-secondary: #2563EB;
+ --workspace-primary-rgb: 147, 51, 234;
+ --workspace-secondary-rgb: 37, 99, 235;
+ --workspace-bg-rgb: 10, 13, 24;
+ --workspace-chat-side-w: 380px;
+ --t1: #F1F5F9;
+ --t2: #94A3B8;
+ --t3: #475569;
+ --fd: 'Syne', sans-serif;
+ --fb: 'DM Sans', sans-serif;
+ --topbar-h: 46px;
+ --statusbar-h: 26px;
+ --modebar-h: 36px;
+ --left-rail-w: 48px;
+ --right-panel-w: 360px;
+ --right-panel-min-w: 320px;
+ --right-panel-max-w: min(48vw, 640px);
+ --composer-h: 48px;
+ --voicebar-h: 28px;
+ --transcript-max-h: clamp(150px, 30dvh, 360px);
+ --coding-action-min-h: clamp(300px, 46dvh, 520px);
+ --coding-transcript-min-h: clamp(170px, 22dvh, 280px);
+ --coding-transcript-max-h: clamp(230px, 32dvh, 390px);
+ --action-window-min-h: 260px;
+ --action-window-max-w: 100%;
+ }
+
+ *,
+ *::before,
+ *::after {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ body {
+ background: var(--bg-base);
+ background-image:
+ radial-gradient(ellipse 55% 35% at 12% 12%, rgba(var(--workspace-primary-rgb), 0.1) 0%, transparent 55%),
+ radial-gradient(ellipse 45% 45% at 88% 82%, rgba(var(--workspace-secondary-rgb), 0.08) 0%, transparent 55%),
+ radial-gradient(#172040 1px, transparent 0);
+ background-size: auto, auto, 26px 26px;
+ color: var(--t1);
+ font-family: var(--fb);
+ height: 100vh;
+ height: 100dvh;
+ width: 100%;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .glass {
+ background: var(--bg-panel);
+ backdrop-filter: blur(18px);
+ -webkit-backdrop-filter: blur(18px);
+ border: 1px solid var(--border);
+ }
+
+ .glass-hi {
+ background: var(--bg-elevated);
+ backdrop-filter: blur(24px);
+ -webkit-backdrop-filter: blur(24px);
+ border: 1px solid var(--border-hi);
+ }
+
+ .hid {
+ display: none !important;
+ }
+
+ [role="button"][tabindex]:focus-visible,
+ [role="tab"][tabindex]:focus-visible,
+ [role="radio"][tabindex]:focus-visible,
+ [role="option"][tabindex]:focus-visible,
+ .site-item:focus-visible {
+ outline: 2px solid var(--workspace-primary);
+ outline-offset: 2px;
+ }
+
+ /* ══ TOP BAR ══ */
+ #topbar {
+ height: var(--topbar-h);
+ display: flex;
+ align-items: center;
+ padding: 0 10px;
+ gap: 6px;
+ border-bottom: 1px solid var(--border);
+ background: rgba(var(--workspace-bg-rgb), 0.95);
+ backdrop-filter: blur(22px);
+ flex-shrink: 0;
+ z-index: 50;
+ }
+
+ .logo {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ text-decoration: none;
+ }
+
+ .logo-icon {
+ width: 26px;
+ height: 26px;
+ border-radius: 7px;
+ background: var(--grad);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-family: var(--fd);
+ font-weight: 800;
+ font-size: 12px;
+ color: white;
+ box-shadow: 0 0 16px var(--sv-g);
+ flex-shrink: 0;
+ }
+
+ .logo-text {
+ font-family: var(--fd);
+ font-weight: 700;
+ font-size: 14px;
+ color: var(--t1);
+ }
+
+ .vdiv {
+ width: 1px;
+ height: 18px;
+ background: var(--border);
+ flex-shrink: 0;
+ }
+
+ /* Agent pills */
+ .agent-pills {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ }
+
+ .pill {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 4px 8px 4px 6px;
+ border-radius: 18px;
+ font-size: 11px;
+ font-weight: 500;
+ cursor: pointer;
+ border: 1px solid transparent;
+ transition: all 0.2s;
+ white-space: nowrap;
+ }
+
+ .pill:hover {
+ background: rgba(255, 255, 255, 0.04);
+ }
+
+ .pill-dot {
+ width: 7px;
+ height: 7px;
+ border-radius: 50%;
+ transition: all 0.3s;
+ flex-shrink: 0;
+ }
+
+ .pill-sub {
+ font-size: 9.5px;
+ opacity: 0.5;
+ margin-left: 2px;
+ }
+
+ .p-sv {
+ color: #D8B4FE;
+ }
+
+ .p-sv .pill-dot {
+ background: var(--sv);
+ }
+
+ .p-sv:hover {
+ border-color: rgba(147, 51, 234, .3);
+ }
+
+ .p-cd {
+ color: #93C5FD;
+ }
+
+ .p-cd .pill-dot {
+ background: var(--cd);
+ }
+
+ .p-cd:hover {
+ border-color: rgba(37, 99, 235, .3);
+ }
+
+ .p-qa {
+ color: #6EE7B7;
+ }
+
+ .p-qa .pill-dot {
+ background: var(--qa);
+ }
+
+ .p-qa:hover {
+ border-color: rgba(5, 150, 105, .3);
+ }
+
+ .p-as {
+ color: #FCD34D;
+ }
+
+ .p-as .pill-dot {
+ background: var(--as);
+ }
+
+ .p-as:hover {
+ border-color: rgba(217, 119, 6, .3);
+ }
+
+ .s-idle .pill-dot {
+ opacity: 0.3;
+ }
+
+ .s-think .pill-dot {
+ animation: pthink 1.4s ease-in-out infinite;
+ }
+
+ .s-work .pill-dot {
+ animation: pwork .85s linear infinite;
+ border-radius: 2px;
+ }
+
+ .s-done .pill-dot {
+ animation: pdone .7s ease-out forwards;
+ }
+
+ @keyframes pthink {
+
+ 0%,
+ 100% {
+ transform: scale(1);
+ opacity: .8;
+ }
+
+ 50% {
+ transform: scale(1.45);
+ opacity: 1;
+ box-shadow: 0 0 8px 3px currentColor;
+ }
+ }
+
+ @keyframes pwork {
+ 0% {
+ transform: rotate(0) scale(1.1);
+ border-radius: 2px;
+ }
+
+ 50% {
+ transform: rotate(180deg) scale(.9);
+ border-radius: 50%;
+ }
+
+ 100% {
+ transform: rotate(360deg) scale(1.1);
+ border-radius: 2px;
+ }
+ }
+
+ @keyframes pdone {
+ 0% {
+ box-shadow: 0 0 14px 5px currentColor;
+ transform: scale(1.6);
+ }
+
+ 100% {
+ box-shadow: none;
+ transform: scale(1);
+ }
+ }
+
+ /* Panel tabs */
+ .panel-tabs {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ }
+
+ .ptab {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 9px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--t3);
+ cursor: pointer;
+ border: none;
+ background: transparent;
+ transition: all .15s;
+ white-space: nowrap;
+ }
+
+ .ptab:hover {
+ color: var(--t2);
+ background: rgba(255, 255, 255, .04);
+ }
+
+ .ptab.on {
+ color: var(--t1);
+ background: rgba(255, 255, 255, .08);
+ }
+
+ .ptab .badge {
+ background: rgba(139, 92, 246, .7);
+ color: white;
+ border-radius: 10px;
+ padding: 0 5px;
+ font-size: 9px;
+ line-height: 14px;
+ }
+
+ /* Topbar right */
+ .tb-right {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ }
+
+ .tb-btn {
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--t3);
+ cursor: pointer;
+ border: none;
+ background: transparent;
+ transition: all 0.2s;
+ }
+
+ .tb-btn:hover {
+ background: rgba(255, 255, 255, .07);
+ color: var(--t2);
+ }
+
+ .tb-btn.on {
+ background: rgba(147, 51, 234, .15);
+ color: #D8B4FE;
+ }
+
+ .layout-btn {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0 9px;
+ height: 28px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--t2);
+ cursor: pointer;
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, .02);
+ transition: all 0.2s;
+ }
+
+ .layout-btn:hover {
+ border-color: var(--border-hi);
+ color: var(--t1);
+ }
+
+ .avatar {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: var(--grad);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ font-weight: 700;
+ font-family: var(--fd);
+ cursor: pointer;
+ box-shadow: 0 0 12px var(--sv-g);
+ transition: all 0.2s;
+ position: relative;
+ }
+
+ .avatar:hover {
+ transform: scale(1.08);
+ box-shadow: 0 0 20px var(--sv-g);
+ }
+
+ .dropdown {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ width: 170px;
+ border-radius: 10px;
+ padding: 5px;
+ z-index: 300;
+ display: none;
+ }
+
+ .dropdown.open {
+ display: block;
+ animation: din .15s ease-out;
+ }
+
+ @keyframes din {
+ from {
+ opacity: 0;
+ transform: translateY(-5px) scale(.97);
+ }
+
+ to {
+ opacity: 1;
+ transform: none;
+ }
+ }
+
+ .dd-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 7px 9px;
+ border-radius: 7px;
+ font-size: 12px;
+ color: var(--t2);
+ cursor: pointer;
+ transition: all .15s;
+ }
+
+ .dd-item:hover {
+ background: rgba(255, 255, 255, .06);
+ color: var(--t1);
+ }
+
+ .dd-div {
+ height: 1px;
+ background: var(--border);
+ margin: 4px 0;
+ }
+
+ /* ══ MAIN LAYOUT ══ */
+ #main {
+ flex: 1;
+ display: flex;
+ overflow: hidden;
+ position: relative;
+ }
+
+ /* ══ LEFT SIDEBAR ══ */
+ #lsb {
+ width: var(--left-rail-w);
+ order: 0;
+ flex-shrink: 0;
+ background: rgba(var(--workspace-bg-rgb), 0.78);
+ border-right: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 6px 0;
+ gap: 2px;
+ z-index: 40;
+ }
+
+ .sb-btn {
+ width: 34px;
+ height: 34px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--t3);
+ cursor: pointer;
+ transition: all 0.2s;
+ position: relative;
+ border: none;
+ background: transparent;
+ }
+
+ .sb-btn:hover {
+ background: rgba(255, 255, 255, .07);
+ color: var(--t2);
+ }
+
+ .sb-btn.on {
+ background: rgba(147, 51, 234, .15);
+ color: #D8B4FE;
+ }
+
+ .sb-btn::after {
+ content: attr(data-tip);
+ position: absolute;
+ left: calc(100% + 8px);
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(10, 16, 32, .99);
+ color: var(--t1);
+ font-size: 11px;
+ font-weight: 500;
+ padding: 3px 8px;
+ border-radius: 5px;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity .12s;
+ border: 1px solid var(--border);
+ z-index: 200;
+ }
+
+ .sb-btn:hover::after {
+ opacity: 1;
+ }
+
+ .sb-div {
+ width: 24px;
+ height: 1px;
+ background: var(--border);
+ margin: 3px 0;
+ }
+
+ /* Device mode buttons in sidebar */
+ .dev-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 2px;
+ width: 100%;
+ padding: 4px 0;
+ border-top: 1px solid var(--border);
+ border-bottom: 1px solid var(--border);
+ margin: 2px 0;
+ }
+
+ .dev-btn {
+ width: 30px;
+ height: 26px;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--t3);
+ cursor: pointer;
+ transition: all 0.18s;
+ border: none;
+ background: transparent;
+ font-size: 11px;
+ position: relative;
+ }
+
+ .dev-btn::after {
+ content: attr(data-tip);
+ position: absolute;
+ left: calc(100% + 8px);
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(10, 16, 32, .99);
+ color: var(--t1);
+ font-size: 11px;
+ font-weight: 500;
+ padding: 3px 8px;
+ border-radius: 5px;
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity .12s;
+ border: 1px solid var(--border);
+ z-index: 200;
+ }
+
+ .dev-btn:hover::after {
+ opacity: 1;
+ }
+
+ .dev-btn:hover {
+ background: rgba(255, 255, 255, .06);
+ color: var(--t2);
+ }
+
+ .dev-btn.on {
+ background: rgba(37, 99, 235, .2);
+ color: #60A5FA;
+ }
+
+ /* ══ FLY-OUT PANEL ══ */
+ #pp {
+ position: absolute;
+ left: var(--left-rail-w);
+ top: 0;
+ bottom: 0;
+ width: 280px;
+ z-index: 35;
+ border-right: 1px solid rgba(147, 51, 234, .15);
+ background: rgba(5, 9, 22, 0.98);
+ backdrop-filter: blur(22px);
+ transform: translateX(-100%);
+ transition: transform .28s cubic-bezier(.4, 0, .2, 1);
+ display: flex;
+ flex-direction: column;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ }
+
+ #pp.open {
+ transform: translateX(0);
+ }
+
+ /* Pull-tab to reveal hidden sidebar */
+ #pp-pull-tab {
+ position: absolute;
+ left: var(--left-rail-w);
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 34;
+ width: 14px;
+ height: 56px;
+ background: rgba(147, 51, 234, .12);
+ border: 1px solid rgba(147, 51, 234, .2);
+ border-left: none;
+ border-radius: 0 8px 8px 0;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all .2s;
+ opacity: .5;
+ }
+
+ #pp-pull-tab:hover {
+ opacity: 1;
+ width: 18px;
+ background: rgba(147, 51, 234, .25);
+ }
+
+ #pp-pull-tab i {
+ font-size: 8px;
+ color: #D8B4FE;
+ }
+
+ #pp-pull-tab.hidden {
+ display: none;
+ }
+
+ /* Auto-hide timer bar at top of sidebar */
+ #pp-timer-bar {
+ height: 2px;
+ background: linear-gradient(90deg, #7C3AED, #A78BFA);
+ width: 100%;
+ flex-shrink: 0;
+ transition: width linear;
+ opacity: .6;
+ }
+
+ .pp-head {
+ padding: 10px 12px 8px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-shrink: 0;
+ }
+
+ .pp-head h3 {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .1em;
+ color: var(--t3);
+ }
+
+ .fp-tabs {
+ display: flex;
+ gap: 2px;
+ padding: 6px 8px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ }
+
+ .fp-tab {
+ flex: 1;
+ padding: 4px 6px;
+ border-radius: 5px;
+ font-size: 10.5px;
+ font-weight: 600;
+ text-align: center;
+ cursor: pointer;
+ transition: all .15s;
+ color: var(--t3);
+ border: none;
+ background: transparent;
+ }
+
+ .fp-tab.on {
+ background: rgba(147, 51, 234, .18);
+ color: #D8B4FE;
+ }
+
+ .fp-tab:hover:not(.on) {
+ background: rgba(255, 255, 255, .04);
+ color: var(--t2);
+ }
+
+ .fp-body {
+ flex: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ }
+
+ /* Page/Site items */
+ .site-item,
+ .page-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 12px;
+ cursor: pointer;
+ transition: background .12s;
+ font-size: 12px;
+ color: var(--t2);
+ }
+
+ .site-item {
+ text-decoration: none;
+ }
+
+ .site-item:hover,
+ .page-item:hover {
+ background: rgba(255, 255, 255, .04);
+ color: var(--t1);
+ }
+
+ .site-item.on,
+ .page-item.on {
+ background: rgba(147, 51, 234, .1);
+ color: #D8B4FE;
+ border-left: 2px solid var(--sv);
+ padding-left: 10px;
+ }
+
+ .site-dot,
+ .pdot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--t3);
+ flex-shrink: 0;
+ }
+
+ .site-item.on .site-dot,
+ .page-item.on .pdot {
+ background: var(--sv);
+ }
+
+ .site-badge {
+ margin-left: auto;
+ font-size: 9px;
+ padding: 1px 5px;
+ border-radius: 3px;
+ background: rgba(5, 150, 105, .18);
+ color: #6EE7B7;
+ }
+
+ .pp-path {
+ margin-left: auto;
+ font-size: 9.5px;
+ color: var(--t3);
+ font-family: monospace;
+ }
+
+ /* ══ PROJECTS PANEL ══ */
+ .proj-cats {
+ display: flex;
+ gap: 3px;
+ padding: 6px 8px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ overflow-x: hidden;
+ }
+
+ .proj-cat {
+ flex: 1;
+ padding: 5px 6px;
+ border-radius: 6px;
+ font-size: 10px;
+ font-weight: 600;
+ text-align: center;
+ cursor: pointer;
+ color: var(--t3);
+ border: none;
+ background: transparent;
+ transition: all .15s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 3px;
+ white-space: nowrap;
+ min-width: 0;
+ }
+
+ .proj-cat i {
+ font-size: 9px;
+ }
+
+ .proj-cat.on {
+ background: rgba(139, 92, 246, .18);
+ color: #D8B4FE;
+ }
+
+ .proj-cat:hover:not(.on) {
+ background: rgba(255, 255, 255, .04);
+ color: var(--t2);
+ }
+
+ .proj-cat .pcat-count {
+ font-size: 8px;
+ background: rgba(255, 255, 255, .08);
+ border-radius: 8px;
+ padding: 1px 4px;
+ min-width: 14px;
+ text-align: center;
+ }
+
+ .proj-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ font-size: 11px;
+ cursor: pointer;
+ color: var(--t2);
+ transition: all .15s;
+ border-left: 2px solid transparent;
+ }
+
+ .proj-item:hover {
+ background: rgba(255, 255, 255, .04);
+ color: var(--t1);
+ }
+
+ .proj-item.on {
+ background: rgba(139, 92, 246, .1);
+ color: #D8B4FE;
+ border-left-color: var(--sv);
+ }
+
+ .proj-icon {
+ width: 28px;
+ height: 28px;
+ border-radius: 7px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 12px;
+ flex-shrink: 0;
+ }
+
+ .proj-icon.web {
+ background: rgba(96, 165, 250, .15);
+ color: #60A5FA;
+ }
+
+ .proj-icon.apps {
+ background: rgba(167, 139, 250, .15);
+ color: #A78BFA;
+ }
+
+ .proj-icon.games {
+ background: rgba(251, 146, 60, .15);
+ color: #FB923C;
+ }
+
+ .proj-icon.other {
+ background: rgba(148, 163, 184, .15);
+ color: #94A3B8;
+ }
+
+ .proj-meta {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .proj-name {
+ font-size: 11px;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .proj-url {
+ font-size: 9px;
+ color: var(--t3);
+ font-family: monospace;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .proj-badge {
+ font-size: 8px;
+ padding: 1px 5px;
+ border-radius: 3px;
+ flex-shrink: 0;
+ text-transform: uppercase;
+ font-weight: 700;
+ letter-spacing: .04em;
+ }
+
+ .proj-badge.live {
+ background: rgba(5, 150, 105, .18);
+ color: #6EE7B7;
+ }
+
+ .proj-badge.draft {
+ background: rgba(251, 146, 60, .18);
+ color: #FCD34D;
+ }
+
+ .proj-del {
+ opacity: 0;
+ font-size: 9px;
+ color: var(--t3);
+ cursor: pointer;
+ background: none;
+ border: none;
+ padding: 2px 4px;
+ transition: all .15s;
+ pointer-events: none;
+ }
+
+ .proj-item:hover .proj-del,
+ .proj-del:focus-visible {
+ opacity: 1;
+ pointer-events: auto;
+ }
+
+ .proj-del:hover {
+ color: #F87171;
+ }
+
+ .proj-add-form {
+ padding: 8px 12px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ .proj-add-form input,
+ .proj-add-form select {
+ width: 100%;
+ padding: 5px 8px;
+ font-size: 10.5px;
+ background: rgba(255, 255, 255, .04);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--t1);
+ font-family: 'DM Sans', sans-serif;
+ }
+
+ .proj-add-form input::placeholder {
+ color: var(--t3);
+ }
+
+ .proj-add-form select {
+ appearance: none;
+ cursor: pointer;
+ }
+
+ .proj-add-btns {
+ display: flex;
+ gap: 6px;
+ }
+
+ .proj-add-btns button {
+ flex: 1;
+ padding: 5px 8px;
+ border-radius: 6px;
+ font-size: 10px;
+ font-weight: 600;
+ cursor: pointer;
+ border: none;
+ font-family: 'DM Sans', sans-serif;
+ }
+
+ .proj-save-btn {
+ background: rgba(139, 92, 246, .3);
+ color: #D8B4FE;
+ }
+
+ .proj-save-btn:hover {
+ background: rgba(139, 92, 246, .5);
+ }
+
+ .proj-cancel-btn {
+ background: rgba(255, 255, 255, .05);
+ color: var(--t3);
+ }
+
+ .proj-cancel-btn:hover {
+ background: rgba(255, 255, 255, .1);
+ color: var(--t2);
+ }
+
+ .proj-empty {
+ padding: 20px 14px;
+ text-align: center;
+ color: var(--t3);
+ font-size: 10.5px;
+ }
+
+ .proj-empty i {
+ font-size: 20px;
+ display: block;
+ margin-bottom: 8px;
+ opacity: .3;
+ }
+
+ /* ══ TOOLS PANEL ══ */
+ .tool-search {
+ padding: 8px 10px 4px;
+ }
+
+ .tool-search input {
+ width: 100%;
+ padding: 5px 9px;
+ background: rgba(255, 255, 255, .04);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--t1);
+ font-size: 11px;
+ font-family: var(--fb);
+ outline: none;
+ transition: border-color .15s;
+ }
+
+ .tool-search input:focus {
+ border-color: rgba(147, 51, 234, .4);
+ }
+
+ .tool-role-btns {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 3px;
+ padding: 4px 10px 6px;
+ }
+
+ .tool-runtime-status {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 2px 10px 6px;
+ color: var(--t3);
+ font-size: 9px;
+ }
+
+ #fp-tool-count {
+ color: #D8B4FE;
+ }
+
+ .trb {
+ padding: 2px 7px;
+ border-radius: 4px;
+ font-size: 10px;
+ font-weight: 600;
+ cursor: pointer;
+ border: none;
+ background: rgba(255, 255, 255, .05);
+ color: var(--t3);
+ transition: all .15s;
+ }
+
+ .trb:hover {
+ color: var(--t2);
+ }
+
+ .trb.on {
+ background: rgba(139, 92, 246, .25);
+ color: #D8B4FE;
+ }
+
+ /* Tool category */
+ .tool-cat {
+ padding: 5px 10px 3px;
+ font-size: 9.5px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: .08em;
+ color: var(--t3);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ cursor: pointer;
+ user-select: none;
+ }
+
+ .tool-cat:hover {
+ color: var(--t2);
+ }
+
+ .tool-cat-count {
+ font-size: 9px;
+ background: rgba(255, 255, 255, .06);
+ padding: 1px 5px;
+ border-radius: 9px;
+ color: var(--t3);
+ }
+
+ .tool-cat-body {
+ overflow: hidden;
+ transition: max-height .2s ease;
+ }
+
+ .tool-cat-body.collapsed {
+ max-height: 0 !important;
+ }
+
+ /* Tool item */
+ .tool-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 8px;
+ padding: 6px 10px;
+ cursor: pointer;
+ transition: background .12s;
+ border-radius: 0;
+ }
+
+ .tool-item:hover {
+ background: rgba(255, 255, 255, .04);
+ }
+
+ .tool-item:hover .tool-run {
+ opacity: 1;
+ }
+
+ .tool-icon {
+ width: 24px;
+ height: 24px;
+ border-radius: 5px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ flex-shrink: 0;
+ margin-top: 1px;
+ }
+
+ .tool-info {
+ flex: 1;
+ min-width: 0;
+ }
+
+ .tool-name {
+ font-size: 10.5px;
+ font-weight: 600;
+ color: var(--t1);
+ font-family: monospace;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .tool-desc {
+ font-size: 10px;
+ color: var(--t3);
+ line-height: 1.4;
+ margin-top: 1px;
+ }
+
+ .tool-badges {
+ display: flex;
+ gap: 3px;
+ margin-top: 3px;
+ flex-wrap: wrap;
+ }
+
+ .tool-badge {
+ font-size: 8.5px;
+ padding: 1px 5px;
+ border-radius: 3px;
+ font-weight: 600;
+ }
+
+ .tb-sv {
+ background: rgba(147, 51, 234, .18);
+ color: #D8B4FE;
+ }
+
+ .tb-cd {
+ background: rgba(37, 99, 235, .18);
+ color: #93C5FD;
+ }
+
+ .tb-qa {
+ background: rgba(5, 150, 105, .18);
+ color: #6EE7B7;
+ }
+
+ .tb-as {
+ background: rgba(217, 119, 6, .18);
+ color: #FCD34D;
+ }
+
+ .tool-run {
+ opacity: 0;
+ background: rgba(147, 51, 234, .25);
+ border: 1px solid rgba(147, 51, 234, .3);
+ color: #D8B4FE;
+ font-size: 9px;
+ font-weight: 600;
+ padding: 2px 7px;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: all .15s;
+ flex-shrink: 0;
+ margin-top: 2px;
+ }
+
+ .tool-run:hover {
+ background: rgba(147, 51, 234, .4);
+ }
+
+ /* Agent item */
+ .agent-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, .03);
+ }
+
+ .ag-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ }
+
+ .ag-role {
+ font-size: 11px;
+ font-weight: 600;
+ width: 62px;
+ }
+
+ .ag-task {
+ font-size: 10px;
+ color: var(--t3);
+ flex: 1;
+ }
+
+ .ag-state {
+ font-size: 9px;
+ padding: 2px 6px;
+ border-radius: 9px;
+ background: rgba(255, 255, 255, .06);
+ color: var(--t3);
+ }
+
+ /* ══ CANVAS ══ */
+ #canvas {
+ flex: 1;
+ order: 1;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ background: rgba(var(--workspace-bg-rgb), 0.52);
+ position: relative;
+ }
+
+ #ctb {
+ height: var(--modebar-h);
+ display: flex;
+ align-items: center;
+ padding: 0 8px;
+ gap: 3px;
+ border-bottom: 1px solid var(--border);
+ background: rgba(3, 6, 16, 0.8);
+ flex-shrink: 0;
+ }
+
+ .ctab {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 9px;
+ border-radius: 5px;
+ font-size: 10.5px;
+ font-weight: 600;
+ color: var(--t3);
+ cursor: pointer;
+ transition: all .15s;
+ white-space: nowrap;
+ border: none;
+ background: transparent;
+ }
+
+ .ctab:hover {
+ color: var(--t2);
+ background: rgba(255, 255, 255, .04);
+ }
+
+ .ctab.on {
+ color: var(--t1);
+ background: rgba(255, 255, 255, .09);
+ }
+
+ .ctab.dev-mode {
+ padding: 4px 7px;
+ }
+
+ /* device mode indicator */
+ .dm-indicator {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ margin-left: 6px;
+ padding: 3px 8px;
+ background: rgba(37, 99, 235, .1);
+ border: 1px solid rgba(37, 99, 235, .2);
+ border-radius: 12px;
+ font-size: 9.5px;
+ color: #60A5FA;
+ font-weight: 600;
+ }
+
+ .curl {
+ font-size: 10px;
+ color: var(--t3);
+ font-family: monospace;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 0 6px;
+ }
+
+ #panel-area {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ position: relative;
+ min-height: var(--action-window-min-h);
+ }
+
+ #bframe {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ margin: 8px 8px 0;
+ border-radius: 10px;
+ overflow: hidden;
+ border: 1px solid var(--border);
+ box-shadow: 0 0 50px rgba(0, 0, 0, .55);
+ background: #08101E;
+ position: relative;
+ min-width: 0;
+ min-height: var(--action-window-min-h);
+ max-width: var(--action-window-max-w);
+ transition: width .3s ease, margin .3s ease;
+ }
+
+ /* Device mode frames */
+ .dm-desktop #bframe {
+ align-self: stretch;
+ flex: 1 1 auto;
+ width: calc(100% - 16px);
+ max-width: calc(100% - 16px);
+ margin: 8px 8px 0;
+ }
+
+ .dm-tablet #bframe {
+ align-self: center;
+ flex: 1 1 auto;
+ min-width: 0;
+ width: 768px;
+ max-width: calc(100% - 16px);
+ margin: 8px auto 0;
+ }
+
+ .dm-android #bframe {
+ align-self: center;
+ flex: 1 1 auto;
+ min-width: 0;
+ width: 412px;
+ max-width: calc(100% - 16px);
+ margin: 8px auto 0;
+ }
+
+ .dm-mobile #bframe {
+ align-self: center;
+ flex: 1 1 auto;
+ min-width: 0;
+ width: 375px;
+ max-width: calc(100% - 16px);
+ margin: 8px auto 0;
+ }
+
+ /* Device chrome ring */
+ .dm-tablet .frame-chrome,
+ .dm-android .frame-chrome,
+ .dm-mobile .frame-chrome {
+ display: flex;
+ }
+
+ .frame-chrome {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ height: 28px;
+ background: rgba(255, 255, 255, .03);
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ }
+
+ .fc-notch {
+ width: 60px;
+ height: 6px;
+ background: rgba(255, 255, 255, .12);
+ border-radius: 3px;
+ }
+
+ /* Preview inner */
+ .btb {
+ height: 32px;
+ background: rgba(6, 10, 24, 0.9);
+ display: flex;
+ align-items: center;
+ padding: 0 10px;
+ gap: 8px;
+ flex-shrink: 0;
+ }
+
+ .bdots {
+ display: flex;
+ gap: 5px;
+ align-items: center;
+ }
+
+ .bd {
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ }
+
+ .bd-r {
+ background: #EF4444;
+ opacity: .6;
+ }
+
+ .bd-y {
+ background: #EAB308;
+ opacity: .6;
+ }
+
+ .bd-g {
+ background: #22C55E;
+ opacity: .6;
+ }
+
+ .burl {
+ flex: 1;
+ text-align: center;
+ font-size: 10px;
+ color: var(--t3);
+ font-family: monospace;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ }
+
+ #lp {
+ flex: 1;
+ overflow-y: auto;
+ background: linear-gradient(160deg, #050B1A 0%, #080F22 60%, #040A16 100%);
+ position: relative;
+ }
+
+ #pc {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 48px 24px 40px;
+ min-height: 100%;
+ }
+
+ /* preview page elements */
+ .ph {
+ text-align: center;
+ max-width: 620px;
+ }
+
+ .p-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 12px;
+ border-radius: 20px;
+ border: 1px solid rgba(147, 51, 234, .3);
+ background: rgba(147, 51, 234, .08);
+ font-size: 10.5px;
+ font-weight: 600;
+ color: #D8B4FE;
+ margin-bottom: 16px;
+ cursor: pointer;
+ }
+
+ .p-title {
+ font-family: var(--fd);
+ font-size: clamp(28px, 4.5vw, 52px);
+ font-weight: 800;
+ background: linear-gradient(135deg, #F1F5F9 30%, #94A3B8 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ color: transparent;
+ line-height: 1.12;
+ margin-bottom: 14px;
+ cursor: pointer;
+ }
+
+ .p-sub {
+ font-size: clamp(13px, 1.5vw, 16px);
+ color: #4E6070;
+ line-height: 1.65;
+ max-width: 500px;
+ margin: 0 auto 26px;
+ cursor: pointer;
+ }
+
+ .p-acts {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+ flex-wrap: wrap;
+ }
+
+ .pb1 {
+ padding: 11px 28px;
+ border-radius: 10px;
+ background: var(--grad);
+ color: white;
+ font-weight: 700;
+ font-size: 13.5px;
+ border: none;
+ cursor: pointer;
+ box-shadow: 0 0 28px rgba(147, 51, 234, .45);
+ transition: all .2s;
+ }
+
+ .pb1:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 0 40px rgba(147, 51, 234, .6);
+ }
+
+ .pb2 {
+ padding: 11px 28px;
+ border-radius: 10px;
+ background: rgba(255, 255, 255, .05);
+ color: var(--t2);
+ font-weight: 600;
+ font-size: 13.5px;
+ border: 1px solid var(--border);
+ cursor: pointer;
+ transition: all .2s;
+ }
+
+ .pb2:hover {
+ border-color: var(--border-hi);
+ color: var(--t1);
+ }
+
+ .ed {
+ transition: outline .15s;
+ outline: 2px solid transparent;
+ outline-offset: 3px;
+ }
+
+ .ed:hover {
+ outline-color: rgba(147, 51, 234, .5);
+ }
+
+ .hf {
+ animation: hfl .85s ease-out;
+ }
+
+ @keyframes hfl {
+ 0% {
+ outline-color: rgba(139, 92, 246, .9);
+ }
+
+ 100% {
+ outline-color: transparent;
+ }
+ }
+
+ @keyframes fiu {
+ from {
+ opacity: 0;
+ transform: translateY(14px);
+ }
+
+ to {
+ opacity: 1;
+ transform: none;
+ }
+ }
+
+ /* Build status bar */
+ #bst {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ padding: 4px 12px;
+ font-size: 10px;
+ font-weight: 600;
+ color: #6EE7B7;
+ background: rgba(5, 150, 105, .1);
+ border-top: 1px solid rgba(5, 150, 105, .15);
+ display: none;
+ align-items: center;
+ gap: 6px;
+ }
+
+ /* Panel content */
+ .panel-content {
+ flex: 1;
+ overflow-y: auto;
+ display: none;
+ flex-direction: column;
+ animation: panelFadeIn .2s ease;
+ }
+
+ .panel-content.vis {
+ display: flex !important;
+ }
+
+ @keyframes panelFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(4px);
+ }
+
+ to {
+ opacity: 1;
+ transform: none;
+ }
+ }
+
+ .pc-header {
+ padding: 11px 14px;
+ border-bottom: 1px solid var(--border);
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--t2);
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ flex-shrink: 0;
+ }
+
+ /* Build/Analytics/Admin panels */
+ .bt-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 10px;
+ padding: 8px 14px;
+ border-bottom: 1px solid rgba(255, 255, 255, .03);
+ }
+
+ .bt-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ margin-top: 3px;
+ }
+
+ .bt-status {
+ display: inline-flex;
+ align-items: center;
+ font-size: 9.5px;
+ padding: 1px 6px;
+ border-radius: 3px;
+ font-weight: 700;
+ background: rgba(255, 255, 255, .06);
+ color: var(--t3);
+ margin-bottom: 2px;
+ }
+
+ .bt-status.pass {
+ background: rgba(5, 150, 105, .15);
+ color: #6EE7B7;
+ }
+
+ .bt-meta {
+ font-size: 9.5px;
+ color: var(--t3);
+ margin-top: 2px;
+ font-family: monospace;
+ }
+
+ .bt-meta.bt-commit {
+ color: var(--qa);
+ }
+
+ .ana-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px;
+ padding: 12px 14px;
+ }
+
+ .ana-card {
+ background: rgba(255, 255, 255, .03);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 10px 12px;
+ }
+
+ .ana-label {
+ font-size: 10px;
+ color: var(--t3);
+ margin-bottom: 4px;
+ }
+
+ .ana-val {
+ font-size: 22px;
+ font-weight: 800;
+ font-family: var(--fd);
+ line-height: 1;
+ }
+
+ .ana-sub {
+ font-size: 9.5px;
+ color: var(--t3);
+ margin-top: 3px;
+ }
+
+ .ana-link {
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ padding: 7px 14px;
+ font-size: 11px;
+ color: var(--t2);
+ border-bottom: 1px solid rgba(255, 255, 255, .03);
+ text-decoration: none;
+ transition: color .15s;
+ }
+
+ .ana-link:hover {
+ color: var(--t1);
+ background: rgba(255, 255, 255, .03);
+ }
+
+ .adm-tabs {
+ display: flex;
+ gap: 4px;
+ padding: 8px 14px;
+ border-bottom: 1px solid var(--border);
+ flex-shrink: 0;
+ }
+
+ .adm-tab {
+ padding: 4px 10px;
+ border-radius: 5px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--t3);
+ cursor: pointer;
+ border: none;
+ background: transparent;
+ transition: all .15s;
+ }
+
+ .adm-tab.on {
+ background: rgba(147, 51, 234, .18);
+ color: #D8B4FE;
+ }
+
+ .adm-section {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .adm-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 6px 0;
+ border-bottom: 1px solid rgba(255, 255, 255, .04);
+ font-size: 11px;
+ }
+
+ .adm-row:last-child {
+ border-bottom: none;
+ }
+
+ .adm-row span {
+ color: var(--t3);
+ }
+
+ .adm-row code {
+ font-family: monospace;
+ font-size: 10px;
+ color: var(--t2);
+ background: rgba(255, 255, 255, .05);
+ padding: 1px 5px;
+ border-radius: 3px;
+ }
+
+ /* Chat */
+ #chat {
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ max-height: calc(var(--transcript-max-h) + var(--voicebar-h) + var(--composer-h) + 16px);
+ border-top: 1px solid var(--border);
+ background: rgba(3, 6, 16, 0.85);
+ padding: 0 0 6px;
+ overflow: hidden;
+ }
+
+ #chat-feed {
+ flex: 0 1 auto;
+ max-height: var(--transcript-max-h) !important;
+ min-height: 0;
+ overflow-y: auto !important;
+ overflow-x: hidden;
+ box-sizing: border-box;
+ padding-bottom: 12px;
+ scroll-padding-bottom: 12px;
+ }
+
+ #chat-feed > .cb:last-child {
+ margin-bottom: 4px;
+ }
+
+ #chat-feed .cb {
+ max-width: min(100%, 1120px);
+ overflow: visible;
+ }
+
+ #chat-feed .cb-inner {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ line-height: 1.55;
+ max-height: none;
+ }
+
+ #chat-feed .cb-artifact-summary .cb-inner {
+ border-color: rgba(96, 165, 250, .22) !important;
+ background: rgba(37, 99, 235, .08) !important;
+ color: var(--t2);
+ font-size: 11.5px;
+ }
+
+ body.has-chat-transcript #panel-area {
+ min-height: var(--coding-action-min-h);
+ }
+
+ body.has-chat-transcript #bframe {
+ min-height: calc(var(--coding-action-min-h) - 10px);
+ }
+
+ body.has-chat-transcript #chat {
+ max-height: min(44dvh, calc(var(--coding-transcript-max-h) + 120px));
+ }
+
+ body.has-chat-transcript #chat-feed {
+ display: flex !important;
+ min-height: var(--coding-transcript-min-h);
+ max-height: var(--coding-transcript-max-h) !important;
+ }
+
+ body.has-chat-transcript #chat-feed:empty {
+ display: none !important;
+ min-height: 0;
+ max-height: 0 !important;
+ }
+
+ #ts {
+ padding: 6px 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, .04);
+ display: none;
+ align-items: center;
+ gap: 8px;
+ font-size: 11px;
+ }
+
+ .ta {
+ font-weight: 700;
+ }
+
+ #ts.vis {
+ display: flex;
+ }
+
+ .ts-ic {
+ color: var(--sv);
+ opacity: .7;
+ font-size: 11px;
+ }
+
+ #cb {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 8px 10px 4px;
+ width: min(calc(100% - 20px), 1120px);
+ max-width: 1120px;
+ min-height: var(--composer-h);
+ box-sizing: border-box;
+ margin-right: auto;
+ }
+
+ #tc {
+ display: none;
+ align-items: center;
+ gap: 5px;
+ padding: 3px 8px;
+ border-radius: 5px;
+ background: rgba(147, 51, 234, .15);
+ border: 1px solid rgba(147, 51, 234, .25);
+ font-size: 10px;
+ color: #D8B4FE;
+ cursor: pointer;
+ white-space: nowrap;
+ }
+
+ #tc.on {
+ display: flex;
+ }
+
+ #pi {
+ flex: 1;
+ background: rgba(255, 255, 255, .04);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 8px 12px;
+ color: var(--t1);
+ font-size: 12.5px;
+ font-family: var(--fb);
+ outline: none;
+ resize: none;
+ min-height: 36px;
+ max-height: 120px;
+ overflow-y: auto;
+ line-height: 1.4;
+ transition: border-color .15s;
+ }
+
+ #pi:focus {
+ border-color: rgba(147, 51, 234, .4);
+ }
+
+ .cib {
+ width: 30px;
+ height: 30px;
+ border-radius: 7px;
+ background: none;
+ border: none;
+ color: var(--t3);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all .2s;
+ }
+
+ .cib:hover {
+ color: var(--t2);
+ background: rgba(255, 255, 255, .06);
+ }
+
+ .cib.mic-on {
+ color: #F87171;
+ background: rgba(239, 68, 68, .15);
+ box-shadow: 0 0 12px rgba(239, 68, 68, .4), 0 0 24px rgba(239, 68, 68, .15);
+ animation: mic-pulse 1.2s ease-in-out infinite;
+ position: relative;
+ }
+
+ .cib.mic-on::after {
+ content: '';
+ position: absolute;
+ inset: -3px;
+ border-radius: 10px;
+ border: 2px solid rgba(239, 68, 68, .3);
+ animation: mic-ring 1.2s ease-in-out infinite;
+ pointer-events: none;
+ }
+
+ @keyframes mic-pulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ 50% {
+ opacity: .7;
+ transform: scale(1.05);
+ }
+ }
+
+ @keyframes mic-ring {
+
+ 0%,
+ 100% {
+ transform: scale(1);
+ opacity: .5;
+ }
+
+ 50% {
+ transform: scale(1.15);
+ opacity: 0;
+ }
+ }
+
+ /* Agent typing indicator in chat */
+ .agent-typing {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ font-size: 10.5px;
+ color: var(--t3);
+ animation: fiu .3s ease;
+ }
+
+ .agent-typing-name {
+ font-weight: 700;
+ }
+
+ .agent-typing-name.sv {
+ color: #D8B4FE;
+ }
+
+ .agent-typing-name.cd {
+ color: #93C5FD;
+ }
+
+ .agent-typing-name.qa {
+ color: #6EE7B7;
+ }
+
+ .agent-typing-name.as {
+ color: #FCD34D;
+ }
+
+ .typing-dots {
+ display: flex;
+ gap: 3px;
+ align-items: center;
+ }
+
+ .typing-dots span {
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ background: var(--t3);
+ animation: typeDot 1.4s ease-in-out infinite;
+ }
+
+ .typing-dots span:nth-child(2) {
+ animation-delay: .2s;
+ }
+
+ .typing-dots span:nth-child(3) {
+ animation-delay: .4s;
+ }
+
+ @keyframes typeDot {
+
+ 0%,
+ 60%,
+ 100% {
+ opacity: .3;
+ transform: translateY(0);
+ }
+
+ 30% {
+ opacity: 1;
+ transform: translateY(-3px);
+ }
+ }
+
+ /* Chat message styling */
+ .chat-msg {
+ padding: 6px 12px;
+ font-size: 11px;
+ line-height: 1.5;
+ animation: fiu .3s ease;
+ border-bottom: 1px solid rgba(255, 255, 255, .03);
+ }
+
+ .chat-msg-user {
+ color: var(--t1);
+ background: rgba(147, 51, 234, .06);
+ }
+
+ .chat-msg-agent {
+ color: var(--t2);
+ }
+
+ .chat-msg-agent .chat-agent-name {
+ font-weight: 700;
+ font-size: 9.5px;
+ margin-bottom: 2px;
+ }
+
+ @keyframes sbPulse {
+
+ 0%,
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+
+ 50% {
+ opacity: .3;
+ transform: scale(.6);
+ }
+ }
+
+ .csep {
+ width: 1px;
+ height: 18px;
+ background: var(--border);
+ }
+
+ .sbtn {
+ height: 30px;
+ padding: 0 14px;
+ border-radius: 7px;
+ background: var(--grad);
+ color: white;
+ font-size: 12px;
+ font-weight: 700;
+ border: none;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ transition: all .2s;
+ }
+
+ .sbtn:hover {
+ opacity: .9;
+ transform: translateY(-1px);
+ }
+
+ #sugs {
+ display: flex;
+ gap: 5px;
+ padding: 0 10px 4px;
+ flex-wrap: wrap;
+ width: min(calc(100% - 20px), 1120px);
+ max-width: 1120px;
+ box-sizing: border-box;
+ margin-right: auto;
+ }
+
+ .sug {
+ padding: 3px 10px;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, .04);
+ border: 1px solid var(--border);
+ font-size: 10.5px;
+ color: var(--t2);
+ cursor: pointer;
+ transition: all .15s;
+ white-space: nowrap;
+ }
+
+ .sug:hover {
+ border-color: var(--border-hi);
+ color: var(--t1);
+ }
+
+ /* Right panel */
+ #rp {
+ width: var(--right-panel-w);
+ order: 2;
+ min-width: var(--right-panel-min-w);
+ max-width: var(--right-panel-max-w);
+ flex-shrink: 0;
+ background: rgba(var(--workspace-bg-rgb), 0.92);
+ border-left: 1px solid var(--border);
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ transition: width .25s ease;
+ overflow: hidden;
+ }
+
+ #rp.col,
+ #rp.collapsed {
+ width: 0;
+ min-width: 0;
+ max-width: 0;
+ border-left: none;
+ }
+
+ #rp.is-resizing {
+ transition: none;
+ }
+
+ body.rp-resizing {
+ cursor: col-resize;
+ user-select: none;
+ }
+
+ #rp-resizer {
+ position: absolute;
+ z-index: 12;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 8px;
+ cursor: col-resize;
+ touch-action: none;
+ outline: none;
+ }
+
+ #rp-resizer::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 2px;
+ background: transparent;
+ transition: background .15s ease, box-shadow .15s ease;
+ }
+
+ #rp-resizer:hover::after,
+ #rp-resizer:focus-visible::after,
+ #rp.is-resizing #rp-resizer::after {
+ background: #8B5CF6;
+ box-shadow: 0 0 10px rgba(139, 92, 246, .55);
+ }
+
+ .rph {
+ display: flex;
+ align-items: center;
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border);
+ gap: 6px;
+ flex-shrink: 0;
+ min-width: 0;
+ }
+
+ .rp-tabs {
+ display: flex;
+ gap: 3px;
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+
+ .rp-tab {
+ padding: 3px 8px;
+ border-radius: 5px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--t3);
+ cursor: pointer;
+ transition: all .15s;
+ }
+
+ .rp-tab.on {
+ color: var(--t1);
+ background: rgba(255, 255, 255, .06);
+ }
+
+ .rp-mode-toggle {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ flex: 0 0 auto;
+ }
+
+ .rp-close {
+ appearance: none;
+ width: 22px;
+ height: 22px;
+ flex: 0 0 22px;
+ display: grid;
+ place-items: center;
+ border: 0;
+ border-radius: 5px;
+ background: transparent;
+ color: var(--t3);
+ cursor: pointer;
+ }
+
+ .rp-close:hover,
+ .rp-close:focus-visible {
+ background: rgba(255, 255, 255, .06);
+ color: var(--t1);
+ outline: none;
+ }
+
+ /* ── Memory pane (Wave 4 Phase D) — mobile inherits #rp (≤600px collapse) ── */
+ #rp-memory-pane { flex-direction:column; flex:1; overflow:hidden }
+ #rpm-list { flex:1; overflow-y:auto; padding:4px 0 }
+ #rpm-empty { padding:20px 14px; text-align:center; font-size:11px; color:var(--t3); line-height:1.55 }
+ #rpm-empty .rpm-empty-icon { font-size:22px; display:block; margin-bottom:8px; opacity:.35 }
+ #rpm-empty button { margin-top:10px; background:rgba(167,139,250,.12); border:1px solid rgba(167,139,250,.35); color:#c4b5fd; padding:5px 12px; border-radius:12px; font-size:10px; font-weight:600; cursor:pointer; font-family:inherit }
+ #rpm-empty button:hover { background:rgba(167,139,250,.22) }
+ .rpm-card { margin:6px 8px; padding:9px 10px 8px; background:rgba(255,255,255,.04); border-radius:8px; border-left:3px solid var(--rpm-type-color,#8B5CF6); transition:background .15s ease, transform .15s ease; animation:fadeIn .2s ease }
+ .rpm-card:hover { background:rgba(255,255,255,.07); transform:translateY(-1px) }
+ .rpm-card[data-type="project"] { --rpm-type-color:#8B5CF6; --rpm-type-tint:rgba(139,92,246,.14) }
+ .rpm-card[data-type="chat"], .rpm-card[data-type="session"] { --rpm-type-color:#A78BFA; --rpm-type-tint:rgba(167,139,250,.14) }
+ .rpm-card[data-type="build"] { --rpm-type-color:#EC4899; --rpm-type-tint:rgba(236,72,153,.14) }
+ .rpm-card[data-type="edit"] { --rpm-type-color:#60A5FA; --rpm-type-tint:rgba(96,165,250,.14) }
+ .rpm-row-top { display:flex; align-items:center; gap:6px; margin-bottom:4px }
+ .rpm-badge { font-size:8px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; padding:2px 7px; border-radius:9px; color:var(--rpm-type-color); background:var(--rpm-type-tint); flex-shrink:0 }
+ .rpm-loc-icon { font-size:11px; line-height:1; flex-shrink:0; opacity:.85 }
+ .rpm-title { font-size:11.5px; font-weight:600; color:var(--t1); line-height:1.35; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; flex:1; min-width:0; word-break:break-word }
+ .rpm-meta { display:flex; align-items:center; gap:6px; font-size:9.5px; color:var(--t3); margin:3px 0 6px; flex-wrap:wrap }
+ .rpm-status { display:inline-flex; align-items:center; padding:1px 6px; border-radius:8px; font-size:8.5px; font-weight:600; text-transform:uppercase; letter-spacing:.04em }
+ .rpm-status.live, .rpm-status.active { color:#6EE7B7; background:rgba(110,231,183,.12) }
+ .rpm-status.draft { color:#FCD34D; background:rgba(252,211,77,.12) }
+ .rpm-status.archived { color:#94a3b8; background:rgba(148,163,184,.12) }
+ .rpm-actions { display:flex; gap:5px; margin-top:2px }
+ .rpm-act { padding:3px 9px; border-radius:10px; font-size:9.5px; font-weight:600; border:1px solid rgba(255,255,255,.14); background:rgba(255,255,255,.04); color:var(--t1); cursor:pointer; font-family:inherit; transition:background .12s ease, border-color .12s ease }
+ .rpm-act:hover { background:rgba(255,255,255,.12); border-color:rgba(255,255,255,.25) }
+ .rpm-act.primary { background:var(--rpm-type-tint); border-color:var(--rpm-type-color); color:var(--rpm-type-color) }
+ .rpm-act.primary:hover { background:var(--rpm-type-color); color:#0a0e1a }
+ .rpm-skel { margin:6px 8px; padding:10px 12px; background:rgba(255,255,255,.03); border-radius:8px; border-left:3px solid rgba(167,139,250,.25) }
+ .rpm-skel-bar { height:8px; background:linear-gradient(90deg,rgba(255,255,255,.04) 0%,rgba(255,255,255,.10) 50%,rgba(255,255,255,.04) 100%); background-size:200% 100%; border-radius:4px; margin-bottom:6px; animation:rpm-shimmer 1.4s infinite linear }
+ .rpm-skel-bar.w50 { width:55% } .rpm-skel-bar.w30 { width:35% } .rpm-skel-bar.w80 { width:80% }
+ @keyframes rpm-shimmer { 0% { background-position:200% 0 } 100% { background-position:-200% 0 } }
+
+ /* Activity feed */
+ #af {
+ flex: 1;
+ overflow-y: auto;
+ padding: 4px 0;
+ }
+
+ .ai {
+ padding: 7px 10px;
+ border-bottom: 1px solid rgba(255, 255, 255, .03);
+ }
+
+ .ai-hd {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ margin-bottom: 3px;
+ }
+
+ .ai-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ }
+
+ .ai-name {
+ font-size: 10px;
+ font-weight: 700;
+ }
+
+ .ai-ts {
+ font-size: 9.5px;
+ color: var(--t3);
+ margin-left: auto;
+ }
+
+ .ai-txt {
+ font-size: 11px;
+ color: var(--t2);
+ }
+
+ .ai-cm {
+ font-size: 9.5px;
+ font-family: monospace;
+ color: var(--t3);
+ margin-top: 2px;
+ background: rgba(255, 255, 255, .03);
+ padding: 1px 6px;
+ border-radius: 3px;
+ display: inline-block;
+ }
+
+ .rp-met {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 6px;
+ padding: 8px 10px;
+ border-top: 1px solid var(--border);
+ }
+
+ .mc {
+ background: rgba(255, 255, 255, .03);
+ border-radius: 6px;
+ padding: 7px 9px;
+ }
+
+ .ml {
+ font-size: 9px;
+ color: var(--t3);
+ margin-bottom: 3px;
+ text-transform: uppercase;
+ letter-spacing: .05em;
+ }
+
+ .mv {
+ font-size: 18px;
+ font-weight: 800;
+ font-family: var(--fd);
+ line-height: 1;
+ }
+
+ .ms {
+ font-size: 9px;
+ color: var(--t3);
+ margin-top: 2px;
+ }
+
+ /* History */
+ .hist-item {
+ padding: 7px 10px;
+ border-bottom: 1px solid rgba(255, 255, 255, .03);
+ cursor: pointer;
+ transition: background .12s;
+ }
+
+ .hist-item:hover {
+ background: rgba(255, 255, 255, .04);
+ }
+
+ .hist-prompt {
+ font-size: 11px;
+ color: var(--t2);
+ }
+
+ .hist-meta {
+ font-size: 9.5px;
+ color: var(--t3);
+ margin-top: 2px;
+ }
+
+ /* Tool output modal */
+ #tool-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 500;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ background: rgba(0, 0, 0, .65);
+ backdrop-filter: blur(6px);
+ }
+
+ #tool-modal.open {
+ display: flex;
+ }
+
+ .tm-box {
+ width: min(600px, 92vw);
+ max-height: 80vh;
+ border-radius: 14px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ animation: din .18s ease-out;
+ }
+
+ .tm-head {
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ }
+
+ .tm-title {
+ font-family: monospace;
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--t1);
+ flex: 1;
+ }
+
+ .tm-badge {
+ font-size: 9px;
+ padding: 2px 7px;
+ border-radius: 4px;
+ font-weight: 600;
+ }
+
+ .tm-close {
+ background: none;
+ border: none;
+ color: var(--t3);
+ cursor: pointer;
+ font-size: 14px;
+ padding: 2px;
+ }
+
+ .tm-close:hover {
+ color: var(--t1);
+ }
+
+ .tm-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 14px 16px;
+ font-family: monospace;
+ font-size: 11.5px;
+ color: #7DD3FC;
+ line-height: 1.7;
+ white-space: pre-wrap;
+ word-break: break-all;
+ }
+
+ .tm-foot {
+ padding: 10px 16px;
+ border-top: 1px solid var(--border);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ }
+
+ .tm-input {
+ flex: 1;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ border-radius: 7px;
+ padding: 6px 10px;
+ color: var(--t1);
+ font-family: monospace;
+ font-size: 11px;
+ outline: none;
+ }
+
+ .tm-input:focus {
+ border-color: rgba(147, 51, 234, .4);
+ }
+
+ .tm-run-btn {
+ padding: 6px 14px;
+ border-radius: 7px;
+ background: rgba(147, 51, 234, .3);
+ border: 1px solid rgba(147, 51, 234, .4);
+ color: #D8B4FE;
+ font-size: 11px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all .15s;
+ }
+
+ .tm-run-btn:hover {
+ background: rgba(147, 51, 234, .5);
+ }
+
+ /* API status dot */
+ .api-status {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 10px;
+ color: var(--t3);
+ padding: 0 6px;
+ }
+
+ .api-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: #EF4444;
+ }
+
+ .api-dot.online {
+ background: #22C55E;
+ animation: pthink 2s ease-in-out infinite;
+ }
+
+ /* Layout picker */
+ #lp-ov {
+ position: fixed;
+ inset: 0;
+ z-index: 400;
+ background: rgba(2, 6, 18, .88);
+ backdrop-filter: blur(16px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: clamp(18px, 3vw, 42px);
+ overflow-y: auto;
+ }
+
+ .lp-shell {
+ position: relative;
+ width: min(1180px, 96vw);
+ max-height: calc(100dvh - 48px);
+ display: flex;
+ flex-direction: column;
+ padding: clamp(22px, 3vw, 34px);
+ border: 1px solid rgba(var(--workspace-primary-rgb), .22);
+ border-radius: 20px;
+ background:
+ linear-gradient(145deg, rgba(var(--workspace-primary-rgb), .08), transparent 35%),
+ rgba(var(--workspace-bg-rgb), .97);
+ box-shadow: 0 28px 90px rgba(0, 0, 0, .58);
+ }
+
+ .lp-x {
+ position: absolute;
+ top: 14px;
+ right: 14px;
+ width: 32px;
+ height: 32px;
+ display: grid;
+ place-items: center;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, .035);
+ color: var(--t2);
+ cursor: pointer;
+ }
+
+ .lp-x:hover,
+ .lp-x:focus-visible {
+ border-color: rgba(var(--workspace-primary-rgb), .45);
+ color: var(--t1);
+ outline: none;
+ }
+
+ .lph {
+ text-align: center;
+ margin-bottom: 22px;
+ padding-inline: 44px;
+ }
+
+ .lph h2 {
+ font-family: var(--fd);
+ font-size: clamp(22px, 2.4vw, 30px);
+ font-weight: 800;
+ margin-bottom: 8px;
+ }
+
+ .lph p {
+ font-size: 12.5px;
+ color: var(--t2);
+ }
+
+ .lgrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
+ gap: 12px;
+ width: 100%;
+ max-height: min(62dvh, 620px);
+ overflow-y: auto;
+ padding: 5px 7px 10px 5px;
+ scrollbar-color: rgba(var(--workspace-primary-rgb), .45) transparent;
+ }
+
+ .lcard {
+ appearance: none;
+ min-width: 0;
+ overflow: hidden;
+ border: 1px solid var(--border-hi);
+ border-radius: 12px;
+ background: rgba(255, 255, 255, .025);
+ color: inherit;
+ font-family: inherit;
+ text-align: left;
+ overflow: hidden;
+ cursor: pointer;
+ transition: transform .18s ease, border-color .18s ease, box-shadow .18s ease, background .18s ease;
+ }
+
+ .lcard:hover,
+ .lcard.sel {
+ transform: translateY(-2px);
+ border-color: var(--workspace-primary);
+ background: rgba(var(--workspace-primary-rgb), .07);
+ box-shadow: 0 10px 28px rgba(0, 0, 0, .28), 0 0 20px rgba(var(--workspace-primary-rgb), .16);
+ }
+
+ .lcard:focus-visible {
+ outline: 2px solid var(--workspace-primary);
+ outline-offset: 2px;
+ }
+
+ .lthumb {
+ height: 96px;
+ position: relative;
+ overflow: hidden;
+ padding: 6px;
+ border-bottom: 1px solid rgba(255, 255, 255, .06);
+ }
+
+ .lt-bar {
+ height: 8px;
+ margin-bottom: 4px;
+ border-radius: 4px;
+ opacity: .62;
+ }
+
+ .lt-workspace {
+ height: calc(100% - 12px);
+ min-width: 0;
+ display: flex;
+ gap: 3px;
+ }
+
+ .lt-rail,
+ .lt-activity,
+ .lt-side-chat,
+ .lt-canvas,
+ .lt-chat {
+ border: 1px solid rgba(255, 255, 255, .1);
+ border-radius: 3px;
+ opacity: .3;
+ }
+
+ .lt-rail,
+ .lt-activity,
+ .lt-side-chat {
+ flex: 0 0 auto;
+ }
+
+ .lt-center {
+ min-width: 0;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ }
+
+ .lt-canvas {
+ min-height: 0;
+ flex: 1;
+ opacity: .2;
+ }
+
+ .lt-chat {
+ flex: 0 0 auto;
+ }
+
+ .lmeta {
+ padding: 9px 10px 10px;
+ }
+
+ .lname {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 0;
+ font-size: 11px;
+ font-weight: 800;
+ color: var(--t1);
+ }
+
+ .lname i {
+ display: none;
+ color: var(--workspace-primary);
+ font-size: 10px;
+ }
+
+ .lcard.sel .lname i {
+ display: inline-block;
+ }
+
+ .ldesc {
+ min-height: 30px;
+ margin-top: 5px;
+ font-size: 9.5px;
+ line-height: 1.45;
+ color: var(--t2);
+ }
+
+ .lp-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-top: 18px;
+ padding-top: 16px;
+ border-top: 1px solid var(--border);
+ }
+
+ .lp-selection {
+ min-width: 0;
+ flex: 1;
+ color: var(--t2);
+ font-size: 11px;
+ line-height: 1.4;
+ }
+
+ .lp-cancel,
+ .lpclose {
+ min-height: 40px;
+ padding: 9px 20px;
+ border-radius: 10px;
+ font-family: inherit;
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+ }
+
+ .lp-cancel {
+ border: 1px solid var(--border-hi);
+ background: rgba(255, 255, 255, .035);
+ color: var(--t2);
+ }
+
+ .lp-cancel:hover {
+ color: var(--t1);
+ background: rgba(255, 255, 255, .07);
+ }
+
+ .lpclose {
+ min-width: 170px;
+ border: 0;
+ background: var(--grad);
+ color: white;
+ box-shadow: 0 8px 22px rgba(var(--workspace-primary-rgb), .25);
+ }
+
+ .lpclose:hover {
+ filter: brightness(1.1);
+ transform: translateY(-1px);
+ }
+
+ /* Applied workspace themes */
+ body[data-workspace-layout] #topbar,
+ body[data-workspace-layout] #ctb,
+ body[data-workspace-layout] #chat,
+ body[data-workspace-layout] #rp,
+ body[data-workspace-layout] #lsb {
+ border-color: rgba(var(--workspace-primary-rgb), .13);
+ }
+
+ body[data-workspace-layout] .layout-btn {
+ border-color: rgba(var(--workspace-primary-rgb), .28);
+ background: rgba(var(--workspace-primary-rgb), .08);
+ }
+
+ body[data-workspace-layout] #rp-resizer:hover::after,
+ body[data-workspace-layout] #rp-resizer:focus-visible::after,
+ body[data-workspace-layout] #rp.is-resizing #rp-resizer::after {
+ background: var(--workspace-primary);
+ box-shadow: 0 0 10px rgba(var(--workspace-primary-rgb), .55);
+ }
+
+ @media (min-width: 1025px) {
+ body.workspace-rail-wide #lsb .sb-btn {
+ width: 42px;
+ }
+
+ body.workspace-rail-hidden #lsb,
+ body.workspace-rail-hidden #pp,
+ body.workspace-rail-hidden #pp-pull-tab {
+ display: none !important;
+ }
+
+ body.workspace-rp-left #rp {
+ order: 1;
+ border-left: 0;
+ border-right: 1px solid rgba(var(--workspace-primary-rgb), .13);
+ }
+
+ body.workspace-rp-left #canvas {
+ order: 2;
+ }
+
+ body.workspace-rp-left #rp-resizer {
+ right: 0;
+ left: auto;
+ }
+
+ body.workspace-rp-left #rp-resizer::after {
+ right: 0;
+ left: auto;
+ }
+
+ body.workspace-chat-side-right #canvas,
+ body.workspace-chat-side-left #canvas {
+ display: grid;
+ grid-template-rows: var(--modebar-h) minmax(0, 1fr);
+ }
+
+ body.workspace-chat-side-right #canvas {
+ grid-template-columns: minmax(0, 1fr) minmax(320px, var(--workspace-chat-side-w));
+ }
+
+ body.workspace-chat-side-left #canvas {
+ grid-template-columns: minmax(320px, var(--workspace-chat-side-w)) minmax(0, 1fr);
+ }
+
+ body.workspace-chat-side-right #ctb,
+ body.workspace-chat-side-left #ctb {
+ grid-row: 1;
+ grid-column: 1 / -1;
+ }
+
+ body.workspace-chat-side-right #panel-area {
+ grid-row: 2;
+ grid-column: 1;
+ }
+
+ body.workspace-chat-side-left #panel-area {
+ grid-row: 2;
+ grid-column: 2;
+ }
+
+ body.workspace-chat-side-right #chat,
+ body.workspace-chat-side-left #chat {
+ grid-row: 2;
+ width: 100%;
+ min-width: 0;
+ height: 100%;
+ max-height: none !important;
+ border-top: 0;
+ padding-bottom: 6px;
+ }
+
+ body.workspace-chat-side-right #chat {
+ grid-column: 2;
+ border-left: 1px solid rgba(var(--workspace-primary-rgb), .13);
+ }
+
+ body.workspace-chat-side-left #chat {
+ grid-column: 1;
+ border-right: 1px solid rgba(var(--workspace-primary-rgb), .13);
+ }
+
+ body.workspace-chat-side-right #chat-feed,
+ body.workspace-chat-side-left #chat-feed {
+ display: flex !important;
+ flex: 1 1 auto;
+ max-height: none !important;
+ }
+
+ body.workspace-chat-side-right #cb,
+ body.workspace-chat-side-left #cb {
+ width: 100%;
+ }
+
+ body.workspace-chat-side-right #sugs,
+ body.workspace-chat-side-left #sugs {
+ display: none;
+ }
+
+ body.workspace-chat-compact {
+ --transcript-max-h: clamp(90px, 17dvh, 170px);
+ --composer-h: 44px;
+ }
+
+ body.workspace-chat-compact #sugs {
+ display: none;
+ }
+
+ body.workspace-chat-expanded {
+ --transcript-max-h: clamp(230px, 38dvh, 480px);
+ }
+
+ body.workspace-chat-minimal #chat {
+ max-height: 62px;
+ }
+
+ body.workspace-chat-minimal #chat-feed,
+ body.workspace-chat-minimal #ts,
+ body.workspace-chat-minimal #dv-voice-bar,
+ body.workspace-chat-minimal #sugs {
+ display: none !important;
+ }
+
+ body.workspace-density-compact {
+ --topbar-h: 40px;
+ --modebar-h: 32px;
+ }
+
+ body.workspace-density-compact .pill {
+ padding: 3px 6px;
+ }
+
+ body.workspace-status-hidden #sb-wrap {
+ display: none !important;
+ }
+ }
+
+ @media (max-width: 760px) {
+ .lp-shell {
+ max-height: calc(100dvh - 24px);
+ padding: 20px 14px 14px;
+ }
+
+ .lph {
+ padding-inline: 32px;
+ }
+
+ .lgrid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ max-height: 66dvh;
+ }
+
+ .lthumb {
+ height: 78px;
+ }
+
+ .lp-actions {
+ flex-wrap: wrap;
+ }
+
+ .lp-selection {
+ flex-basis: 100%;
+ }
+
+ .lp-cancel,
+ .lpclose {
+ flex: 1;
+ }
+ }
+
+ /* Auth modal */
+ #am-ov {
+ position: fixed;
+ inset: 0;
+ z-index: 600;
+ background: rgba(2, 4, 16, .96);
+ backdrop-filter: blur(20px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .am-box {
+ width: min(380px, 92vw);
+ border-radius: 18px;
+ padding: 32px 28px;
+ animation: din .22s ease-out;
+ }
+
+ .am-logo {
+ text-align: center;
+ margin-bottom: 20px;
+ }
+
+ .am-logo-icon {
+ width: 48px;
+ height: 48px;
+ border-radius: 14px;
+ background: var(--grad);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-family: var(--fd);
+ font-weight: 800;
+ font-size: 22px;
+ color: white;
+ box-shadow: 0 0 32px var(--sv-g);
+ margin-bottom: 10px;
+ }
+
+ .am-logo h1 {
+ font-family: var(--fd);
+ font-size: 22px;
+ font-weight: 800;
+ }
+
+ .am-logo p {
+ font-size: 12px;
+ color: var(--t3);
+ margin-top: 4px;
+ }
+
+ .at-tabs {
+ display: flex;
+ gap: 2px;
+ margin-bottom: 18px;
+ background: rgba(255, 255, 255, .03);
+ border-radius: 8px;
+ padding: 3px;
+ }
+
+ .at-t {
+ flex: 1;
+ text-align: center;
+ padding: 6px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--t3);
+ cursor: pointer;
+ transition: all .15s;
+ }
+
+ .at-t.on {
+ background: rgba(147, 51, 234, .2);
+ color: #D8B4FE;
+ }
+
+ .am-field {
+ margin-bottom: 12px;
+ }
+
+ .am-field label {
+ display: block;
+ font-size: 10.5px;
+ font-weight: 600;
+ color: var(--t3);
+ margin-bottom: 5px;
+ }
+
+ .am-field input {
+ width: 100%;
+ padding: 9px 12px;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ color: var(--t1);
+ font-size: 13px;
+ font-family: var(--fb);
+ outline: none;
+ transition: border-color .15s;
+ }
+
+ .am-field input:focus {
+ border-color: rgba(147, 51, 234, .5);
+ }
+
+ .am-hint {
+ font-size: 10px;
+ color: var(--t3);
+ text-align: center;
+ margin-bottom: 12px;
+ padding: 6px;
+ background: rgba(5, 150, 105, .06);
+ border: 1px solid rgba(5, 150, 105, .15);
+ border-radius: 6px;
+ }
+
+ .am-btn {
+ width: 100%;
+ padding: 11px;
+ border-radius: 10px;
+ background: var(--grad);
+ color: white;
+ font-weight: 700;
+ font-size: 14px;
+ border: none;
+ cursor: pointer;
+ box-shadow: 0 0 24px rgba(147, 51, 234, .4);
+ margin-top: 4px;
+ transition: all .2s;
+ }
+
+ .am-btn:hover {
+ opacity: .92;
+ transform: translateY(-1px);
+ }
+
+ .am-err {
+ color: #F87171;
+ font-size: 11px;
+ margin-bottom: 8px;
+ display: none;
+ text-align: center;
+ padding: 5px;
+ background: rgba(239, 68, 68, .08);
+ border-radius: 6px;
+ }
+
+ .oab {
+ width: 100%;
+ padding: 10px;
+ border-radius: 9px;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ color: var(--t1);
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ transition: all .15s;
+ margin-bottom: 8px;
+ }
+
+ .oab:hover {
+ background: rgba(255, 255, 255, .09);
+ }
+
+ .af2 {
+ font-size: 10px;
+ color: var(--t3);
+ text-align: center;
+ }
+
+ /* ══ INTRO VIDEO OVERLAY ══ */
+ #intro-ov {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ width: 100dvw;
+ height: 100dvh;
+ z-index: 700;
+ background: #000;
+ overflow: hidden;
+ transition: opacity .5s ease;
+ }
+
+ #intro-ov.hid {
+ display: none;
+ }
+
+ #intro-ov.fade-out {
+ opacity: 0;
+ pointer-events: none;
+ }
+
+ #intro-video {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+
+ /* Click-to-start splash — device chooser */
+ #intro-splash {
+ position: absolute;
+ inset: 0;
+ z-index: 702;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: radial-gradient(ellipse at center, rgba(10, 15, 30, .88) 0%, rgba(3, 7, 18, .96) 100%);
+ gap: 28px;
+ }
+
+ .splash-title {
+ font-family: 'Syne', sans-serif;
+ font-weight: 700;
+ font-size: 22px;
+ color: #fff;
+ letter-spacing: .02em;
+ text-align: center;
+ }
+
+ .splash-sub {
+ font-size: 13px;
+ color: rgba(255, 255, 255, .45);
+ font-family: 'DM Sans', sans-serif;
+ margin-top: -16px;
+ text-align: center;
+ }
+
+ .device-chooser {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ .dev-choice {
+ width: 140px;
+ padding: 24px 12px 18px;
+ border-radius: 16px;
+ background: rgba(255, 255, 255, .04);
+ border: 2px solid rgba(255, 255, 255, .08);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ cursor: pointer;
+ transition: all .25s;
+ color: rgba(255, 255, 255, .6);
+ font-family: 'DM Sans', sans-serif;
+ }
+
+ .dev-choice:hover {
+ background: rgba(139, 92, 246, .12);
+ border-color: rgba(139, 92, 246, .4);
+ color: #D8B4FE;
+ transform: translateY(-2px);
+ }
+
+ .dev-choice.recommended {
+ border-color: rgba(139, 92, 246, .5);
+ background: rgba(139, 92, 246, .1);
+ color: #D8B4FE;
+ box-shadow: 0 0 24px rgba(139, 92, 246, .15);
+ }
+
+ .dev-choice.recommended .dc-badge {
+ display: block;
+ }
+
+ .dev-choice i {
+ font-size: 32px;
+ transition: all .25s;
+ }
+
+ .dev-choice:hover i {
+ transform: scale(1.1);
+ }
+
+ .dc-label {
+ font-size: 13px;
+ font-weight: 600;
+ }
+
+ .dc-size {
+ font-size: 10px;
+ opacity: .5;
+ font-family: monospace;
+ }
+
+ .dc-badge {
+ display: none;
+ font-size: 9px;
+ padding: 2px 8px;
+ border-radius: 8px;
+ background: rgba(139, 92, 246, .25);
+ color: #D8B4FE;
+ font-weight: 600;
+ letter-spacing: .04em;
+ margin-top: -4px;
+ }
+
+ @keyframes introPlayPulse {
+
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 0 rgba(139, 92, 246, .3);
+ }
+
+ 50% {
+ box-shadow: 0 0 0 16px rgba(139, 92, 246, 0);
+ }
+ }
+
+ .dev-choice.recommended {
+ animation: introPlayPulse 2.5s ease-in-out infinite;
+ }
+
+ @media (max-width: 600px) {
+ .splash-title {
+ font-size: 18px;
+ }
+
+ .splash-sub {
+ font-size: 11px;
+ }
+
+ .device-chooser {
+ gap: 10px;
+ }
+
+ .dev-choice {
+ width: 100px;
+ padding: 16px 8px 14px;
+ border-radius: 12px;
+ }
+
+ .dev-choice i {
+ font-size: 24px;
+ }
+
+ .dc-label {
+ font-size: 11px;
+ }
+
+ .dc-size {
+ font-size: 9px;
+ }
+ }
+
+ @media (max-width: 380px) {
+ .dev-choice {
+ width: 88px;
+ padding: 12px 6px 10px;
+ }
+
+ .dev-choice i {
+ font-size: 20px;
+ }
+
+ .dc-label {
+ font-size: 10px;
+ }
+ }
+
+ .intro-btn {
+ position: absolute;
+ z-index: 701;
+ background: rgba(255, 255, 255, .12);
+ backdrop-filter: blur(12px);
+ border: 1px solid rgba(255, 255, 255, .2);
+ border-radius: 10px;
+ color: #fff;
+ font-family: 'DM Sans', sans-serif;
+ font-size: 13px;
+ padding: 10px 22px;
+ cursor: pointer;
+ transition: all .2s;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .intro-btn:hover {
+ background: rgba(255, 255, 255, .22);
+ }
+
+ #intro-vol-control {
+ position: absolute;
+ bottom: 24px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 701;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ background: rgba(255, 255, 255, .12);
+ backdrop-filter: blur(12px);
+ border: 1px solid rgba(255, 255, 255, .2);
+ border-radius: 20px;
+ padding: 8px 18px;
+ color: #fff;
+ font-family: 'DM Sans', sans-serif;
+ font-size: 12px;
+ }
+
+ #intro-vol-slider {
+ width: 100px;
+ height: 4px;
+ -webkit-appearance: none;
+ appearance: none;
+ background: rgba(255, 255, 255, .2);
+ border-radius: 2px;
+ outline: none;
+ cursor: pointer;
+ }
+
+ #intro-vol-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #8B5CF6;
+ cursor: pointer;
+ box-shadow: 0 0 8px rgba(139, 92, 246, .6);
+ }
+
+ #intro-vol-slider::-moz-range-thumb {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: #8B5CF6;
+ cursor: pointer;
+ box-shadow: 0 0 8px rgba(139, 92, 246, .6);
+ border: none;
+ }
+
+ #intro-vol-label {
+ min-width: 36px;
+ text-align: right;
+ font-weight: 600;
+ color: #D8B4FE;
+ }
+
+ #intro-skip {
+ bottom: 40px;
+ right: 40px;
+ }
+
+ #intro-skip .skip-timer {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ border: 2px solid rgba(255, 255, 255, .3);
+ border-top-color: #fff;
+ animation: introSpin 1s linear infinite;
+ }
+
+ @keyframes introSpin {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+
+ #intro-mute {
+ bottom: 40px;
+ left: 40px;
+ font-size: 15px;
+ padding: 10px 14px;
+ }
+
+ /* ── Responsive intro for tablet / mobile ── */
+ @media (max-width: 1024px) {
+ #intro-skip {
+ bottom: 24px;
+ right: 24px;
+ font-size: 12px;
+ padding: 8px 16px;
+ }
+
+ #intro-mute {
+ bottom: 24px;
+ left: 24px;
+ font-size: 13px;
+ padding: 8px 12px;
+ }
+
+ #intro-unmute-prompt {
+ font-size: 14px;
+ padding: 14px 24px;
+ gap: 10px;
+ }
+
+ #intro-unmute-prompt i {
+ font-size: 18px;
+ }
+ }
+
+ @media (max-width: 600px) {
+ #intro-skip {
+ bottom: 16px;
+ right: 16px;
+ font-size: 11px;
+ padding: 7px 14px;
+ border-radius: 8px;
+ }
+
+ #intro-mute {
+ bottom: 16px;
+ left: 16px;
+ font-size: 12px;
+ padding: 7px 10px;
+ border-radius: 8px;
+ }
+
+ #intro-unmute-prompt {
+ font-size: 13px;
+ padding: 12px 20px;
+ gap: 8px;
+ border-radius: 12px;
+ }
+
+ #intro-unmute-prompt i {
+ font-size: 16px;
+ }
+
+ #intro-skip .skip-timer {
+ width: 16px;
+ height: 16px;
+ }
+ }
+
+ #intro-unmute-prompt {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 702;
+ background: rgba(0, 0, 0, .6);
+ backdrop-filter: blur(16px);
+ border: 1px solid rgba(255, 255, 255, .25);
+ border-radius: 16px;
+ color: #fff;
+ font-family: 'DM Sans', sans-serif;
+ font-size: 16px;
+ padding: 18px 32px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ animation: introUnmutePulse 2s ease-in-out infinite;
+ transition: opacity .3s;
+ }
+
+ #intro-unmute-prompt:hover {
+ background: rgba(139, 92, 246, .3);
+ }
+
+ #intro-unmute-prompt i {
+ font-size: 22px;
+ }
+
+ @keyframes introUnmutePulse {
+
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 0 rgba(139, 92, 246, .4);
+ }
+
+ 50% {
+ box-shadow: 0 0 0 14px rgba(139, 92, 246, 0);
+ }
+ }
+
+ /* ══ SETTINGS MODAL ══ */
+ #settings-ov {
+ position: fixed;
+ inset: 0;
+ z-index: 500;
+ background: rgba(2, 4, 16, .85);
+ backdrop-filter: blur(16px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: din .18s ease-out;
+ }
+
+ #settings-ov.hid {
+ display: none;
+ }
+
+ .stg-box {
+ width: min(420px, 92vw);
+ border-radius: 16px;
+ padding: 28px 24px;
+ background: var(--bg-elevated);
+ border: 1px solid var(--border-hi);
+ }
+
+ .stg-title {
+ font-family: 'Syne', sans-serif;
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--t1);
+ margin-bottom: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .stg-title button {
+ background: none;
+ border: none;
+ color: var(--t3);
+ cursor: pointer;
+ font-size: 16px;
+ padding: 4px;
+ }
+
+ .stg-title button:hover {
+ color: var(--t1);
+ }
+
+ .stg-section {
+ margin-bottom: 18px;
+ }
+
+ .stg-label {
+ font-size: 11px;
+ color: var(--t2);
+ text-transform: uppercase;
+ letter-spacing: .06em;
+ margin-bottom: 8px;
+ font-weight: 600;
+ }
+
+ .stg-desc {
+ font-size: 10.5px;
+ color: var(--t3);
+ margin-bottom: 10px;
+ line-height: 1.4;
+ }
+
+ .stg-options {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ }
+
+ .stg-opt {
+ padding: 8px 16px;
+ border-radius: 8px;
+ font-size: 11.5px;
+ font-family: 'DM Sans', sans-serif;
+ cursor: pointer;
+ border: 1px solid var(--border);
+ background: rgba(255, 255, 255, .03);
+ color: var(--t2);
+ transition: all .15s;
+ }
+
+ .stg-opt:hover {
+ border-color: var(--border-hi);
+ color: var(--t1);
+ }
+
+ .stg-opt.active {
+ background: rgba(139, 92, 246, .18);
+ border-color: rgba(139, 92, 246, .5);
+ color: #C4B5FD;
+ font-weight: 600;
+ }
+
+ .stg-saved {
+ font-size: 10px;
+ color: #6EE7B7;
+ opacity: 0;
+ transition: opacity .3s;
+ margin-top: 6px;
+ }
+
+ .stg-saved.show {
+ opacity: 1;
+ }
+
+ /* Toggle switch for settings */
+ .stg-toggle-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ }
+
+ .stg-toggle-label {
+ font-size: 11px;
+ color: var(--t2);
+ flex: 1;
+ }
+
+ .stg-toggle-label small {
+ display: block;
+ font-size: 9.5px;
+ color: var(--t3);
+ margin-top: 2px;
+ font-weight: 400;
+ }
+
+ .stg-toggle {
+ position: relative;
+ width: 36px;
+ height: 20px;
+ flex-shrink: 0;
+ }
+
+ .stg-toggle input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ position: absolute;
+ }
+
+ .stg-toggle .stg-slider {
+ position: absolute;
+ inset: 0;
+ background: rgba(255, 255, 255, .1);
+ border-radius: 10px;
+ cursor: pointer;
+ transition: background .2s;
+ }
+
+ .stg-toggle .stg-slider::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: var(--t3);
+ transition: transform .2s, background .2s;
+ }
+
+ .stg-toggle input:checked+.stg-slider {
+ background: rgba(139, 92, 246, .4);
+ }
+
+ .stg-toggle input:checked+.stg-slider::after {
+ transform: translateX(16px);
+ background: #C4B5FD;
+ }
+
+ .stg-toggle input:focus-visible+.stg-slider {
+ outline: 2px solid #8B5CF6;
+ outline-offset: 2px;
+ }
+
+ .stg-locked {
+ opacity: .4;
+ pointer-events: none;
+ }
+
+ .stg-locked .stg-slider {
+ cursor: not-allowed;
+ }
+
+ .stg-lock-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 3px;
+ font-size: 8px;
+ color: var(--t3);
+ background: rgba(255, 255, 255, .05);
+ padding: 1px 6px;
+ border-radius: 4px;
+ margin-left: 6px;
+ vertical-align: middle;
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ DEMO / SHOWCASE MODE
+ Admin sets a project as the default showcase in Action Window
+ ══════════════════════════════════════════════════════════ */
+ #demo-hero {
+ position: absolute;
+ inset: 0;
+ z-index: 8;
+ display: none;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: radial-gradient(ellipse at 50% 40%, rgba(15, 20, 40, .88) 0%, rgba(4, 7, 17, .96) 70%);
+ backdrop-filter: blur(6px);
+ text-align: center;
+ padding: 32px 24px;
+ gap: 12px;
+ animation: demoFadeIn .5s ease;
+ }
+
+ #demo-hero.active {
+ display: flex;
+ }
+
+ @keyframes demoFadeIn {
+ from {
+ opacity: 0;
+ }
+
+ to {
+ opacity: 1;
+ }
+ }
+
+ .demo-cat-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 14px;
+ border-radius: 20px;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: .08em;
+ text-transform: uppercase;
+ }
+
+ .demo-cat-badge.games {
+ background: rgba(239, 68, 68, .15);
+ color: #FCA5A5;
+ border: 1px solid rgba(239, 68, 68, .3);
+ }
+
+ .demo-cat-badge.web {
+ background: rgba(59, 130, 246, .15);
+ color: #93C5FD;
+ border: 1px solid rgba(59, 130, 246, .3);
+ }
+
+ .demo-cat-badge.apps {
+ background: rgba(16, 185, 129, .15);
+ color: #6EE7B7;
+ border: 1px solid rgba(16, 185, 129, .3);
+ }
+
+ .demo-cat-badge.other {
+ background: rgba(245, 158, 11, .15);
+ color: #FCD34D;
+ border: 1px solid rgba(245, 158, 11, .3);
+ }
+
+ .demo-title {
+ font-family: var(--fd);
+ font-size: clamp(26px, 4vw, 48px);
+ font-weight: 800;
+ background: linear-gradient(135deg, #F8FAFC 20%, #A78BFA 60%, #60A5FA 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ color: transparent;
+ line-height: 1.15;
+ margin: 0;
+ }
+
+ .demo-sub {
+ font-size: clamp(12px, 1.3vw, 15px);
+ color: #64748B;
+ max-width: 440px;
+ line-height: 1.6;
+ }
+
+ .demo-actions {
+ display: flex;
+ gap: 16px;
+ margin-top: 12px;
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ .demo-btn-play {
+ padding: 14px 44px;
+ border-radius: 14px;
+ background: linear-gradient(135deg, #7C3AED 0%, #2563EB 100%);
+ color: white;
+ font-weight: 800;
+ font-size: 16px;
+ border: none;
+ cursor: pointer;
+ box-shadow: 0 0 40px rgba(124, 58, 237, .5), 0 8px 24px rgba(0, 0, 0, .3);
+ transition: all .25s;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ letter-spacing: .02em;
+ }
+
+ .demo-btn-play:hover {
+ transform: translateY(-2px) scale(1.03);
+ box-shadow: 0 0 60px rgba(124, 58, 237, .7), 0 12px 32px rgba(0, 0, 0, .4);
+ }
+
+ .demo-btn-play:active {
+ transform: scale(.98);
+ }
+
+ .demo-btn-play i {
+ font-size: 18px;
+ filter: drop-shadow(0 0 6px rgba(255, 255, 255, .4));
+ }
+
+ .demo-btn-create {
+ padding: 14px 36px;
+ border-radius: 14px;
+ background: rgba(255, 255, 255, .06);
+ color: var(--t1);
+ font-weight: 700;
+ font-size: 15px;
+ border: 1px solid rgba(255, 255, 255, .12);
+ cursor: pointer;
+ transition: all .2s;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ backdrop-filter: blur(4px);
+ }
+
+ .demo-btn-create:hover {
+ background: rgba(255, 255, 255, .1);
+ border-color: rgba(255, 255, 255, .2);
+ transform: translateY(-1px);
+ }
+
+ .demo-meta {
+ font-size: 10px;
+ color: rgba(255, 255, 255, .2);
+ margin-top: 8px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .demo-meta i {
+ font-size: 8px;
+ }
+
+ /* Game Carousel */
+ #game-carousel {
+ position: absolute;
+ bottom: 60px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 90%;
+ max-width: 700px;
+ display: none;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ z-index: 9;
+ }
+
+ #game-carousel.active {
+ display: flex;
+ }
+
+ .carousel-container {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ }
+
+ .carousel-arrow {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: rgba(147, 51, 234, 0.15);
+ border: 1px solid rgba(147, 51, 234, 0.3);
+ color: #D8B4FE;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+
+ .carousel-arrow:hover {
+ background: rgba(147, 51, 234, 0.3);
+ transform: scale(1.05);
+ border-color: rgba(147, 51, 234, 0.5);
+ }
+
+ .carousel-arrow:active {
+ transform: scale(0.95);
+ }
+
+ .carousel-track {
+ flex: 1;
+ overflow: hidden;
+ position: relative;
+ height: 90px;
+ }
+
+ .carousel-cards {
+ display: flex;
+ gap: 12px;
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ .game-card {
+ min-width: 180px;
+ height: 85px;
+ background: rgba(20, 24, 45, 0.6);
+ border: 1px solid rgba(147, 51, 234, 0.2);
+ border-radius: 6px;
+ padding: 10px 12px;
+ cursor: pointer;
+ transition: all 0.2s;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 4px;
+ }
+
+ .game-card:hover {
+ border-color: rgba(147, 51, 234, 0.5);
+ background: rgba(147, 51, 234, 0.1);
+ transform: translateY(-2px);
+ }
+
+ .game-card.active {
+ border-color: #9333EA;
+ background: rgba(147, 51, 234, 0.2);
+ box-shadow: 0 0 12px rgba(147, 51, 234, 0.3);
+ }
+
+ .game-card-title {
+ font-size: 11px;
+ font-weight: 600;
+ color: #fff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .game-card-sub {
+ font-size: 9px;
+ color: var(--t3);
+ line-height: 1.3;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+
+ .carousel-dots {
+ display: flex;
+ gap: 6px;
+ justify-content: center;
+ }
+
+ .carousel-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.2);
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+
+ .carousel-dot:hover {
+ background: rgba(255, 255, 255, 0.4);
+ }
+
+ .carousel-dot.active {
+ background: #9333EA;
+ width: 20px;
+ border-radius: 3px;
+ }
+
+ /* Admin demo editor in settings */
+ .demo-editor {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .demo-editor select {
+ width: 100%;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ color: var(--t1);
+ padding: 7px 10px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-family: inherit;
+ }
+
+ .demo-editor select:focus {
+ outline: none;
+ border-color: rgba(147, 51, 234, .5);
+ }
+
+ .demo-editor-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .demo-editor-row input {
+ flex: 1;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ color: var(--t1);
+ padding: 7px 10px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-family: inherit;
+ }
+
+ .demo-editor-row input:focus {
+ outline: none;
+ border-color: rgba(147, 51, 234, .5);
+ }
+
+ .demo-editor-apply {
+ padding: 6px 14px;
+ background: rgba(147, 51, 234, .25);
+ border: 1px solid rgba(147, 51, 234, .4);
+ color: #D8B4FE;
+ border-radius: 6px;
+ font-size: 10px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all .15s;
+ white-space: nowrap;
+ }
+
+ .demo-editor-apply:hover {
+ background: rgba(147, 51, 234, .4);
+ }
+
+ .demo-editor-clear {
+ padding: 6px 12px;
+ background: rgba(239, 68, 68, .15);
+ border: 1px solid rgba(239, 68, 68, .3);
+ color: #FCA5A5;
+ border-radius: 6px;
+ font-size: 10px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all .15s;
+ white-space: nowrap;
+ }
+
+ .demo-editor-clear:hover {
+ background: rgba(239, 68, 68, .3);
+ }
+
+ /* Agentic pulse ring on Play button */
+ .demo-btn-play::before {
+ content: '';
+ position: absolute;
+ inset: -4px;
+ border-radius: 18px;
+ background: transparent;
+ border: 2px solid rgba(124, 58, 237, .3);
+ animation: demoPulse 2s ease-in-out infinite;
+ pointer-events: none;
+ }
+
+ .demo-btn-play {
+ position: relative;
+ overflow: visible;
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ GAME MODE — chat + game side by side
+ Game stays in preview, chat remains fully usable.
+ Voice narration works seamlessly during gameplay.
+ ══════════════════════════════════════════════════════════ */
+
+ /* Floating game control bar — top-right of preview area */
+ #game-ctrl-bar {
+ display: none;
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ z-index: 12;
+ background: rgba(10, 13, 24, .85);
+ backdrop-filter: blur(8px);
+ border: 1px solid rgba(255, 255, 255, .08);
+ border-radius: 8px;
+ padding: 4px 6px;
+ gap: 4px;
+ align-items: center;
+ animation: demoFadeIn .3s ease;
+ }
+
+ #game-ctrl-bar.active {
+ display: flex;
+ }
+
+ #game-ctrl-bar button {
+ background: rgba(255, 255, 255, .06);
+ border: 1px solid rgba(255, 255, 255, .08);
+ color: var(--t2);
+ font-size: 9px;
+ padding: 4px 8px;
+ border-radius: 5px;
+ cursor: pointer;
+ transition: all .15s;
+ white-space: nowrap;
+ }
+
+ #game-ctrl-bar button:hover {
+ background: rgba(147, 51, 234, .2);
+ color: #D8B4FE;
+ border-color: rgba(147, 51, 234, .3);
+ }
+
+ #game-ctrl-bar .gcb-label {
+ font-size: 8px;
+ color: rgba(255, 255, 255, .25);
+ text-transform: uppercase;
+ letter-spacing: .5px;
+ padding: 0 4px;
+ }
+
+ /* Mini-chat overlay — floating bubble during immersive game */
+ #mini-chat {
+ display: none;
+ position: fixed;
+ bottom: 16px;
+ right: 16px;
+ width: 320px;
+ max-height: 280px;
+ z-index: 10002;
+ background: rgba(10, 13, 24, .92);
+ backdrop-filter: blur(12px);
+ border: 1px solid rgba(147, 51, 234, .2);
+ border-radius: 12px;
+ flex-direction: column;
+ overflow: hidden;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, .5);
+ animation: demoFadeIn .3s ease;
+ }
+
+ #mini-chat.active {
+ display: flex;
+ }
+
+ #mini-chat .mc-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, .06);
+ }
+
+ #mini-chat .mc-head span {
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--t2);
+ }
+
+ #mini-chat .mc-head button {
+ background: none;
+ border: none;
+ color: var(--t3);
+ cursor: pointer;
+ font-size: 10px;
+ }
+
+ #mini-chat .mc-messages {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px 12px;
+ font-size: 11px;
+ color: var(--t2);
+ max-height: 180px;
+ }
+
+ #mini-chat .mc-messages .mc-msg {
+ margin-bottom: 6px;
+ padding: 4px 8px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, .04);
+ line-height: 1.4;
+ }
+
+ #mini-chat .mc-messages .mc-msg.mc-user {
+ background: rgba(147, 51, 234, .15);
+ text-align: right;
+ }
+
+ #mini-chat .mc-input {
+ display: flex;
+ border-top: 1px solid rgba(255, 255, 255, .06);
+ padding: 6px 8px;
+ gap: 6px;
+ }
+
+ #mini-chat .mc-input input {
+ flex: 1;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid rgba(255, 255, 255, .08);
+ color: var(--t1);
+ padding: 5px 8px;
+ border-radius: 6px;
+ font-size: 10px;
+ outline: none;
+ }
+
+ #mini-chat .mc-input input:focus {
+ border-color: rgba(147, 51, 234, .4);
+ }
+
+ #mini-chat .mc-input button {
+ background: rgba(147, 51, 234, .3);
+ border: none;
+ color: #D8B4FE;
+ padding: 5px 10px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 10px;
+ }
+
+ /* Game voice indicator — pulsing mic icon during TTS in game mode */
+ #game-voice-indicator {
+ display: none;
+ position: absolute;
+ bottom: 8px;
+ left: 8px;
+ z-index: 12;
+ background: rgba(10, 13, 24, .8);
+ backdrop-filter: blur(6px);
+ border: 1px solid rgba(16, 185, 129, .3);
+ border-radius: 20px;
+ padding: 4px 10px;
+ font-size: 9px;
+ color: #6EE7B7;
+ gap: 5px;
+ align-items: center;
+ animation: demoFadeIn .3s ease;
+ }
+
+ #game-voice-indicator.active {
+ display: flex;
+ }
+
+ #game-voice-indicator i {
+ animation: demoPulse 1.5s ease-in-out infinite;
+ }
+
+ @keyframes demoPulse {
+
+ 0%,
+ 100% {
+ transform: scale(1);
+ opacity: .6;
+ }
+
+ 50% {
+ transform: scale(1.06);
+ opacity: 0;
+ }
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ VOICE STUDIO — Professional TTS Control Panel
+ Full voice tuning, presets, waveform, agent narration
+ ══════════════════════════════════════════════════════════ */
+ #voice-studio-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ z-index: 9999;
+ background: rgba(0, 0, 0, .7);
+ backdrop-filter: blur(8px);
+ align-items: center;
+ justify-content: center;
+ }
+
+ #voice-studio-overlay.open {
+ display: flex;
+ }
+
+ #voice-studio {
+ width: 720px;
+ max-width: 96vw;
+ max-height: 88vh;
+ background: linear-gradient(135deg, rgba(15, 17, 28, .98), rgba(20, 22, 38, .98));
+ border: 1px solid rgba(147, 51, 234, .25);
+ border-radius: 16px;
+ box-shadow: 0 24px 80px rgba(0, 0, 0, .6), 0 0 40px rgba(147, 51, 234, .1);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .vs-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px 12px;
+ border-bottom: 1px solid rgba(255, 255, 255, .06);
+ }
+
+ .vs-header h2 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 800;
+ color: var(--t1);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+
+ .vs-header h2 i {
+ color: #A78BFA;
+ font-size: 14px;
+ }
+
+ .vs-header .vs-close {
+ width: 30px;
+ height: 30px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, .06);
+ border: none;
+ color: var(--t3);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all .15s;
+ }
+
+ .vs-header .vs-close:hover {
+ background: rgba(239, 68, 68, .2);
+ color: #FCA5A5;
+ }
+
+ /* Voice Studio Tabs */
+ .vs-tabs {
+ display: flex;
+ gap: 0;
+ padding: 0 20px;
+ border-bottom: 1px solid rgba(255, 255, 255, .06);
+ }
+
+ .vs-tab {
+ padding: 10px 16px;
+ font-size: 10.5px;
+ font-weight: 700;
+ color: var(--t3);
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all .15s;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ text-transform: uppercase;
+ letter-spacing: .5px;
+ }
+
+ .vs-tab:hover {
+ color: var(--t2);
+ }
+
+ .vs-tab.on {
+ color: #D8B4FE;
+ border-bottom-color: #7C3AED;
+ }
+
+ .vs-tab i {
+ font-size: 10px;
+ }
+
+ /* Voice Studio Body */
+ .vs-body {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px 20px;
+ }
+
+ .vs-pane {
+ display: none;
+ }
+
+ .vs-pane.on {
+ display: block;
+ }
+
+ /* Voice Cards */
+ .vs-voice-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+ margin-bottom: 16px;
+ }
+
+ .vs-voice-card {
+ padding: 12px 14px;
+ border-radius: 10px;
+ cursor: pointer;
+ background: rgba(255, 255, 255, .03);
+ border: 1px solid rgba(255, 255, 255, .06);
+ transition: all .2s;
+ position: relative;
+ }
+
+ .vs-voice-card:hover {
+ background: rgba(255, 255, 255, .06);
+ border-color: rgba(255, 255, 255, .12);
+ }
+
+ .vs-voice-card.selected {
+ background: rgba(147, 51, 234, .12);
+ border-color: rgba(147, 51, 234, .4);
+ box-shadow: 0 0 16px rgba(147, 51, 234, .15);
+ }
+
+ .vs-voice-card .vc-name {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--t1);
+ margin-bottom: 2px;
+ }
+
+ .vs-voice-card .vc-desc {
+ font-size: 9.5px;
+ color: var(--t3);
+ line-height: 1.3;
+ }
+
+ .vs-voice-card .vc-grade {
+ position: absolute;
+ top: 8px;
+ right: 10px;
+ font-size: 8px;
+ font-weight: 800;
+ padding: 2px 6px;
+ border-radius: 4px;
+ }
+
+ .vc-grade.a {
+ background: rgba(34, 197, 94, .2);
+ color: #6EE7B7;
+ }
+
+ .vc-grade.b {
+ background: rgba(59, 130, 246, .2);
+ color: #93C5FD;
+ }
+
+ .vc-grade.c {
+ background: rgba(251, 191, 36, .2);
+ color: #FCD34D;
+ }
+
+ .vc-grade.d {
+ background: rgba(156, 163, 175, .2);
+ color: #9CA3AF;
+ }
+
+ .vs-voice-card .vc-preview-btn {
+ margin-top: 6px;
+ padding: 3px 10px;
+ border-radius: 5px;
+ font-size: 9px;
+ font-weight: 700;
+ background: rgba(147, 51, 234, .2);
+ border: 1px solid rgba(147, 51, 234, .3);
+ color: #D8B4FE;
+ cursor: pointer;
+ transition: all .15s;
+ }
+
+ .vs-voice-card .vc-preview-btn:hover {
+ background: rgba(147, 51, 234, .35);
+ }
+
+ .vs-voice-card .vc-preview-btn.playing {
+ background: rgba(239, 68, 68, .2);
+ border-color: rgba(239, 68, 68, .3);
+ color: #FCA5A5;
+ }
+
+ /* Gender/Accent Tabs inside voice pane */
+ .vs-filter-bar {
+ display: flex;
+ gap: 6px;
+ margin-bottom: 14px;
+ flex-wrap: wrap;
+ }
+
+ .vs-filter-btn {
+ padding: 5px 12px;
+ border-radius: 6px;
+ font-size: 9.5px;
+ font-weight: 700;
+ background: rgba(255, 255, 255, .04);
+ border: 1px solid rgba(255, 255, 255, .08);
+ color: var(--t3);
+ cursor: pointer;
+ transition: all .15s;
+ }
+
+ .vs-filter-btn:hover {
+ background: rgba(255, 255, 255, .08);
+ }
+
+ .vs-filter-btn.on {
+ background: rgba(147, 51, 234, .2);
+ border-color: rgba(147, 51, 234, .4);
+ color: #D8B4FE;
+ }
+
+ /* Sliders */
+ .vs-slider-group {
+ margin-bottom: 14px;
+ }
+
+ .vs-slider-label {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 10px;
+ color: var(--t3);
+ margin-bottom: 5px;
+ }
+
+ .vs-slider-label .vs-val {
+ color: #D8B4FE;
+ font-weight: 700;
+ font-family: 'JetBrains Mono', monospace;
+ }
+
+ .vs-slider {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 100%;
+ height: 6px;
+ border-radius: 3px;
+ background: rgba(255, 255, 255, .08);
+ outline: none;
+ transition: background .15s;
+ }
+
+ .vs-slider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: #7C3AED;
+ border: 2px solid #D8B4FE;
+ cursor: pointer;
+ box-shadow: 0 0 8px rgba(124, 58, 237, .4);
+ transition: all .15s;
+ }
+
+ .vs-slider::-webkit-slider-thumb:hover {
+ transform: scale(1.15);
+ box-shadow: 0 0 14px rgba(124, 58, 237, .6);
+ }
+
+ .vs-slider::-moz-range-thumb {
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: #7C3AED;
+ border: 2px solid #D8B4FE;
+ cursor: pointer;
+ }
+
+ /* Waveform Visualizer */
+ .vs-waveform {
+ width: 100%;
+ height: 60px;
+ border-radius: 8px;
+ background: rgba(0, 0, 0, .3);
+ border: 1px solid rgba(255, 255, 255, .06);
+ margin-bottom: 14px;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .vs-waveform canvas {
+ width: 100%;
+ height: 100%;
+ }
+
+ .vs-waveform .wf-status {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 9px;
+ color: var(--t3);
+ pointer-events: none;
+ }
+
+ /* Voice Blend */
+ .vs-blend-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 10px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, .03);
+ border: 1px solid rgba(255, 255, 255, .06);
+ }
+
+ .vs-blend-row select {
+ flex: 1;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ color: var(--t1);
+ padding: 6px 8px;
+ border-radius: 5px;
+ font-size: 10px;
+ font-family: inherit;
+ }
+
+ .vs-blend-row select:focus {
+ outline: none;
+ border-color: rgba(147, 51, 234, .5);
+ }
+
+ .vs-blend-weight {
+ width: 50px;
+ text-align: center;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ color: #D8B4FE;
+ padding: 5px;
+ border-radius: 5px;
+ font-size: 10px;
+ font-weight: 700;
+ font-family: inherit;
+ }
+
+ /* EQ Bars */
+ .vs-eq-row {
+ display: flex;
+ gap: 14px;
+ align-items: flex-end;
+ justify-content: center;
+ padding: 12px 0;
+ margin-bottom: 14px;
+ }
+
+ .vs-eq-band {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .vs-eq-band input[type="range"] {
+ -webkit-appearance: none;
+ appearance: none;
+ writing-mode: vertical-lr;
+ direction: rtl;
+ width: 20px;
+ height: 80px;
+ accent-color: var(--mode-active);
+ }
+
+ .vs-eq-band label {
+ font-size: 8px;
+ color: var(--t3);
+ font-weight: 700;
+ text-transform: uppercase;
+ }
+
+ /* Preset Cards */
+ .vs-preset-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .vs-preset-card {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, .03);
+ border: 1px solid rgba(255, 255, 255, .06);
+ transition: all .15s;
+ cursor: pointer;
+ }
+
+ .vs-preset-card:hover {
+ background: rgba(255, 255, 255, .06);
+ }
+
+ .vs-preset-card.active {
+ border-color: rgba(147, 51, 234, .4);
+ background: rgba(147, 51, 234, .08);
+ }
+
+ .vs-preset-card .vp-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 13px;
+ }
+
+ .vs-preset-card .vp-icon.male {
+ background: rgba(59, 130, 246, .15);
+ color: #93C5FD;
+ }
+
+ .vs-preset-card .vp-icon.female {
+ background: rgba(236, 72, 153, .15);
+ color: #F9A8D4;
+ }
+
+ .vs-preset-card .vp-info {
+ flex: 1;
+ }
+
+ .vs-preset-card .vp-name {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--t1);
+ }
+
+ .vs-preset-card .vp-meta {
+ font-size: 9px;
+ color: var(--t3);
+ }
+
+ .vs-preset-card .vp-actions {
+ display: flex;
+ gap: 4px;
+ }
+
+ .vs-preset-card .vp-actions button {
+ width: 26px;
+ height: 26px;
+ border-radius: 5px;
+ background: rgba(255, 255, 255, .05);
+ border: none;
+ color: var(--t3);
+ cursor: pointer;
+ font-size: 9px;
+ transition: all .15s;
+ }
+
+ .vs-preset-card .vp-actions button:hover {
+ background: rgba(255, 255, 255, .12);
+ color: var(--t1);
+ }
+
+ /* Test Area */
+ .vs-test-area {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 14px;
+ }
+
+ .vs-test-input {
+ flex: 1;
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ color: var(--t1);
+ padding: 8px 12px;
+ border-radius: 8px;
+ font-size: 11px;
+ font-family: inherit;
+ resize: none;
+ min-height: 36px;
+ }
+
+ .vs-test-input:focus {
+ outline: none;
+ border-color: rgba(147, 51, 234, .5);
+ }
+
+ .vs-test-btn {
+ padding: 8px 16px;
+ border-radius: 8px;
+ font-size: 11px;
+ font-weight: 700;
+ background: linear-gradient(135deg, #7C3AED, #9333EA);
+ border: none;
+ color: #fff;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ transition: all .15s;
+ white-space: nowrap;
+ }
+
+ .vs-test-btn:hover {
+ filter: brightness(1.15);
+ transform: translateY(-1px);
+ }
+
+ .vs-test-btn:disabled {
+ opacity: .5;
+ cursor: not-allowed;
+ transform: none;
+ filter: none;
+ }
+
+ .vs-test-btn.playing {
+ background: linear-gradient(135deg, #DC2626, #EF4444);
+ }
+
+ /* Agent Voice Config */
+ .vs-agent-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, .03);
+ border: 1px solid rgba(255, 255, 255, .06);
+ margin-bottom: 8px;
+ }
+
+ .vs-agent-row .va-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ }
+
+ .vs-agent-row .va-dot.sv {
+ background: #D8B4FE;
+ }
+
+ .vs-agent-row .va-dot.cd {
+ background: #93C5FD;
+ }
+
+ .vs-agent-row .va-dot.qa {
+ background: #6EE7B7;
+ }
+
+ .vs-agent-row .va-dot.as {
+ background: #FCD34D;
+ }
+
+ .vs-agent-row .va-name {
+ font-size: 11px;
+ font-weight: 700;
+ color: var(--t1);
+ flex: 1;
+ }
+
+ .vs-agent-row select {
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid var(--border);
+ color: var(--t2);
+ padding: 4px 8px;
+ border-radius: 5px;
+ font-size: 9.5px;
+ font-family: inherit;
+ }
+
+ .vs-agent-toggle {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ /* Section Titles */
+ .vs-section-title {
+ font-size: 10px;
+ font-weight: 800;
+ color: var(--t3);
+ text-transform: uppercase;
+ letter-spacing: .8px;
+ margin-bottom: 10px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .vs-section-title i {
+ font-size: 9px;
+ color: #A78BFA;
+ }
+
+ .vs-divider {
+ height: 1px;
+ background: rgba(255, 255, 255, .06);
+ margin: 16px 0;
+ }
+
+ /* Footer Actions */
+ .vs-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 20px;
+ border-top: 1px solid rgba(255, 255, 255, .06);
+ }
+
+ .vs-footer-status {
+ font-size: 9px;
+ color: var(--t3);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .vs-footer-status .vs-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ }
+
+ .vs-footer-status .vs-dot.online {
+ background: #22C55E;
+ box-shadow: 0 0 6px rgba(34, 197, 94, .5);
+ }
+
+ .vs-footer-status .vs-dot.warn {
+ background: #F59E0B;
+ box-shadow: 0 0 6px rgba(245, 158, 11, .5);
+ }
+
+ .vs-footer-status .vs-dot.offline {
+ background: #EF4444;
+ }
+
+ .vs-footer-actions {
+ display: flex;
+ gap: 6px;
+ }
+
+ .vs-footer-actions button {
+ padding: 6px 14px;
+ border-radius: 6px;
+ font-size: 10px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all .15s;
+ }
+
+ .vs-btn-save {
+ background: rgba(147, 51, 234, .25);
+ border: 1px solid rgba(147, 51, 234, .4);
+ color: #D8B4FE;
+ }
+
+ .vs-btn-save:hover {
+ background: rgba(147, 51, 234, .4);
+ }
+
+ .vs-btn-export {
+ background: rgba(255, 255, 255, .05);
+ border: 1px solid rgba(255, 255, 255, .08);
+ color: var(--t3);
+ }
+
+ .vs-btn-export:hover {
+ background: rgba(255, 255, 255, .1);
+ color: var(--t1);
+ }
+
+ /* ── Inline Voice Control Bar (above chat input) ── */
+ #dv-voice-bar {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ margin: 0 8px 2px;
+ width: min(calc(100% - 16px), 1120px);
+ max-width: 1120px;
+ box-sizing: border-box;
+ background: rgba(147, 51, 234, .08);
+ border: 1px solid rgba(147, 51, 234, .15);
+ border-radius: 8px;
+ font-size: 10px;
+ color: var(--t3);
+ transition: all .2s;
+ }
+
+ #dv-voice-bar:hover {
+ border-color: rgba(147, 51, 234, .24);
+ }
+
+ #dv-voice-bar .dvb-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ }
+
+ #dv-voice-bar .dvb-dot.on {
+ background: #4ade80;
+ box-shadow: 0 0 5px rgba(74, 222, 128, .5);
+ }
+
+ #dv-voice-bar .dvb-dot.warn {
+ background: #fbbf24;
+ }
+
+ #dv-voice-bar .dvb-dot.off {
+ background: #ef4444;
+ }
+
+ #dv-voice-bar .dvb-mode,
+ #dv-voice-bar .dvb-engine {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 7px;
+ border-radius: 5px;
+ border: 1px solid rgba(255, 255, 255, .1);
+ background: rgba(255, 255, 255, .045);
+ font-weight: 600;
+ font-size: 10px;
+ font-family: inherit;
+ line-height: 1.4;
+ cursor: pointer;
+ transition: all .15s;
+ }
+
+ #dv-voice-bar .dvb-mode {
+ color: var(--mode-active, #D8B4FE);
+ background: var(--mode-active-tint, rgba(147, 51, 234, .12));
+ border-color: color-mix(in srgb, var(--mode-active, #D8B4FE) 34%, transparent);
+ }
+
+ #dv-voice-bar .dvb-mode:hover {
+ background: var(--mode-active-tint, rgba(147, 51, 234, .2));
+ color: var(--t1);
+ }
+
+ #dv-voice-bar .dvb-engine {
+ color: #FCD34D;
+ background: rgba(245, 158, 11, .1);
+ border-color: rgba(245, 158, 11, .22);
+ }
+
+ #dv-voice-bar .dvb-engine:hover {
+ background: rgba(245, 158, 11, .18);
+ color: #FDE68A;
+ }
+
+ #dv-voice-bar .dvb-voice {
+ color: #D8B4FE;
+ font-weight: 600;
+ cursor: pointer;
+ padding: 2px 6px;
+ border-radius: 4px;
+ background: rgba(147, 51, 234, .12);
+ border: 1px solid rgba(147, 51, 234, .22);
+ }
+
+ #dv-voice-bar .dvb-voice:hover {
+ background: rgba(147, 51, 234, .25);
+ }
+
+ #dv-voice-bar .dvb-btn {
+ padding: 2px 6px;
+ border-radius: 4px;
+ cursor: pointer;
+ border: none;
+ background: rgba(255, 255, 255, .06);
+ color: var(--t3);
+ font-size: 10px;
+ font-family: inherit;
+ transition: all .15s;
+ }
+
+ #dv-voice-bar .dvb-btn:hover {
+ background: rgba(255, 255, 255, .12);
+ color: var(--t1);
+ }
+
+ #dv-voice-bar .dvb-btn.active {
+ background: rgba(147, 51, 234, .2);
+ color: #D8B4FE;
+ }
+
+ #dv-voice-bar .dvb-sep {
+ width: 1px;
+ height: 14px;
+ background: rgba(255, 255, 255, .08);
+ }
+
+ .dvb-speaking {
+ animation: dvbPulse 1s infinite;
+ background: rgba(147, 51, 234, .18) !important;
+ box-shadow: 0 0 12px rgba(147, 51, 234, .15);
+ }
+
+ @keyframes dvbPulse {
+
+ 0%,
+ 100% {
+ opacity: 1
+ }
+
+ 50% {
+ opacity: .7
+ }
+ }
+
+ /* ── Speaking Overlay: waveform + mouth animation ── */
+ #dv-speak-overlay {
+ display: none;
+ position: absolute;
+ bottom: 100%;
+ left: 0;
+ right: 0;
+ height: 64px;
+ margin-bottom: 2px;
+ background: rgba(8, 6, 20, .95);
+ border: 1px solid rgba(147, 51, 234, .25);
+ border-radius: 12px;
+ overflow: hidden;
+ z-index: 200;
+ backdrop-filter: blur(12px);
+ align-items: center;
+ gap: 0;
+ box-shadow: 0 -4px 24px rgba(0, 0, 0, .4);
+ transition: opacity .15s, transform .15s;
+ opacity: 0;
+ transform: translateY(6px);
+ }
+
+ #dv-speak-overlay.active {
+ display: flex;
+ opacity: 1;
+ transform: translateY(0);
+ }
+
+ /* Mouth / Avatar indicator */
+ .dv-mouth-wrap {
+ width: 48px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ position: relative;
+ }
+
+ .dv-mouth-circle {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 14px;
+ position: relative;
+ transition: box-shadow .08s;
+ }
+
+ .dv-mouth-circle.female {
+ background: rgba(244, 114, 182, .15);
+ border: 1.5px solid rgba(244, 114, 182, .4);
+ color: #F472B6;
+ box-shadow: 0 0 8px rgba(244, 114, 182, .2);
+ }
+
+ .dv-mouth-circle.male {
+ background: rgba(96, 165, 250, .15);
+ border: 1.5px solid rgba(96, 165, 250, .4);
+ color: #60A5FA;
+ box-shadow: 0 0 8px rgba(96, 165, 250, .2);
+ }
+
+ /* Mouth open/close animation driven by JS */
+ .dv-mouth-bar {
+ position: absolute;
+ bottom: 8px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 12px;
+ height: 3px;
+ border-radius: 2px;
+ transition: height .06s, width .06s, border-radius .06s;
+ }
+
+ .dv-mouth-bar.female {
+ background: #F472B6;
+ }
+
+ .dv-mouth-bar.male {
+ background: #60A5FA;
+ }
+
+ /* Waveform canvas area */
+ .dv-wf-area {
+ flex: 1;
+ height: 100%;
+ position: relative;
+ }
+
+ .dv-wf-area canvas {
+ width: 100%;
+ height: 100%;
+ display: block;
+ }
+
+ /* Voice name + engine label */
+ .dv-speak-info {
+ width: 80px;
+ flex-shrink: 0;
+ padding: 0 8px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ justify-content: center;
+ gap: 2px;
+ }
+
+ .dv-speak-name {
+ font-size: 10px;
+ font-weight: 700;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 72px;
+ }
+
+ .dv-speak-name.female {
+ color: #F472B6;
+ }
+
+ .dv-speak-name.male {
+ color: #60A5FA;
+ }
+
+ .dv-speak-engine {
+ font-size: 8px;
+ color: var(--t3);
+ text-transform: uppercase;
+ letter-spacing: .5px;
+ }
+
+ /* Glow ring animation around mouth when speaking */
+ @keyframes dvMouthGlow {
+ 0% {
+ box-shadow: 0 0 6px rgba(244, 114, 182, .2);
+ }
+
+ 50% {
+ box-shadow: 0 0 18px rgba(244, 114, 182, .5);
+ }
+
+ 100% {
+ box-shadow: 0 0 6px rgba(244, 114, 182, .2);
+ }
+ }
+
+ @keyframes dvMouthGlowMale {
+ 0% {
+ box-shadow: 0 0 6px rgba(96, 165, 250, .2);
+ }
+
+ 50% {
+ box-shadow: 0 0 18px rgba(96, 165, 250, .5);
+ }
+
+ 100% {
+ box-shadow: 0 0 6px rgba(96, 165, 250, .2);
+ }
+ }
+
+ .dv-mouth-circle.female.speaking {
+ animation: dvMouthGlow .4s ease-in-out infinite;
+ }
+
+ .dv-mouth-circle.male.speaking {
+ animation: dvMouthGlowMale .4s ease-in-out infinite;
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ WAVEFORM COLOR SYSTEM — 16 neon colors + custom picker
+ ══════════════════════════════════════════════════════════ */
+ .wf-color-grid {
+ display: grid;
+ grid-template-columns: repeat(8, 1fr);
+ gap: 4px;
+ margin: 8px 0;
+ }
+
+ .wf-color-swatch {
+ width: 100%;
+ aspect-ratio: 1;
+ border-radius: 6px;
+ cursor: pointer;
+ border: 2px solid transparent;
+ transition: all .15s;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .wf-color-swatch:hover {
+ transform: scale(1.15);
+ z-index: 2;
+ box-shadow: 0 0 12px currentColor;
+ }
+
+ .wf-color-swatch.selected {
+ border-color: #fff;
+ box-shadow: 0 0 16px currentColor;
+ transform: scale(1.1);
+ }
+
+ .wf-color-swatch::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: 4px;
+ background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, .3), transparent 60%);
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ WAVEFORM TYPE SELECTOR — 12 SOTA visualizations
+ ══════════════════════════════════════════════════════════ */
+ .wf-type-grid {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 6px;
+ margin: 8px 0;
+ }
+
+ .wf-type-card {
+ padding: 8px 4px;
+ border-radius: 8px;
+ text-align: center;
+ cursor: pointer;
+ background: rgba(255, 255, 255, .03);
+ border: 1px solid rgba(255, 255, 255, .06);
+ transition: all .2s;
+ font-size: 9px;
+ color: var(--t3);
+ }
+
+ .wf-type-card:hover {
+ background: rgba(147, 51, 234, .12);
+ border-color: rgba(147, 51, 234, .3);
+ color: var(--t1);
+ transform: translateY(-1px);
+ }
+
+ .wf-type-card.selected {
+ background: rgba(147, 51, 234, .2);
+ border-color: rgba(147, 51, 234, .5);
+ color: #D8B4FE;
+ box-shadow: 0 2px 12px rgba(147, 51, 234, .2);
+ }
+
+ .wf-type-card i {
+ display: block;
+ font-size: 16px;
+ margin-bottom: 3px;
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ VISUAL EFFECTS SYSTEM — Hover, particles, flames, glow
+ ══════════════════════════════════════════════════════════ */
+
+ /* Universal hover lift for interactive elements */
+ .dv-fx-hover {
+ transition: transform .18s cubic-bezier(.34, 1.56, .64, 1), box-shadow .18s, filter .18s !important;
+ }
+
+ .dv-fx-hover:hover {
+ transform: translateY(-2px) scale(1.02) !important;
+ box-shadow: 0 6px 20px rgba(0, 0, 0, .3), 0 0 15px rgba(147, 51, 234, .15) !important;
+ }
+
+ .dv-fx-hover:active {
+ transform: translateY(0) scale(.98) !important;
+ transition-duration: .06s !important;
+ }
+
+ /* Button ripple effect */
+ .dv-ripple {
+ position: relative;
+ overflow: hidden;
+ }
+
+ .dv-ripple::after {
+ content: '';
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, .2);
+ transform: translate(-50%, -50%);
+ pointer-events: none;
+ transition: width .4s, height .4s, opacity .4s;
+ opacity: 0;
+ }
+
+ .dv-ripple:active::after {
+ width: 300px;
+ height: 300px;
+ opacity: 0;
+ transition: 0s;
+ }
+
+ /* Card shimmer on hover */
+ .dv-shimmer {
+ position: relative;
+ overflow: hidden;
+ }
+
+ .dv-shimmer::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 60%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .04), transparent);
+ transition: left .6s;
+ pointer-events: none;
+ z-index: 1;
+ }
+
+ .dv-shimmer:hover::before {
+ left: 150%;
+ }
+
+ /* Glow border pulse */
+ .dv-glow-border {
+ position: relative;
+ }
+
+ .dv-glow-border::after {
+ content: '';
+ position: absolute;
+ inset: -1px;
+ border-radius: inherit;
+ background: linear-gradient(135deg, rgba(147, 51, 234, .3), rgba(96, 165, 250, .3), rgba(244, 114, 182, .3));
+ background-size: 300% 300%;
+ z-index: -1;
+ opacity: 0;
+ transition: opacity .3s;
+ animation: dvGlowShift 3s ease infinite;
+ }
+
+ .dv-glow-border:hover::after {
+ opacity: 1;
+ }
+
+ @keyframes dvGlowShift {
+ 0% {
+ background-position: 0% 50%
+ }
+
+ 50% {
+ background-position: 100% 50%
+ }
+
+ 100% {
+ background-position: 0% 50%
+ }
+ }
+
+ /* Particle burst on click */
+ .dv-particles {
+ position: absolute;
+ pointer-events: none;
+ z-index: 9999;
+ }
+
+ .dv-particle {
+ position: absolute;
+ width: 4px;
+ height: 4px;
+ border-radius: 50%;
+ animation: dvParticleFly .6s ease-out forwards;
+ }
+
+ @keyframes dvParticleFly {
+ 0% {
+ transform: translate(0, 0) scale(1);
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translate(var(--dx), var(--dy)) scale(0);
+ opacity: 0;
+ }
+ }
+
+ /* Flame effect (toggle-able) */
+ .dv-flame-wrap {
+ position: absolute;
+ bottom: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 100%;
+ height: 30px;
+ pointer-events: none;
+ overflow: hidden;
+ opacity: 0;
+ transition: opacity .3s;
+ }
+
+ .dv-flame-active .dv-flame-wrap {
+ opacity: 1;
+ }
+
+ .dv-flame {
+ position: absolute;
+ bottom: 0;
+ border-radius: 50% 50% 20% 20%;
+ filter: blur(2px);
+ animation: dvFlameFlicker .3s ease-in-out infinite alternate;
+ }
+
+ @keyframes dvFlameFlicker {
+ 0% {
+ transform: scaleY(1) translateY(0);
+ opacity: .8
+ }
+
+ 100% {
+ transform: scaleY(1.3) translateY(-4px);
+ opacity: 1
+ }
+ }
+
+ /* Static / glitch effect (toggle-able) */
+ .dv-static-overlay {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 10;
+ background: repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255, 255, 255, .015) 2px, rgba(255, 255, 255, .015) 4px);
+ opacity: 0;
+ transition: opacity .2s;
+ }
+
+ .dv-static-active:hover .dv-static-overlay {
+ opacity: 1;
+ }
+
+ /* Neon text glow on hover */
+ .dv-neon-text:hover {
+ text-shadow: 0 0 7px currentColor, 0 0 10px currentColor, 0 0 21px currentColor;
+ }
+
+ /* Smooth panel transitions */
+ .dv-slide-up {
+ animation: dvSlideUp .3s cubic-bezier(.16, 1, .3, 1);
+ }
+
+ @keyframes dvSlideUp {
+ from {
+ transform: translateY(20px);
+ opacity: 0
+ }
+
+ to {
+ transform: translateY(0);
+ opacity: 1
+ }
+ }
+
+ .dv-slide-in {
+ animation: dvSlideIn .3s cubic-bezier(.16, 1, .3, 1);
+ }
+
+ @keyframes dvSlideIn {
+ from {
+ transform: translateX(-20px);
+ opacity: 0
+ }
+
+ to {
+ transform: translateX(0);
+ opacity: 1
+ }
+ }
+
+ .dv-fade-in {
+ animation: dvFadeIn .25s ease-out;
+ }
+
+ @keyframes dvFadeIn {
+ from {
+ opacity: 0
+ }
+
+ to {
+ opacity: 1
+ }
+ }
+
+ .dv-scale-in {
+ animation: dvScaleIn .2s cubic-bezier(.16, 1, .3, 1);
+ }
+
+ @keyframes dvScaleIn {
+ from {
+ transform: scale(.9);
+ opacity: 0
+ }
+
+ to {
+ transform: scale(1);
+ opacity: 1
+ }
+ }
+
+ /* Electric border spark effect */
+ .dv-spark {
+ position: relative;
+ }
+
+ .dv-spark::before {
+ content: '';
+ position: absolute;
+ inset: -2px;
+ border-radius: inherit;
+ background: conic-gradient(from var(--spark-angle, 0deg), transparent 80%, rgba(147, 51, 234, .6));
+ z-index: -1;
+ opacity: 0;
+ transition: opacity .3s;
+ }
+
+ .dv-spark:hover::before {
+ opacity: 1;
+ animation: dvSparkRotate 1.5s linear infinite;
+ }
+
+ @keyframes dvSparkRotate {
+ to {
+ --spark-angle: 360deg
+ }
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ ICON/THEME STUDIO — Settings panel styles
+ ══════════════════════════════════════════════════════════ */
+ .icon-studio-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 8px;
+ margin: 8px 0;
+ }
+
+ .icon-pack-card {
+ padding: 10px;
+ border-radius: 8px;
+ text-align: center;
+ cursor: pointer;
+ background: rgba(255, 255, 255, .03);
+ border: 1px solid rgba(255, 255, 255, .06);
+ transition: all .2s;
+ position: relative;
+ }
+
+ .icon-pack-card:hover {
+ background: rgba(147, 51, 234, .1);
+ border-color: rgba(147, 51, 234, .3);
+ transform: translateY(-2px);
+ }
+
+ .icon-pack-card.active {
+ background: rgba(147, 51, 234, .15);
+ border-color: rgba(147, 51, 234, .5);
+ box-shadow: 0 2px 12px rgba(147, 51, 234, .2);
+ }
+
+ .icon-pack-card.broken {
+ opacity: .5;
+ border-color: rgba(239, 68, 68, .3);
+ }
+
+ .icon-pack-card.broken::after {
+ content: 'BROKEN';
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ font-size: 7px;
+ font-weight: 700;
+ color: #FCA5A5;
+ background: rgba(239, 68, 68, .2);
+ padding: 1px 4px;
+ border-radius: 3px;
+ }
+
+ .icon-pack-name {
+ font-size: 10px;
+ font-weight: 700;
+ color: var(--t1);
+ margin-top: 4px;
+ }
+
+ .icon-pack-count {
+ font-size: 8px;
+ color: var(--t3);
+ }
+
+ .icon-pack-preview {
+ font-size: 18px;
+ display: flex;
+ gap: 4px;
+ justify-content: center;
+ margin-bottom: 4px;
+ }
+
+ .theme-backup-bar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px;
+ margin: 8px 0;
+ background: rgba(255, 255, 255, .02);
+ border-radius: 6px;
+ border: 1px solid var(--border);
+ }
+
+ .theme-backup-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+ }
+
+ .theme-backup-dot.ok {
+ background: #4ade80;
+ box-shadow: 0 0 6px rgba(74, 222, 128, .3);
+ }
+
+ .theme-backup-dot.warn {
+ background: #fbbf24;
+ }
+
+ .theme-backup-dot.err {
+ background: #f87171;
+ }
+
+ /* ── Quick Voice Picker Popup ── */
+ #dv-voice-picker {
+ display: none;
+ position: absolute;
+ bottom: 100%;
+ left: 0;
+ right: 0;
+ max-height: 220px;
+ overflow-y: auto;
+ margin-bottom: 4px;
+ background: rgba(15, 10, 30, .97);
+ border: 1px solid rgba(147, 51, 234, .2);
+ border-radius: 10px;
+ padding: 6px;
+ z-index: 999;
+ box-shadow: 0 -8px 30px rgba(0, 0, 0, .5);
+ }
+
+ #dv-voice-picker.open {
+ display: block;
+ }
+
+ .dvp-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 8px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 10px;
+ color: var(--t2);
+ transition: all .12s;
+ }
+
+ .dvp-item:hover {
+ background: rgba(147, 51, 234, .15);
+ }
+
+ .dvp-item.selected {
+ background: rgba(147, 51, 234, .2);
+ border: 1px solid rgba(147, 51, 234, .3);
+ }
+
+ .dvp-item .dvp-name {
+ font-weight: 600;
+ }
+
+ .dvp-item .dvp-meta {
+ color: var(--t3);
+ font-size: 9px;
+ }
+
+ .dvp-item .dvp-grade {
+ font-size: 8px;
+ font-weight: 700;
+ padding: 1px 5px;
+ border-radius: 3px;
+ margin-left: auto;
+ }
+
+ @media (max-width:640px) {
+ #voice-studio {
+ width: 98vw;
+ max-height: 95vh;
+ border-radius: 12px;
+ }
+
+ .vs-voice-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .vs-tabs {
+ overflow-x: auto;
+ }
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ IMMERSIVE PROJECT VIEW
+ Maximizes preview space; edge-hover reveals hidden UI
+ ══════════════════════════════════════════════════════════ */
+ .immersive #topbar,
+ .immersive #sb-wrap,
+ .immersive #lsb,
+ .immersive #pp,
+ .immersive #rp,
+ .immersive #chat,
+ .immersive #ctb,
+ .immersive .btb,
+ .immersive .frame-chrome,
+ .immersive #bst {
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ z-index: 200;
+ transition: opacity .25s ease, transform .25s ease;
+ }
+
+ .immersive #topbar {
+ top: 0;
+ left: 0;
+ right: 0;
+ transform: translateY(-100%);
+ height: 42px;
+ }
+
+ .immersive #sb-wrap {
+ top: 0;
+ left: 0;
+ right: 0;
+ transform: translateY(-100%);
+ }
+
+ .immersive #lsb {
+ top: 0;
+ left: 0;
+ bottom: 0;
+ transform: translateX(-100%);
+ width: 48px;
+ }
+
+ .immersive #pp {
+ transform: translateX(-100%);
+ }
+
+ .immersive #rp {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ transform: translateX(100%);
+ width: var(--right-panel-w);
+ border-left: 1px solid var(--border);
+ }
+
+ .immersive #chat {
+ bottom: 0;
+ left: 0;
+ right: 0;
+ transform: translateY(100%);
+ }
+
+ /* When edge-hover reveals panels */
+ .immersive.edge-top #topbar {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+ position: fixed;
+ width: 100%;
+ }
+
+ .immersive.edge-left #lsb {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateX(0);
+ position: fixed;
+ height: 100vh;
+ background: var(--bg);
+ }
+
+ .immersive.edge-right #rp {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateX(0);
+ position: fixed;
+ height: 100vh;
+ background: rgba(4, 8, 20, 0.95);
+ }
+
+ .immersive.edge-bottom #chat {
+ opacity: 1;
+ pointer-events: auto;
+ transform: translateY(0);
+ position: fixed;
+ width: 100%;
+ background: rgba(3, 6, 16, 0.95);
+ border-top: 1px solid var(--border);
+ }
+
+ /* Canvas takes full space in immersive */
+ .immersive #main {
+ display: block;
+ }
+
+ .immersive #canvas {
+ position: fixed;
+ inset: 0;
+ z-index: 100;
+ background: #050a14;
+ }
+
+ .immersive #panel-area {
+ position: absolute;
+ inset: 0;
+ }
+
+ .immersive #bframe {
+ margin: 0;
+ border-radius: 0;
+ border: none;
+ box-shadow: none;
+ width: 100% !important;
+ max-width: 100% !important;
+ }
+
+ .immersive #lp {
+ border-radius: 0;
+ }
+
+ .immersive #preview-frame {
+ width: 100%;
+ height: 100%;
+ }
+
+ /* Edge trigger zones — invisible strips at viewport edges */
+ .edge-zone {
+ position: fixed;
+ z-index: 250;
+ background: transparent;
+ display: none;
+ }
+
+ .immersive .edge-zone {
+ display: block;
+ }
+
+ .edge-zone-top {
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 6px;
+ cursor: n-resize;
+ }
+
+ .edge-zone-bottom {
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 6px;
+ cursor: s-resize;
+ }
+
+ .edge-zone-left {
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 6px;
+ cursor: w-resize;
+ }
+
+ .edge-zone-right {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 6px;
+ cursor: e-resize;
+ }
+
+ /* Edge zone hover indicator — subtle glow line */
+ .edge-zone::after {
+ content: '';
+ position: absolute;
+ background: rgba(147, 51, 234, 0);
+ transition: background .2s;
+ border-radius: 2px;
+ }
+
+ .edge-zone-top::after,
+ .edge-zone-bottom::after {
+ left: 20%;
+ right: 20%;
+ height: 2px;
+ }
+
+ .edge-zone-top::after {
+ top: 0;
+ }
+
+ .edge-zone-bottom::after {
+ bottom: 0;
+ }
+
+ .edge-zone-left::after,
+ .edge-zone-right::after {
+ top: 20%;
+ bottom: 20%;
+ width: 2px;
+ }
+
+ .edge-zone-left::after {
+ left: 0;
+ }
+
+ .edge-zone-right::after {
+ right: 0;
+ }
+
+ .edge-zone:hover::after {
+ background: rgba(147, 51, 234, .6);
+ }
+
+ /* Immersive exit button */
+ .immersive-exit {
+ position: fixed;
+ top: 8px;
+ right: 8px;
+ z-index: 300;
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ background: rgba(0, 0, 0, .6);
+ border: 1px solid rgba(255, 255, 255, .12);
+ color: var(--t2);
+ font-size: 12px;
+ cursor: pointer;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ backdrop-filter: blur(8px);
+ transition: all .2s;
+ opacity: .4;
+ }
+
+ .immersive .immersive-exit {
+ display: flex;
+ }
+
+ .immersive-exit:hover {
+ opacity: 1;
+ background: rgba(147, 51, 234, .35);
+ border-color: rgba(147, 51, 234, .5);
+ color: var(--t1);
+ transform: scale(1.1);
+ }
+
+ /* Immersive enter button (shows in canvas toolbar) */
+ .immersive-enter {
+ background: none;
+ border: none;
+ color: var(--t3);
+ cursor: pointer;
+ font-size: 10px;
+ padding: 4px 7px;
+ border-radius: 5px;
+ transition: all .15s;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ .immersive-enter:hover {
+ color: var(--t1);
+ background: rgba(255, 255, 255, .06);
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ APP MODE — streamlined view for games/apps/web
+ Content fills screen, NO overlapping, collapsible chat
+ Toggle: Ctrl+Shift+F or floating button
+ ══════════════════════════════════════════════════════════ */
+
+ /* Hide all dev tooling in app mode */
+ .app-mode #topbar,
+ .app-mode #sb-wrap,
+ .app-mode #lsb,
+ .app-mode #pp,
+ .app-mode #pp-pull-tab,
+ .app-mode #rp,
+ .app-mode #ctb,
+ .app-mode #sugs,
+ .app-mode #tc,
+ .app-mode .frame-chrome,
+ .app-mode .btb,
+ .app-mode .edge-zone,
+ .app-mode .immersive-enter,
+ .app-mode .immersive-exit {
+ display: none !important;
+ }
+
+ /* Togglable sub-elements: hide when their toggle is off */
+ .app-mode.app-hide-status #ts {
+ display: none !important;
+ }
+
+ .app-mode.app-hide-voice #dv-voice-bar {
+ display: none !important;
+ }
+
+ .app-mode.app-hide-chat #cb,
+ .app-mode.app-hide-chat #chat-feed {
+ display: none !important;
+ }
+
+ /* Full-screen layout: body becomes a flex column, content fills remaining space */
+ .app-mode {
+ display: flex !important;
+ flex-direction: column !important;
+ }
+
+ .app-mode #main {
+ flex: 1;
+ min-height: 0;
+ }
+
+ .app-mode #canvas {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ }
+
+ .app-mode #panel-area {
+ flex: 1;
+ position: relative;
+ min-height: 0;
+ }
+
+ .app-mode #bframe {
+ margin: 0;
+ border-radius: 0;
+ border: none;
+ box-shadow: none;
+ width: 100% !important;
+ max-width: 100% !important;
+ flex: 1;
+ min-height: 0;
+ }
+
+ .app-mode #lp {
+ border-radius: 0;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+ }
+
+ .app-mode #preview-frame {
+ width: 100%;
+ height: 100%;
+ border: none;
+ }
+
+ /* Chat — docked at bottom by default */
+ .app-mode #chat {
+ flex-shrink: 0;
+ background: rgba(3, 6, 16, .95);
+ border-top: 1px solid rgba(255, 255, 255, .08);
+ padding: 0;
+ transition: max-height .25s ease;
+ position: relative;
+ }
+
+ .app-mode #chat-feed {
+ max-height: clamp(120px, 22dvh, 260px) !important;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ border-bottom: 1px solid rgba(255, 255, 255, .05);
+ }
+
+ .app-mode #chat-feed:empty {
+ display: none !important;
+ }
+
+ .app-mode.has-chat-transcript #panel-area {
+ min-height: var(--coding-action-min-h);
+ }
+
+ .app-mode.has-chat-transcript #bframe {
+ min-height: var(--coding-action-min-h);
+ }
+
+ .app-mode.has-chat-transcript #chat {
+ max-height: min(44dvh, calc(var(--coding-transcript-max-h) + 112px));
+ }
+
+ .app-mode.has-chat-transcript #chat-feed {
+ display: flex !important;
+ min-height: var(--coding-transcript-min-h);
+ max-height: var(--coding-transcript-max-h) !important;
+ }
+
+ .app-mode #ts {
+ padding: 3px 10px;
+ font-size: 10px;
+ }
+
+ .app-mode #dv-voice-bar {
+ padding: 2px 8px;
+ font-size: 9px;
+ min-height: 20px;
+ border-bottom: 1px solid rgba(255, 255, 255, .04);
+ }
+
+ .app-mode #cb {
+ padding: 4px 8px;
+ gap: 5px;
+ width: min(calc(100% - 16px), 1120px);
+ }
+
+ .app-mode #cb textarea {
+ font-size: 12px;
+ max-height: 40px;
+ min-height: 18px;
+ padding: 3px 8px;
+ line-height: 1.3;
+ }
+
+ /* ── Floating draggable chat window ── */
+ .app-mode.app-chat-float #chat {
+ position: fixed !important;
+ z-index: 450;
+ bottom: 12px;
+ right: 12px;
+ left: auto;
+ top: auto;
+ width: 360px;
+ max-height: 340px;
+ min-width: 260px;
+ border-radius: 12px;
+ border: 1px solid rgba(255, 255, 255, .12);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, .5);
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ resize: both;
+ background: rgba(3, 6, 16, .96);
+ backdrop-filter: blur(14px);
+ }
+
+ .app-mode.app-chat-float #chat-feed {
+ max-height: 180px !important;
+ flex: 1;
+ min-height: 0;
+ }
+
+ .app-mode.app-chat-float #cb textarea {
+ max-height: 60px;
+ }
+
+ /* Chat float drag handle */
+ #app-chat-drag {
+ display: none;
+ height: 22px;
+ cursor: grab;
+ flex-shrink: 0;
+ background: rgba(255, 255, 255, .04);
+ border-bottom: 1px solid rgba(255, 255, 255, .06);
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 8px;
+ font-size: 9px;
+ color: var(--t3);
+ user-select: none;
+ }
+
+ #app-chat-drag:active {
+ cursor: grabbing;
+ }
+
+ .app-mode.app-chat-float #app-chat-drag {
+ display: flex;
+ }
+
+ #app-chat-drag .chat-drag-dots {
+ display: flex;
+ gap: 2px;
+ opacity: .3;
+ }
+
+ #app-chat-drag .chat-drag-dots span {
+ width: 3px;
+ height: 3px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, .5);
+ }
+
+ #app-chat-drag .chat-drag-actions {
+ display: flex;
+ gap: 4px;
+ }
+
+ #app-chat-drag .chat-drag-actions button {
+ width: 16px;
+ height: 16px;
+ border: none;
+ background: none;
+ color: var(--t3);
+ font-size: 8px;
+ cursor: pointer;
+ border-radius: 3px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ #app-chat-drag .chat-drag-actions button:hover {
+ background: rgba(255, 255, 255, .1);
+ color: #fff;
+ }
+
+ /* ── Floating App Mode Toolbar (draggable) ── */
+ #app-mode-toolbar {
+ position: fixed;
+ top: 8px;
+ right: 8px;
+ z-index: 500;
+ display: none;
+ align-items: center;
+ gap: 3px;
+ background: rgba(3, 6, 16, .88);
+ backdrop-filter: blur(12px);
+ border: 1px solid rgba(255, 255, 255, .1);
+ border-radius: 10px;
+ padding: 3px 5px;
+ opacity: var(--amt-base-opacity, .35);
+ transition: opacity .2s;
+ user-select: none;
+ touch-action: none;
+ }
+
+ #app-mode-toolbar:hover,
+ #app-mode-toolbar.dragging {
+ opacity: 1;
+ }
+
+ .app-mode #app-mode-toolbar {
+ display: flex;
+ }
+
+ /* Drag handle */
+ .amt-drag {
+ width: 14px;
+ height: 24px;
+ cursor: grab;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 2px;
+ opacity: .3;
+ transition: opacity .15s;
+ flex-shrink: 0;
+ }
+
+ .amt-drag:hover {
+ opacity: .7;
+ }
+
+ .amt-drag:active {
+ cursor: grabbing;
+ }
+
+ .amt-drag span {
+ display: block;
+ width: 10px;
+ height: 2px;
+ background: rgba(255, 255, 255, .4);
+ border-radius: 1px;
+ }
+
+ .amt-btn {
+ width: 28px;
+ height: 28px;
+ border-radius: 7px;
+ border: 1px solid rgba(255, 255, 255, .08);
+ background: none;
+ color: var(--t2);
+ font-size: 11px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all .15s;
+ flex-shrink: 0;
+ }
+
+ .amt-btn:hover {
+ background: rgba(147, 51, 234, .25);
+ border-color: rgba(147, 51, 234, .4);
+ color: #fff;
+ }
+
+ .amt-btn.on {
+ background: rgba(147, 51, 234, .2);
+ border-color: rgba(147, 51, 234, .35);
+ color: #D8B4FE;
+ }
+
+ .amt-btn.off {
+ opacity: .35;
+ }
+
+ .amt-btn.mic-active {
+ background: rgba(239, 68, 68, .25);
+ border-color: rgba(239, 68, 68, .5);
+ color: #FCA5A5;
+ animation: amtMicPulse 1.2s ease-in-out infinite;
+ }
+
+ @keyframes amtMicPulse {
+
+ 0%,
+ 100% {
+ box-shadow: 0 0 0 0 rgba(239, 68, 68, .3);
+ }
+
+ 50% {
+ box-shadow: 0 0 8px 2px rgba(239, 68, 68, .4);
+ }
+ }
+
+ .amt-sep {
+ width: 1px;
+ height: 18px;
+ background: rgba(255, 255, 255, .08);
+ margin: 0 2px;
+ flex-shrink: 0;
+ }
+
+ .amt-zoom {
+ font-size: 9px;
+ color: var(--t2);
+ min-width: 32px;
+ text-align: center;
+ font-family: 'Courier New', monospace;
+ }
+
+ /* ── Minimized chat in app mode ── */
+ .app-mode.app-chat-minimized #chat {
+ max-height: 0;
+ overflow: hidden;
+ border: none;
+ padding: 0;
+ transition: max-height .25s ease;
+ }
+
+ .app-mode.app-chat-minimized #dv-voice-bar,
+ .app-mode.app-chat-minimized #cb,
+ .app-mode.app-chat-minimized #chat-feed,
+ .app-mode.app-chat-minimized #ts {
+ display: none !important;
+ }
+
+ /* Mini chat indicator when minimized */
+ #app-chat-mini-bar {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 200;
+ height: 22px;
+ background: rgba(3, 6, 16, .92);
+ border-top: 1px solid rgba(255, 255, 255, .06);
+ display: none;
+ align-items: center;
+ padding: 0 10px;
+ cursor: pointer;
+ font-size: 9px;
+ color: var(--t3);
+ gap: 6px;
+ transition: all .15s;
+ }
+
+ #app-chat-mini-bar:hover {
+ background: rgba(147, 51, 234, .12);
+ color: var(--t1);
+ }
+
+ .app-mode.app-chat-minimized #app-chat-mini-bar {
+ display: flex;
+ }
+
+ /* ── App Mode Enter Button (visible in normal mode) ── */
+ #app-mode-enter {
+ position: fixed;
+ bottom: 10px;
+ right: 10px;
+ z-index: 300;
+ height: 30px;
+ border-radius: 8px;
+ padding: 0 10px;
+ background: rgba(8, 14, 28, .85);
+ border: 1px solid rgba(255, 255, 255, .1);
+ color: var(--t2);
+ font-size: 10px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ backdrop-filter: blur(8px);
+ transition: all .2s;
+ opacity: .45;
+ font-family: var(--fb);
+ }
+
+ #app-mode-enter:hover {
+ opacity: 1;
+ background: rgba(147, 51, 234, .25);
+ border-color: rgba(147, 51, 234, .4);
+ color: #fff;
+ }
+
+ .app-mode #app-mode-enter {
+ display: none !important;
+ }
+
+ /* Zoom transform on the preview frame */
+ .app-mode #preview-frame {
+ transform-origin: 0 0;
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ RESPONSIVE — COMPACT DESKTOP (≤1536px)
+ ══════════════════════════════════════════════════════════ */
+ @media (max-width: 1536px) {
+ .pill-sub,
+ #tb-clock {
+ display: none !important;
+ }
+
+ .agent-pills {
+ gap: 1px;
+ }
+
+ .pill {
+ padding: 4px 5px;
+ }
+
+ .top-device-switcher {
+ padding-inline: 2px !important;
+ }
+
+ .ptab {
+ padding-inline: 6px;
+ }
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ RESPONSIVE — TABLET (Tab S9+, ≤1024px)
+ ══════════════════════════════════════════════════════════ */
+ @media (max-width: 1024px) {
+ #rp {
+ width: 0;
+ min-width: 0;
+ max-width: 0;
+ border-left: none;
+ }
+
+ #rp-resizer {
+ display: none;
+ }
+
+ #rp-tb {
+ display: none;
+ }
+
+ .layout-btn {
+ display: none;
+ }
+
+ .curl {
+ display: none;
+ }
+
+ .dm-indicator {
+ display: none;
+ }
+
+ #ctb .ctab.dev-mode {
+ display: none;
+ }
+
+ #ctb .vdiv {
+ display: none;
+ }
+
+ .pill-sub {
+ display: none;
+ }
+
+ .pill {
+ padding: 3px 6px;
+ font-size: 10px;
+ }
+
+ #topbar {
+ padding: 0 6px;
+ gap: 4px;
+ }
+
+ .tb-right {
+ gap: 2px;
+ }
+
+ #tb-clock {
+ display: none !important;
+ }
+
+ #pp {
+ width: 220px;
+ }
+
+ #bframe {
+ margin: 4px 4px 0;
+ }
+
+ .dm-desktop #bframe {
+ margin: 4px 4px 0;
+ }
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ RESPONSIVE — MOBILE (S21 Ultra, ≤600px)
+ ══════════════════════════════════════════════════════════ */
+ @media (max-width: 600px) {
+ :root {
+ --topbar-h: 40px;
+ --modebar-h: 32px;
+ --transcript-max-h: clamp(130px, 28dvh, 220px);
+ --action-window-min-h: 220px;
+ }
+
+ /* Hide sidebar rail on mobile — too narrow */
+ #lsb {
+ display: none;
+ }
+
+ /* Show mobile menu button */
+ #mobile-menu-btn {
+ display: block !important;
+ }
+
+ /* Right panel always collapsed */
+ #rp {
+ width: 0;
+ min-width: 0;
+ max-width: 0;
+ border-left: none;
+ }
+
+ /* Fly-out panel: full width overlay on mobile */
+ #pp {
+ left: 0;
+ width: 100vw;
+ border-right: none;
+ z-index: 60;
+ }
+
+ /* Header: compact */
+ #topbar {
+ height: 40px;
+ padding: 0 6px;
+ gap: 3px;
+ min-width: 0;
+ overflow: hidden;
+ }
+
+ .logo-text {
+ display: none;
+ }
+
+ .agent-pills {
+ display: none;
+ }
+
+ .vdiv {
+ display: none;
+ }
+
+ .tb-right {
+ gap: 2px;
+ margin-left: auto;
+ flex-shrink: 0;
+ }
+
+ .tb-btn {
+ width: 26px;
+ height: 26px;
+ }
+
+ #tb-clock {
+ display: none !important;
+ }
+
+ .layout-btn {
+ display: none;
+ }
+
+ /* Mobile gets its own preview toolbar below; desktop topbar controls bunch the S21 layout. */
+ .top-device-switcher,
+ #topbar .panel-tabs,
+ #topbar .api-status,
+ #admin-badge,
+ .avatar-name {
+ display: none !important;
+ }
+
+ #mode-badge {
+ min-width: 0;
+ max-width: 104px;
+ flex-shrink: 1;
+ }
+
+ #mode-badge .mode-label {
+ max-width: 54px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .panel-tabs .ptab {
+ font-size: 9px;
+ padding: 3px 6px;
+ }
+
+ .panel-tabs .ptab span {
+ display: none;
+ }
+
+ .api-status span {
+ display: none;
+ }
+
+ /* Canvas toolbar: hide device mode buttons */
+ #ctb .ctab.dev-mode {
+ display: none;
+ }
+
+ #ctb .vdiv {
+ display: none;
+ }
+
+ .curl {
+ display: none;
+ }
+
+ .dm-indicator {
+ display: none;
+ }
+
+ #ctb {
+ height: 32px;
+ padding: 0 4px;
+ min-width: 0;
+ overflow: hidden;
+ }
+
+ /* Preview frame: tighter margins */
+ #bframe {
+ margin: 2px 2px 0;
+ border-radius: 6px;
+ }
+
+ .dm-desktop #bframe {
+ margin: 2px 2px 0;
+ }
+
+ #main,
+ #canvas,
+ #panel-area {
+ min-width: 0;
+ width: 100%;
+ }
+
+ #bframe,
+ .dm-desktop #bframe,
+ .dm-tablet #bframe,
+ .dm-android #bframe,
+ .dm-mobile #bframe {
+ width: auto !important;
+ max-width: none !important;
+ min-width: 0;
+ margin: 2px 2px 0;
+ }
+
+ #lp {
+ overflow-x: hidden;
+ }
+
+ #demo-hero {
+ padding: 18px 16px;
+ gap: 9px;
+ overflow-y: auto;
+ justify-content: center;
+ }
+
+ .demo-cat-badge {
+ padding: 4px 10px;
+ font-size: 8.5px;
+ }
+
+ .demo-title {
+ font-size: clamp(24px, 9vw, 34px);
+ line-height: 1.06;
+ }
+
+ .demo-sub {
+ font-size: 11.5px;
+ line-height: 1.45;
+ max-width: 330px;
+ }
+
+ .demo-actions {
+ gap: 8px;
+ margin-top: 6px;
+ }
+
+ .demo-btn-play,
+ .demo-btn-create {
+ min-width: min(240px, calc(100vw - 96px));
+ justify-content: center;
+ padding: 11px 18px;
+ border-radius: 12px;
+ font-size: 13px;
+ }
+
+ .demo-meta {
+ font-size: 8.5px;
+ gap: 5px;
+ margin-top: 2px;
+ }
+
+ #game-carousel {
+ bottom: 38px;
+ width: calc(100% - 28px);
+ gap: 8px;
+ }
+
+ .carousel-container {
+ gap: 8px;
+ justify-content: center;
+ }
+
+ .carousel-arrow {
+ width: 34px;
+ height: 34px;
+ }
+
+ .carousel-track {
+ flex: 0 1 min(236px, calc(100vw - 118px));
+ height: 82px;
+ }
+
+ .game-card {
+ min-width: min(236px, calc(100vw - 118px));
+ height: 78px;
+ padding: 9px 11px;
+ }
+
+ .btb {
+ height: 28px;
+ padding: 0 6px;
+ }
+
+ /* Status bar: hide on mobile to save space */
+ #sb-wrap {
+ display: none !important;
+ }
+
+ #sb-wrap>div:first-child {
+ padding: 0 6px;
+ }
+
+ #sb-wrap>div:first-child span:last-child {
+ display: none;
+ }
+
+ #sb-ts {
+ display: none;
+ }
+
+ /* Chat area: full width, comfortable padding */
+ #pc {
+ padding: 24px 12px 20px;
+ }
+
+ .ph {
+ max-width: 100%;
+ }
+
+ .p-title {
+ font-size: 22px;
+ }
+
+ .p-sub {
+ font-size: 12px;
+ }
+
+ .sug {
+ font-size: 10px;
+ padding: 8px 10px;
+ }
+
+ #cb {
+ margin: 0 4px 4px;
+ width: calc(100% - 8px);
+ max-width: none;
+ gap: 4px;
+ padding: 6px 6px 4px;
+ }
+
+ #chat {
+ max-height: clamp(150px, 34dvh, 280px);
+ overflow: hidden;
+ padding-bottom: 4px;
+ }
+
+ #chat-feed {
+ max-height: var(--transcript-max-h) !important;
+ overflow: auto;
+ }
+
+ body.has-chat-transcript {
+ --coding-action-min-h: clamp(220px, 42dvh, 380px);
+ --coding-transcript-min-h: clamp(110px, 20dvh, 180px);
+ --coding-transcript-max-h: clamp(150px, 28dvh, 240px);
+ }
+
+ body.has-chat-transcript #chat-feed {
+ min-height: var(--coding-transcript-min-h);
+ max-height: var(--coding-transcript-max-h) !important;
+ }
+
+ #sugs {
+ display: none;
+ }
+
+ #pi {
+ min-height: 34px;
+ max-height: 70px;
+ padding: 7px 9px;
+ font-size: 12px;
+ }
+
+ .cib {
+ width: 28px;
+ height: 28px;
+ flex-shrink: 0;
+ }
+
+ .sbtn {
+ width: 32px;
+ height: 32px;
+ flex-shrink: 0;
+ }
+
+ #dv-voice-bar {
+ width: calc(100% - 8px);
+ max-width: none;
+ margin: 0 4px 2px;
+ padding: 3px 6px;
+ gap: 4px;
+ overflow-x: auto;
+ overflow-y: hidden;
+ white-space: nowrap;
+ scrollbar-width: none;
+ }
+
+ #dv-voice-bar::-webkit-scrollbar {
+ display: none;
+ }
+
+ #dv-voice-bar .dvb-engine,
+ #dv-voice-bar .dvb-voice,
+ #dv-voice-bar #dvb-think-label,
+ #dv-voice-bar #dvb-chatmode-label,
+ #dvb-hint {
+ display: none !important;
+ }
+
+ #dv-voice-bar .dvb-mode,
+ #dv-voice-bar .dvb-btn {
+ flex-shrink: 0;
+ }
+
+ #app-mode-enter {
+ bottom: 100px;
+ right: 8px;
+ width: 34px;
+ height: 34px;
+ padding: 0;
+ justify-content: center;
+ border-radius: 10px;
+ font-size: 0;
+ opacity: .65;
+ }
+
+ #app-mode-enter i {
+ font-size: 12px !important;
+ }
+
+ /* Auth modal */
+ .am-box {
+ width: 92vw;
+ max-width: 360px;
+ padding: 24px 18px;
+ }
+
+ /* Settings modal */
+ #settings-ov .stg-box {
+ width: 92vw;
+ max-width: 360px;
+ }
+
+ /* Tool modal */
+ .tm-box {
+ width: 95vw;
+ max-width: none;
+ height: 70vh;
+ }
+
+ /* Layout picker */
+ .lgrid {
+ max-width: 100%;
+ padding: 2px;
+ }
+
+ .lcard {
+ min-width: 140px;
+ }
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ RESPONSIVE — SMALL MOBILE (≤380px)
+ ══════════════════════════════════════════════════════════ */
+ @media (max-width: 380px) {
+ #topbar {
+ height: 36px;
+ }
+
+ .logo-icon {
+ width: 22px;
+ height: 22px;
+ font-size: 10px;
+ }
+
+ .panel-tabs .ptab {
+ padding: 2px 4px;
+ font-size: 8px;
+ }
+
+ .avatar {
+ width: 24px;
+ height: 24px;
+ font-size: 9px;
+ }
+
+ #sb-wrap {
+ display: none !important;
+ }
+
+ #pc {
+ padding: 16px 8px 12px;
+ }
+
+ .p-title {
+ font-size: 18px;
+ }
+ }
+
+ /* ══ MODE-AWARE UI (Phase 3 + 5 — spec wave2-handoffs/08) ══ */
+ :root {
+ --mode-chat: #A78BFA;
+ --mode-web: #60A5FA;
+ --mode-build: #8B5CF6;
+ --mode-code: #3B82F6;
+ --mode-files: #22D3EE;
+ --mode-research: #14B8A6;
+ --mode-problem: #8B5CF6;
+ --mode-game: #EC4899;
+ --mode-hermes: #10B981;
+ --mode-admin: #F59E0B;
+ --mode-active: var(--mode-build);
+ --mode-active-g: rgba(139, 92, 246, .4);
+ --mode-active-tint: rgba(139, 92, 246, .08);
+ }
+
+ body[data-mode="chat"] { --mode-active: #A78BFA; --mode-active-g: rgba(167,139,250,.4); --mode-active-tint: rgba(167,139,250,.08); }
+ body[data-mode="web"] { --mode-active: #60A5FA; --mode-active-g: rgba(96,165,250,.4); --mode-active-tint: rgba(96,165,250,.08); }
+ body[data-mode="build"] { --mode-active: #8B5CF6; --mode-active-g: rgba(139,92,246,.4); --mode-active-tint: rgba(139,92,246,.08); }
+ body[data-mode="code"] { --mode-active: #3B82F6; --mode-active-g: rgba(59,130,246,.4); --mode-active-tint: rgba(59,130,246,.08); }
+ body[data-mode="files"] { --mode-active: #22D3EE; --mode-active-g: rgba(34,211,238,.4); --mode-active-tint: rgba(34,211,238,.08); }
+ body[data-mode="research"] { --mode-active: #14B8A6; --mode-active-g: rgba(20,184,166,.4); --mode-active-tint: rgba(20,184,166,.08); }
+ body[data-mode="problem"] { --mode-active: #8B5CF6; --mode-active-g: rgba(139,92,246,.4); --mode-active-tint: rgba(139,92,246,.08); }
+ body[data-mode="game"] { --mode-active: #EC4899; --mode-active-g: rgba(236,72,153,.4); --mode-active-tint: rgba(236,72,153,.08); }
+ body[data-mode="hermes"] { --mode-active: #10B981; --mode-active-g: rgba(16,185,129,.4); --mode-active-tint: rgba(16,185,129,.08); }
+ body[data-mode="admin"] { --mode-active: #F59E0B; --mode-active-g: rgba(245,158,11,.4); --mode-active-tint: rgba(245,158,11,.08); }
+
+ /* Mode badge in topbar */
+ #mode-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ height: 24px;
+ padding: 0 10px 0 8px;
+ border-radius: 14px;
+ background: var(--mode-active-tint);
+ border: 1px solid var(--mode-active);
+ color: var(--mode-active);
+ font-family: var(--fb);
+ font-size: 10.5px;
+ font-weight: 600;
+ letter-spacing: .02em;
+ cursor: pointer;
+ user-select: none;
+ transition: background-color 300ms ease, color 300ms ease, border-color 300ms ease;
+ white-space: nowrap;
+ }
+ #mode-badge:hover {
+ background: var(--mode-active);
+ color: #fff;
+ }
+ #mode-badge .mode-icon {
+ font-size: 11px;
+ line-height: 1;
+ }
+ #mode-badge .mode-caret {
+ opacity: .6;
+ font-size: 8px;
+ margin-left: 2px;
+ }
+ #mode-badge.locked::after {
+ content: "🔒";
+ font-size: 8px;
+ margin-left: 2px;
+ opacity: .8;
+ }
+
+ /* Mode picker dropdown */
+ #mode-picker {
+ position: absolute;
+ top: 48px;
+ left: 8px;
+ min-width: 220px;
+ background: var(--bg-elevated);
+ backdrop-filter: blur(24px);
+ border: 1px solid var(--border-hi);
+ border-radius: 10px;
+ box-shadow: 0 16px 40px rgba(0, 0, 0, .55);
+ padding: 6px;
+ z-index: 200;
+ display: none;
+ font-family: var(--fb);
+ }
+ #mode-picker.open { display: block; }
+ #mode-picker .mp-item {
+ display: flex;
+ align-items: center;
+ gap: 9px;
+ padding: 7px 10px;
+ border-radius: 7px;
+ cursor: pointer;
+ font-size: 11px;
+ color: var(--t1);
+ transition: background-color 120ms ease;
+ }
+ #mode-picker .mp-item:hover { background: rgba(255, 255, 255, .06); }
+ #mode-picker .mp-item.cur { background: rgba(255, 255, 255, .08); }
+ #mode-picker .mp-item .mp-dot {
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ box-shadow: 0 0 6px currentColor;
+ }
+ #mode-picker .mp-item .mp-check {
+ margin-left: auto;
+ opacity: .85;
+ font-size: 9px;
+ }
+ #mode-picker .mp-foot {
+ border-top: 1px solid var(--border);
+ margin-top: 4px;
+ padding: 6px 10px;
+ font-size: 9px;
+ color: var(--t3);
+ line-height: 1.5;
+ }
+
+ /* Mode-pane container — overlays the existing #panel-area for non-build modes */
+ .mode-pane {
+ display: none;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+ padding: 0;
+ }
+ .mode-pane.vis { display: flex; }
+
+ .mode-stub {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 28px;
+ overflow-y: auto;
+ }
+ .mode-stub-card {
+ max-width: 560px;
+ width: 100%;
+ background: rgba(255, 255, 255, .03);
+ border: 1px solid var(--border-hi);
+ border-radius: 14px;
+ padding: 22px 24px;
+ color: var(--t1);
+ font-family: var(--fb);
+ }
+ .mode-stub-card h3 {
+ font-size: 15px;
+ font-weight: 700;
+ margin: 0 0 6px;
+ color: var(--mode-active);
+ font-family: var(--fd);
+ letter-spacing: .01em;
+ }
+ .mode-stub-card .ms-sub {
+ font-size: 11px;
+ color: var(--t2);
+ margin-bottom: 14px;
+ line-height: 1.5;
+ }
+ .mode-stub-card ul {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 14px;
+ font-size: 11px;
+ color: var(--t1);
+ }
+ .mode-stub-card li {
+ padding: 6px 0;
+ border-bottom: 1px solid rgba(255, 255, 255, .04);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .mode-stub-card li:last-child { border-bottom: none; }
+ .mode-stub-card li::before {
+ content: "•";
+ color: var(--mode-active);
+ font-size: 14px;
+ line-height: 0;
+ }
+ .mode-stub-card .ms-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ font-size: 10.5px;
+ color: var(--mode-active);
+ text-decoration: none;
+ padding: 6px 10px;
+ border-radius: 6px;
+ background: var(--mode-active-tint);
+ border: 1px solid var(--mode-active);
+ }
+ .mode-stub-card .ms-link:hover { background: var(--mode-active); color: #fff; }
+ .mode-stub-card .ms-urlbar {
+ display: flex;
+ gap: 6px;
+ margin: 0 0 12px;
+ }
+ .mode-stub-card .ms-urlbar input {
+ flex: 1;
+ background: rgba(255, 255, 255, .04);
+ border: 1px solid var(--border-hi);
+ border-radius: 6px;
+ color: var(--t1);
+ padding: 7px 10px;
+ font-size: 11px;
+ font-family: inherit;
+ outline: none;
+ }
+ .mode-stub-card .ms-urlbar input:focus { border-color: var(--mode-active); }
+ .mode-stub-card .ms-urlbar button {
+ background: var(--mode-active);
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ padding: 7px 14px;
+ font-size: 11px;
+ font-weight: 600;
+ cursor: pointer;
+ }
+
+ @media (prefers-reduced-motion: reduce) {
+ #mode-badge { transition: none; }
+ }
+
+ /* ── Codex/Claude-Code Parity Phase A1 — Command Palette, Slash Commands, Files Tree ── */
+ /* Command Palette overlay */
+ #cmd-palette-ov {
+ position: fixed; inset: 0; background: rgba(0,0,0,.55);
+ display: none; align-items: flex-start; justify-content: center;
+ padding-top: 12vh; z-index: 10500;
+ backdrop-filter: blur(4px);
+ }
+ #cmd-palette-ov.open { display: flex; }
+ #cmd-palette {
+ width: min(640px, 92vw); max-height: 70vh;
+ background: #0f1424; border: 1px solid rgba(139,92,246,.25);
+ border-radius: 10px; box-shadow: 0 24px 60px rgba(0,0,0,.55);
+ display: flex; flex-direction: column; overflow: hidden;
+ font-family: inherit;
+ }
+ #cmd-palette-input {
+ width: 100%; padding: 14px 16px; background: transparent;
+ border: none; outline: none; color: #fff; font-size: 14px;
+ border-bottom: 1px solid rgba(255,255,255,.06);
+ }
+ #cmd-palette-input::placeholder { color: rgba(255,255,255,.35); }
+ #cmd-palette-list {
+ flex: 1; overflow-y: auto; padding: 4px 0;
+ }
+ .cp-item {
+ display: flex; align-items: center; gap: 10px;
+ padding: 8px 14px; cursor: pointer; color: #cbd5e1;
+ font-size: 12.5px; border-left: 2px solid transparent;
+ }
+ .cp-item:hover, .cp-item.cp-active {
+ background: rgba(139,92,246,.12); color: #fff;
+ border-left-color: var(--mode-active, #8B5CF6);
+ }
+ .cp-item-icon {
+ width: 18px; text-align: center; color: var(--mode-active, #8B5CF6);
+ font-size: 11px; flex-shrink: 0;
+ }
+ .cp-item-label { flex: 1; }
+ .cp-item-hint {
+ font-size: 10px; color: rgba(255,255,255,.4);
+ padding: 1px 6px; border: 1px solid rgba(255,255,255,.1);
+ border-radius: 3px; font-family: ui-monospace, monospace;
+ }
+ .cp-section {
+ padding: 6px 14px 4px; font-size: 9.5px;
+ color: rgba(255,255,255,.4); text-transform: uppercase;
+ letter-spacing: .8px;
+ }
+ #cmd-palette-foot {
+ padding: 6px 14px; border-top: 1px solid rgba(255,255,255,.06);
+ font-size: 10px; color: rgba(255,255,255,.45);
+ display: flex; gap: 12px;
+ }
+
+ /* Slash command dropdown above chat input */
+ #slash-dd {
+ position: absolute; left: 0; right: 0; bottom: 100%;
+ margin-bottom: 6px; background: #0f1424;
+ border: 1px solid rgba(139,92,246,.3); border-radius: 8px;
+ box-shadow: 0 -8px 24px rgba(0,0,0,.4);
+ max-height: 260px; overflow-y: auto; display: none;
+ z-index: 1200;
+ }
+ #slash-dd.open { display: block; }
+ .sl-item {
+ display: flex; align-items: center; gap: 8px;
+ padding: 7px 12px; cursor: pointer; color: #cbd5e1;
+ font-size: 12px; border-left: 2px solid transparent;
+ }
+ .sl-item:hover, .sl-item.sl-active {
+ background: rgba(139,92,246,.13); color: #fff;
+ border-left-color: var(--mode-active, #8B5CF6);
+ }
+ .sl-item-cmd {
+ font-family: ui-monospace, monospace; color: #93C5FD;
+ font-weight: 600; min-width: 90px;
+ }
+ .sl-item-desc {
+ flex: 1; font-size: 11px; color: rgba(255,255,255,.55);
+ }
+ .sl-item-locked { opacity: .55; cursor: not-allowed; }
+ .sl-item-locked .sl-item-cmd { color: #fbbf24; }
+
+ /* Files mini-tree (mode-pane-files content) */
+ #mode-pane-files .ft-wrap {
+ display: flex; flex-direction: column; width: 100%; height: 100%;
+ background: #0a0e1a; color: #cbd5e1;
+ }
+ #mode-pane-files .ft-bar {
+ display: flex; gap: 6px; padding: 8px 10px;
+ border-bottom: 1px solid rgba(255,255,255,.06);
+ background: #0f1424;
+ }
+ #mode-pane-files .ft-filter {
+ flex: 1; padding: 5px 9px; background: rgba(255,255,255,.04);
+ border: 1px solid rgba(255,255,255,.08); border-radius: 4px;
+ color: #fff; font-size: 11px; outline: none;
+ }
+ #mode-pane-files .ft-filter:focus { border-color: var(--mode-active, #8B5CF6); }
+ #mode-pane-files .ft-root-select {
+ padding: 5px 8px; background: rgba(255,255,255,.04);
+ border: 1px solid rgba(255,255,255,.08); border-radius: 4px;
+ color: #fff; font-size: 11px; outline: none; cursor: pointer;
+ }
+ #mode-pane-files .ft-refresh {
+ padding: 5px 9px; background: rgba(139,92,246,.18);
+ border: 1px solid rgba(139,92,246,.4); border-radius: 4px;
+ color: #fff; font-size: 11px; cursor: pointer;
+ }
+ #mode-pane-files .ft-body {
+ display: flex; flex: 1; min-height: 0;
+ }
+ #mode-pane-files .ft-tree {
+ width: 38%; min-width: 220px;
+ overflow-y: auto; padding: 6px 0;
+ border-right: 1px solid rgba(255,255,255,.06);
+ font-family: ui-monospace, monospace; font-size: 11.5px;
+ }
+ #mode-pane-files .ft-node {
+ padding: 3px 8px 3px 0; cursor: pointer; user-select: none;
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
+ }
+ #mode-pane-files .ft-node:hover { background: rgba(34,211,238,.10); }
+ #mode-pane-files .ft-node.ft-sel { background: rgba(34,211,238,.20); color: #fff; }
+ #mode-pane-files .ft-icon { display: inline-block; width: 14px; color: #94a3b8; }
+ #mode-pane-files .ft-dir { color: #93C5FD; }
+ #mode-pane-files .ft-file { color: #cbd5e1; }
+ #mode-pane-files .ft-empty { padding: 12px; color: rgba(255,255,255,.4); font-size: 11px; }
+ #mode-pane-files .ft-view {
+ flex: 1; overflow: auto; padding: 10px 12px;
+ background: #060916; font-family: ui-monospace, monospace;
+ font-size: 11.5px; line-height: 1.5; color: #e2e8f0;
+ white-space: pre-wrap; word-break: break-word;
+ }
+ #mode-pane-files .ft-view-empty {
+ color: rgba(255,255,255,.4); font-family: inherit;
+ }
+ #mode-pane-files .ft-view-head {
+ font-family: inherit; font-size: 10.5px; color: var(--mode-active, #8B5CF6);
+ padding-bottom: 6px; margin-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,.06);
+ }
+
+ /* ══ MOBILE TOUCH TARGET FIX (S21 Ultra audit) ══ */
+ /* Voice bar buttons (action, voice, auto, settings) — enforce 44px minimum touch target */
+ #dv-voice-bar .dvb-mode,
+ #dv-voice-bar .dvb-engine,
+ #dv-voice-bar .dvb-voice,
+ #dv-voice-bar .dvb-btn {
+ min-height: 44px;
+ min-width: 44px;
+ }
+
+ /* Composer buttons (screenshot, code, mic, send) — enforce 44px minimum touch target */
+ #cb .cib {
+ min-height: 44px;
+ min-width: 44px;
+ }
+
+ /* Side-chat workspace presets keep the full composer usable in a narrow column. */
+ @media (min-width: 1025px) {
+ body.workspace-chat-side-right #dv-voice-bar,
+ body.workspace-chat-side-left #dv-voice-bar {
+ width: calc(100% - 16px);
+ max-width: none;
+ overflow: hidden;
+ gap: 5px;
+ }
+
+ body.workspace-chat-side-right #dv-voice-bar .dvb-engine,
+ body.workspace-chat-side-left #dv-voice-bar .dvb-engine,
+ body.workspace-chat-side-right #dv-voice-bar .dvb-voice,
+ body.workspace-chat-side-left #dv-voice-bar .dvb-voice,
+ body.workspace-chat-side-right #dv-voice-bar .dvb-sep,
+ body.workspace-chat-side-left #dv-voice-bar .dvb-sep,
+ body.workspace-chat-side-right #dvb-think-label,
+ body.workspace-chat-side-left #dvb-think-label,
+ body.workspace-chat-side-right #dvb-chatmode-label,
+ body.workspace-chat-side-left #dvb-chatmode-label,
+ body.workspace-chat-side-right #dvb-hint,
+ body.workspace-chat-side-left #dvb-hint {
+ display: none !important;
+ }
+
+ body.workspace-chat-side-right #dv-voice-bar .dvb-mode,
+ body.workspace-chat-side-left #dv-voice-bar .dvb-mode {
+ flex: 1 1 auto;
+ min-width: 0;
+ justify-content: center;
+ }
+
+ body.workspace-chat-side-right #dv-voice-bar .dvb-btn,
+ body.workspace-chat-side-left #dv-voice-bar .dvb-btn {
+ flex: 0 0 44px;
+ width: 44px;
+ min-width: 44px !important;
+ padding-inline: 0;
+ text-align: center;
+ }
+
+ body.workspace-chat-side-right #dvb-chatmode,
+ body.workspace-chat-side-left #dvb-chatmode {
+ min-width: 44px !important;
+ }
+
+ body.workspace-chat-side-right #cb,
+ body.workspace-chat-side-left #cb {
+ gap: 4px;
+ padding-inline: 8px;
+ }
+
+ body.workspace-chat-side-right #pi,
+ body.workspace-chat-side-left #pi {
+ min-width: 0;
+ padding-inline: 9px;
+ }
+
+ body.workspace-chat-side-right #cb .csep,
+ body.workspace-chat-side-left #cb .csep {
+ display: none;
+ }
+
+ body.workspace-chat-side-right #app-mode-enter {
+ right: calc(var(--workspace-chat-side-w) + 10px);
+ }
+ }
+
+ /* ══════════════════════════════════════════════════════════
+ COMPLETE DEVICE WORKSPACES
+ Device selection adapts the full builder shell, not only the iframe.
+ ══════════════════════════════════════════════════════════ */
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) {
+ --right-panel-max-w: min(72vw, 420px);
+ --transcript-max-h: clamp(90px, 19dvh, 170px);
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #main {
+ position: relative;
+ min-width: 0;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #canvas {
+ order: 1 !important;
+ display: flex !important;
+ flex: 1 1 auto;
+ flex-direction: column;
+ min-width: 0;
+ width: 100%;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #ctb,
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #panel-area,
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #chat {
+ grid-column: auto !important;
+ grid-row: auto !important;
+ min-width: 0;
+ width: 100%;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #panel-area {
+ flex: 1 1 auto;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #chat {
+ flex: 0 0 auto;
+ border-right: 0;
+ border-left: 0;
+ border-top: 1px solid rgba(var(--workspace-primary-rgb), .13);
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #rp {
+ position: fixed;
+ top: calc(var(--topbar-h) + var(--statusbar-h));
+ right: 0;
+ bottom: 0;
+ left: auto;
+ z-index: 850;
+ order: 3 !important;
+ width: min(360px, 72vw) !important;
+ min-width: min(320px, 72vw) !important;
+ max-width: min(420px, 78vw) !important;
+ height: auto;
+ border: 1px solid rgba(var(--workspace-primary-rgb), .18);
+ border-right: 0;
+ border-radius: 12px 0 0 12px;
+ box-shadow: -18px 0 48px rgba(0, 0, 0, .48);
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #rp.collapsed {
+ width: 0 !important;
+ min-width: 0 !important;
+ max-width: 0 !important;
+ border: 0;
+ box-shadow: none;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #rp-resizer {
+ display: none !important;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #rp-tb {
+ display: flex !important;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) .layout-btn {
+ display: flex !important;
+ width: 28px;
+ min-width: 28px;
+ flex: 0 0 28px;
+ justify-content: center;
+ gap: 0;
+ padding: 0;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) .layout-btn #ln,
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) .layout-btn .fa-chevron-down {
+ display: none !important;
+ }
+
+ body[data-device-mode="tablet"] {
+ --topbar-h: 42px;
+ --modebar-h: 34px;
+ --composer-h: 46px;
+ }
+
+ body[data-device-mode="tablet"] #topbar {
+ height: var(--topbar-h);
+ padding-inline: 7px;
+ gap: 4px;
+ }
+
+ body[data-device-mode="tablet"] .pill-sub,
+ body[data-device-mode="tablet"] #tb-clock,
+ body[data-device-mode="tablet"] .avatar-name {
+ display: none !important;
+ }
+
+ body[data-device-mode="tablet"] .agent-pills {
+ gap: 1px;
+ }
+
+ body[data-device-mode="tablet"] .pill {
+ padding: 3px 5px;
+ }
+
+ body[data-device-mode="tablet"] #lsb {
+ width: 44px;
+ min-width: 44px;
+ }
+
+ body[data-device-mode="tablet"] #pp,
+ body[data-device-mode="tablet"] #pp-pull-tab {
+ left: 44px;
+ }
+
+ body[data-device-mode="tablet"] #pp {
+ width: min(300px, 52vw);
+ }
+
+ body[data-device-mode="tablet"] #chat {
+ max-height: clamp(150px, 25dvh, 210px);
+ }
+
+ body[data-device-mode="tablet"] #bframe {
+ width: 768px;
+ max-width: calc(100% - 16px);
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) {
+ --topbar-h: 40px;
+ --statusbar-h: 0px;
+ --modebar-h: 32px;
+ --composer-h: 44px;
+ --transcript-max-h: clamp(80px, 16dvh, 140px);
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #topbar {
+ height: var(--topbar-h);
+ min-width: 0;
+ padding: 0 6px;
+ gap: 3px;
+ overflow: visible;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .logo-text,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .agent-pills,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #topbar > .vdiv,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #topbar .panel-tabs,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #topbar .api-status,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #tb-clock,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .avatar-name,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #admin-badge {
+ display: none !important;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #mobile-menu-btn {
+ display: block !important;
+ flex: 0 0 auto;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #mode-badge {
+ max-width: 94px;
+ min-width: 0;
+ padding-inline: 7px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #mode-badge .mode-label {
+ max-width: 46px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .top-device-switcher {
+ display: flex !important;
+ gap: 1px !important;
+ padding-inline: 1px !important;
+ margin-left: auto;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .top-device-switcher .ctab {
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ width: 25px;
+ height: 25px;
+ padding: 0;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .tb-right {
+ margin-left: 0;
+ gap: 3px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #rp-tb,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .avatar {
+ width: 28px;
+ height: 28px;
+ min-width: 28px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #sb-wrap,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #lsb,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #pp-pull-tab {
+ display: none !important;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #pp {
+ position: fixed;
+ top: var(--topbar-h);
+ bottom: 0;
+ left: 0;
+ width: min(100vw, 420px);
+ z-index: 900;
+ border-right: 1px solid rgba(var(--workspace-primary-rgb), .2);
+ box-shadow: 18px 0 48px rgba(0, 0, 0, .5);
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #rp {
+ top: var(--topbar-h);
+ width: min(92vw, 360px) !important;
+ min-width: min(320px, 92vw) !important;
+ max-width: min(92vw, 360px) !important;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #ctb {
+ height: var(--modebar-h);
+ padding-inline: 5px;
+ overflow: hidden;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #ctb .ctab.dev-mode,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #ctb .vdiv,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .curl {
+ display: none !important;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #chat {
+ max-height: clamp(142px, 24dvh, 180px);
+ padding-bottom: 4px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #sugs,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dvb-hint,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-engine,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-voice,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-sep,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dvb-think-label,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dvb-chatmode-label {
+ display: none !important;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar {
+ width: calc(100% - 8px);
+ max-width: none;
+ margin: 0 4px 2px;
+ padding: 3px 6px;
+ gap: 4px;
+ overflow-x: auto;
+ overflow-y: hidden;
+ white-space: nowrap;
+ scrollbar-width: none;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar::-webkit-scrollbar {
+ display: none;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-mode,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #dv-voice-bar .dvb-btn {
+ flex-shrink: 0;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #cb {
+ width: calc(100% - 8px);
+ max-width: none;
+ margin-inline: 4px;
+ padding: 4px 6px;
+ gap: 4px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #pi {
+ min-width: 0;
+ min-height: 36px;
+ padding: 7px 9px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #bframe {
+ max-width: calc(100% - 8px);
+ margin: 4px auto 0;
+ }
+
+ body[data-device-mode="android"] #bframe {
+ width: 412px;
+ }
+
+ body[data-device-mode="mobile"] #bframe {
+ width: 375px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #app-mode-enter {
+ right: 8px;
+ bottom: 96px;
+ width: 34px;
+ height: 34px;
+ padding: 0;
+ justify-content: center;
+ font-size: 0;
+ }
+
+ /* Responsive showcase content inside simulated Tablet/Phone frames. */
+ body[data-device-mode="tablet"] #demo-hero {
+ justify-content: center;
+ padding: 22px 22px 136px;
+ gap: 8px;
+ overflow: hidden;
+ }
+
+ body[data-device-mode="tablet"] .demo-title {
+ font-size: clamp(30px, 4vw, 40px);
+ }
+
+ body[data-device-mode="tablet"] .demo-sub {
+ max-width: 500px;
+ font-size: 12.5px;
+ line-height: 1.45;
+ }
+
+ body[data-device-mode="tablet"] .demo-actions {
+ margin-top: 5px;
+ gap: 10px;
+ }
+
+ body[data-device-mode="tablet"] .demo-btn-play,
+ body[data-device-mode="tablet"] .demo-btn-create {
+ padding: 11px 24px;
+ border-radius: 11px;
+ font-size: 13px;
+ }
+
+ body[data-device-mode="tablet"] #game-carousel {
+ bottom: 12px;
+ width: calc(100% - 32px);
+ gap: 7px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #demo-hero {
+ justify-content: center;
+ padding: 14px 14px 116px;
+ gap: 7px;
+ overflow: hidden;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-cat-badge {
+ padding: 4px 10px;
+ font-size: 8px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-title {
+ width: 100%;
+ max-width: 332px;
+ font-size: 30px;
+ line-height: 1.04;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-sub {
+ max-width: 320px;
+ font-size: 11px;
+ line-height: 1.38;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ overflow: hidden;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-actions {
+ width: 100%;
+ max-width: 326px;
+ flex-wrap: nowrap;
+ gap: 7px;
+ margin-top: 3px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-btn-play,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-btn-create {
+ flex: 1 1 0;
+ min-width: 0;
+ justify-content: center;
+ padding: 10px 7px;
+ border-radius: 10px;
+ font-size: 11.5px;
+ white-space: nowrap;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-btn-play i {
+ font-size: 12px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .demo-meta {
+ display: none;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) #game-carousel {
+ bottom: 10px;
+ width: calc(100% - 18px);
+ gap: 6px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .carousel-container {
+ gap: 6px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .carousel-arrow {
+ width: 32px;
+ height: 32px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .carousel-track {
+ flex: 1 1 auto;
+ min-width: 0;
+ height: 74px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .game-card {
+ min-width: 100%;
+ height: 72px;
+ padding: 8px 10px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]) .carousel-cards {
+ gap: 6px;
+ }
+
+ /* Responsive preset personalities: preserve each layout's workspace
+ structure instead of reducing the presets to colour changes. */
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #ctb {
+ order: 0;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #panel-area {
+ order: 1;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]) #chat {
+ order: 2;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-side-left #chat {
+ order: 1;
+ border-top-color: rgba(var(--workspace-secondary-rgb), .28);
+ border-bottom: 1px solid rgba(var(--workspace-secondary-rgb), .18);
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-side-left #panel-area {
+ order: 2;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-side-right #chat {
+ border-top-color: rgba(var(--workspace-secondary-rgb), .28);
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]).workspace-rp-left #rp {
+ right: auto;
+ left: 0;
+ border-right: 1px solid rgba(var(--workspace-primary-rgb), .18);
+ border-left: 0;
+ border-radius: 0 12px 12px 0;
+ box-shadow: 18px 0 48px rgba(0, 0, 0, .48);
+ }
+
+ body.workspace-rp-hidden #rp-tb {
+ display: none !important;
+ }
+
+ body:is([data-device-mode="tablet"], [data-device-mode="android"], [data-device-mode="mobile"]):not(.workspace-rp-hidden) #rp {
+ width: min(var(--workspace-activity-w, 360px), 92vw) !important;
+ min-width: min(300px, 86vw) !important;
+ max-width: min(420px, 92vw) !important;
+ }
+
+ body[data-device-mode="tablet"] .layout-btn {
+ width: auto;
+ max-width: 126px;
+ flex-basis: auto;
+ gap: 4px;
+ padding-inline: 8px;
+ }
+
+ body[data-device-mode="tablet"] .layout-btn #ln,
+ body[data-device-mode="tablet"] .layout-btn .fa-chevron-down {
+ display: inline !important;
+ }
+
+ body[data-device-mode="tablet"] .layout-btn #ln {
+ max-width: 82px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ body[data-device-mode="tablet"].workspace-rail-wide #lsb {
+ width: 52px;
+ min-width: 52px;
+ }
+
+ body[data-device-mode="tablet"].workspace-rail-wide #pp,
+ body[data-device-mode="tablet"].workspace-rail-wide #pp-pull-tab {
+ left: 52px;
+ }
+
+ body[data-device-mode="tablet"].workspace-rail-hidden #lsb {
+ display: none !important;
+ }
+
+ body[data-device-mode="tablet"].workspace-rail-hidden #pp,
+ body[data-device-mode="tablet"].workspace-rail-hidden #pp-pull-tab {
+ left: 0;
+ }
+
+ body[data-device-mode="tablet"].workspace-chat-compact {
+ --transcript-max-h: clamp(66px, 11dvh, 100px);
+ }
+
+ body[data-device-mode="tablet"].workspace-chat-compact #chat {
+ flex: 0 0 128px;
+ height: 128px;
+ max-height: 128px;
+ }
+
+ body[data-device-mode="tablet"].workspace-chat-compact #ts,
+ body[data-device-mode="tablet"].workspace-chat-compact #sugs {
+ display: none !important;
+ }
+
+ body[data-device-mode="tablet"].workspace-chat-expanded {
+ --transcript-max-h: clamp(130px, 23dvh, 190px);
+ }
+
+ body[data-device-mode="tablet"].workspace-chat-expanded #chat {
+ flex: 0 0 210px;
+ height: 210px;
+ max-height: 210px;
+ }
+
+ body[data-device-mode="tablet"]:is(.workspace-chat-side-left, .workspace-chat-side-right) {
+ --transcript-max-h: clamp(90px, 16dvh, 130px);
+ }
+
+ body[data-device-mode="tablet"]:is(.workspace-chat-side-left, .workspace-chat-side-right) #chat {
+ flex: 0 0 178px;
+ height: 178px;
+ max-height: 178px;
+ }
+
+ body[data-device-mode="tablet"].workspace-chat-minimal #chat {
+ max-height: 66px;
+ }
+
+ body[data-device-mode="tablet"].workspace-chat-minimal #chat-feed,
+ body[data-device-mode="tablet"].workspace-chat-minimal #ts,
+ body[data-device-mode="tablet"].workspace-chat-minimal #dv-voice-bar,
+ body[data-device-mode="tablet"].workspace-chat-minimal #sugs {
+ display: none !important;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact {
+ --transcript-max-h: 46px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact #chat {
+ flex: 0 0 118px;
+ height: 118px;
+ max-height: 118px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact #ts,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-compact #sugs {
+ display: none !important;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-expanded {
+ --transcript-max-h: clamp(72px, 13dvh, 112px);
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-expanded #chat {
+ flex: 0 0 172px;
+ height: 172px;
+ max-height: 172px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]):is(.workspace-chat-side-left, .workspace-chat-side-right) {
+ --transcript-max-h: clamp(58px, 11dvh, 86px);
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]):is(.workspace-chat-side-left, .workspace-chat-side-right) #chat {
+ flex: 0 0 150px;
+ height: 150px;
+ max-height: 150px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #chat {
+ max-height: 62px;
+ }
+
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #chat-feed,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #ts,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #dv-voice-bar,
+ body:is([data-device-mode="android"], [data-device-mode="mobile"]).workspace-chat-minimal #sugs {
+ display: none !important;
+ }
+
+ /* A physically narrow browser must remain recoverable even if a wider
+ workspace mode was saved on another device. */
+ @media (max-width: 600px) {
+ .top-device-switcher {
+ display: flex !important;
+ gap: 1px !important;
+ padding-inline: 1px !important;
+ margin-left: auto;
+ }
+
+ .top-device-switcher .ctab {
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ width: 25px;
+ height: 25px;
+ padding: 0;
+ }
+
+ #pp-pull-tab {
+ display: none !important;
+ }
+
+ #bframe,
+ .dm-desktop #bframe,
+ .dm-tablet #bframe,
+ .dm-android #bframe,
+ .dm-mobile #bframe {
+ align-self: stretch;
+ width: calc(100% - 4px) !important;
+ max-width: calc(100% - 4px) !important;
+ margin: 2px !important;
+ }
+ }
diff --git a/vps/daveai-project-catalog.json b/vps/daveai-project-catalog.json
new file mode 100644
index 00000000000..cd2a8dcbe50
--- /dev/null
+++ b/vps/daveai-project-catalog.json
@@ -0,0 +1,61 @@
+{
+ "generated": "2026-07-25T03:24:00Z",
+ "source": "DaveAI verified production route and project catalog",
+ "projects": [
+ { "id": "srv-daveai-home", "name": "DaveAI Core", "url": "https://daveai.tech/", "cat": "web", "status": "live", "embed": true, "description": "Main DaveAI Core platform." },
+ { "id": "srv-daveai-dev", "name": "DaveAI Dev", "url": "https://dev.daveai.tech/", "cat": "web", "status": "auth", "description": "Protected developer console with live DaveAI dependency probes." },
+ { "id": "srv-daveai-staging", "name": "DaveAI Staging", "url": "https://staging.daveai.tech/", "cat": "web", "status": "auth", "description": "Protected release gate with route checks and deployed UI fingerprinting." },
+ { "id": "srv-daveai-diy", "name": "DaveAI DIY", "url": "https://diy.daveai.tech/", "cat": "web", "status": "auth", "description": "Protected project scaffold generator with downloadable multi-file output." },
+ { "id": "srv-webui", "name": "DaveAI Web UI", "url": "https://webui.daveai.tech/", "cat": "web", "status": "live", "description": "Hosted web UI." },
+ { "id": "srv-hermes-webui", "name": "Hermes Web UI", "url": "https://hermes-webui.daveai.tech/", "cat": "web", "status": "live", "description": "Hermes web UI." },
+ { "id": "srv-hermes", "name": "Hermes", "url": "https://hermes.daveai.tech/", "cat": "apps", "status": "auth", "description": "Hermes agent surface protected by the DaveAI authentication portal." },
+ { "id": "srv-hermes3d", "name": "Hermes 3D", "url": "https://hermes3d.daveai.tech/", "cat": "apps", "status": "live", "description": "Interactive live view of Brain health, tool count, and four DaveAI agents." },
+ { "id": "srv-voice", "name": "DaveAI Voice", "url": "https://voice.daveai.tech/", "cat": "apps", "status": "live", "description": "Voice/Azure speech surface." },
+ { "id": "srv-openhands", "name": "OpenHands", "url": "https://openhands.daveai.tech/", "cat": "apps", "status": "live", "description": "OpenHands coding agent surface." },
+ { "id": "srv-fleet", "name": "DaveAI Fleet", "url": "https://fleet.daveai.tech/", "cat": "apps", "status": "auth", "description": "Protected redacted inventory of live PM2 processes and Docker containers." },
+ { "id": "srv-auth", "name": "DaveAI Auth", "url": "https://auth.daveai.tech/", "cat": "apps", "status": "live", "description": "Live Authelia/auth gateway surface. Launch card only; cross-origin frame policy blocks embedding." },
+ { "id": "srv-bolt", "name": "Bolt DIY", "url": "https://bolt.daveai.tech/", "cat": "apps", "status": "auth", "description": "Bolt.diy coding surface protected by DaveAI authentication. Launch card only." },
+ { "id": "srv-hermestv", "name": "HermesTV", "url": "https://hermestv.daveai.tech/", "cat": "apps", "status": "live", "description": "Verified live HermesTV web surface." },
+ { "id": "srv-tv", "name": "DaveAI TV", "url": "https://tv.daveai.tech/", "cat": "apps", "status": "live", "description": "Verified live TV web surface." },
+ { "id": "srv-studio", "name": "DaveAI Studio", "url": "https://daveai.tech/studio/index.html", "cat": "web", "status": "needs-proof", "embed": true, "description": "Static studio page hosted under the DaveAI web root." },
+ { "id": "srv-web-pages", "name": "DaveAI Web Pages", "url": "https://daveai.tech/web-pages.html", "cat": "web", "status": "needs-proof", "embed": true, "description": "Hosted web pages index discovered in the DaveAI web root." },
+ { "id": "srv-windsurf", "name": "Windsurf", "url": "https://daveai.tech/windsurf.html", "cat": "web", "status": "needs-proof", "embed": true, "description": "Hosted Windsurf page discovered in the DaveAI web root." },
+ { "id": "srv-voice-landing", "name": "Voice Landing", "url": "https://daveai.tech/voice-landing.html", "cat": "web", "status": "needs-proof", "embed": true, "description": "Voice landing page hosted under the DaveAI web root." },
+ { "id": "srv-landing-v1", "name": "Landing V1", "url": "https://daveai.tech/index-landing-v1.html", "cat": "web", "status": "historical", "embed": true, "description": "Historical landing-page variant kept launchable for review." },
+ { "id": "srv-games-portal", "name": "DaveAI Game Portal", "url": "https://game.daveai.tech/", "cat": "games", "status": "auth", "description": "Game portal subdomain protected by the DaveAI authentication portal." },
+ { "id": "srv-siege-td", "name": "Daves Siege TD", "url": "https://daveai.tech/games/daves-siege-td/index.html", "cat": "games", "status": "live", "embed": true, "description": "Hosted tower-defense game." },
+ { "id": "srv-td2", "name": "DaveAI TD2", "url": "https://daveai.tech/games/daveai-td2/index.html", "cat": "games", "status": "live", "embed": true, "description": "Hosted tower-defense game." },
+ { "id": "srv-asteroids", "name": "DaveAI Asteroids", "url": "https://daveai.tech/games/asteroids/index.html", "cat": "games", "status": "live", "embed": true, "description": "Hosted arcade game." },
+ { "id": "srv-arcade", "name": "DaveAI Arcade", "url": "https://daveai.tech/arcade/index.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Arcade hub discovered from /var/www/agentic-website/arcade." },
+ { "id": "srv-arcade-snake", "name": "Arcade Snake", "url": "https://daveai.tech/arcade/snake.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Static arcade Snake game." },
+ { "id": "srv-arcade-pacman", "name": "Arcade Pacman", "url": "https://daveai.tech/arcade/pacman.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Static arcade Pacman game." },
+ { "id": "srv-arcade-tetris", "name": "Arcade Tetris", "url": "https://daveai.tech/arcade/tetris.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Static arcade Tetris game." },
+ { "id": "srv-arcade-towerdefense", "name": "Arcade Tower Defense", "url": "https://daveai.tech/arcade/towerdefense.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Static arcade tower-defense game." },
+ { "id": "srv-game-snake", "name": "Snake", "url": "https://daveai.tech/snake.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Snake game." },
+ { "id": "srv-game-pacman", "name": "Pacman", "url": "https://daveai.tech/pacman.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Pacman game." },
+ { "id": "srv-game-tictactoe", "name": "Tic Tac Toe", "url": "https://daveai.tech/tictactoe.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Tic Tac Toe game." },
+ { "id": "srv-game-tictactoe-ai", "name": "AI Tic Tac Toe", "url": "https://daveai.tech/tic-tac-toe-ai.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "AI Tic Tac Toe page generated by DaveAI agents." },
+ { "id": "srv-game-chess", "name": "Chess", "url": "https://daveai.tech/chess.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Chess page." },
+ { "id": "srv-game-checkers", "name": "Checkers", "url": "https://daveai.tech/checkers.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Checkers page." },
+ { "id": "srv-game-breakout", "name": "Breakout", "url": "https://daveai.tech/breakout.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Breakout game." },
+ { "id": "srv-game-memory", "name": "Memory", "url": "https://daveai.tech/memory.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Root hosted Memory game." },
+ { "id": "srv-game-wheel-hangman", "name": "Wheel Hangman", "url": "https://daveai.tech/wheel-hangman.html", "cat": "games", "status": "needs-proof", "embed": true, "description": "Wheel/Hangman game page. Needs gameplay proof before accepted." },
+ { "id": "srv-game-checkers-crowning", "name": "Checkers Crowning Draft", "url": "https://daveai.tech/checkers-game-crowning-jumping-do-not.html", "cat": "games", "status": "needs-fix", "embed": true, "description": "Agent-created page with mismatched checkers/hangman history; kept visible but not accepted." },
+ { "id": "srv-game-gangster-wars", "name": "Gangster Wars", "url": "https://game.daveai.tech/gangster-wars/", "cat": "games", "status": "auth", "description": "Game subdomain project protected by the DaveAI authentication portal." },
+ { "id": "srv-streamhub", "name": "StreamHub IPTV", "url": "https://iptv.daveai.tech/", "cat": "apps", "status": "partial-accepted", "description": "StreamHub IPTV app. Provider-vault catalogs, playback, library and playlists are partially accepted; broader voice/audio/provider sweep is pending." },
+ { "id": "srv-apps-launcher", "name": "Apps DaveAI Launcher", "url": "https://apps.daveai.tech/", "cat": "apps", "status": "live", "description": "DaveTV/Apps launcher and authentication surface." },
+ { "id": "srv-iptv-zero", "name": "IPTV Player Zero", "url": "https://apps.daveai.tech/iptv-player-zero/", "cat": "apps", "status": "provider-ready", "description": "Apps fleet IPTV player. Needs app-specific proof before E2E accepted." },
+ { "id": "srv-xstream", "name": "XStream Player", "url": "https://xstream-player.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault XStream player." },
+ { "id": "srv-smart-iptv", "name": "Smart IPTV Web", "url": "https://smart-iptv-web.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Smart IPTV Web player." },
+ { "id": "srv-iptvnator", "name": "IPTVnator", "url": "https://iptvnator.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "IPTVnator provider-vault player." },
+ { "id": "srv-stalker", "name": "Stalker UI", "url": "https://stalker-ui.daveai.tech/", "cat": "apps", "status": "needs-proof", "description": "Stalker UI. Cards/playback proof is still pending." },
+ { "id": "srv-extreme-infinitv", "name": "Extreme InfiniTV", "url": "https://extreme-infinitv.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault IPTV app." },
+ { "id": "srv-nuvio", "name": "NuvioWeb", "url": "https://nuvio.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault Stremio-style app." },
+ { "id": "srv-wizju", "name": "Wizju IPTV Player", "url": "https://wizju-iptv-player.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault IPTV app." },
+ { "id": "srv-iptv-stream", "name": "IPTV Stream", "url": "https://iptv-stream.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault live TV app." },
+ { "id": "srv-iptv-restream", "name": "IPTV Restream", "url": "https://iptv-restream.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault restream app." },
+ { "id": "srv-open-tv", "name": "Open TV", "url": "https://open-tv.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault TV app." },
+ { "id": "srv-ynotv", "name": "ynotv", "url": "https://ynotv.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault TV app." },
+ { "id": "srv-tvapp", "name": "TVapp", "url": "https://tvapp.daveai.tech/", "cat": "apps", "status": "provider-ready", "description": "Provider-vault TV app." }
+ ]
+}
diff --git a/vps/daveai-sites-config.json b/vps/daveai-sites-config.json
new file mode 100644
index 00000000000..b4f2d9df3fa
--- /dev/null
+++ b/vps/daveai-sites-config.json
@@ -0,0 +1,257 @@
+{
+ "sites": [
+ {
+ "id": "daveai-main",
+ "name": "DaveAI",
+ "domain": "daveai.tech",
+ "url": "https://daveai.tech",
+ "port": 3001,
+ "status": "live",
+ "category": "core",
+ "icon": "🌐",
+ "description": "Main DaveAI website"
+ },
+ {
+ "id": "daveai-auth",
+ "name": "Auth",
+ "domain": "auth.daveai.tech",
+ "url": "https://auth.daveai.tech",
+ "port": 8888,
+ "status": "live",
+ "category": "core",
+ "icon": "🔐",
+ "description": "Authelia SSO authentication"
+ },
+ {
+ "id": "daveai-api",
+ "name": "API",
+ "domain": "api.daveai.tech",
+ "url": "https://api.daveai.tech/health",
+ "port": 8888,
+ "status": "live",
+ "category": "core",
+ "icon": "⚡",
+ "description": "Agent Brain API gateway"
+ },
+ {
+ "id": "daveai-voice",
+ "name": "Voice",
+ "domain": "voice.daveai.tech",
+ "url": "https://voice.daveai.tech",
+ "port": 5050,
+ "status": "live",
+ "category": "ai",
+ "icon": "🎤",
+ "description": "Edge-TTS voice synthesis"
+ },
+ {
+ "id": "daveai-ai",
+ "name": "AI",
+ "domain": "ai.daveai.tech",
+ "url": "https://ai.daveai.tech",
+ "port": 4000,
+ "status": "live",
+ "category": "ai",
+ "icon": "🤖",
+ "description": "LiteLLM proxy for all models"
+ },
+ {
+ "id": "daveai-llm",
+ "name": "LLM",
+ "domain": "llm.daveai.tech",
+ "url": "https://llm.daveai.tech",
+ "port": 4000,
+ "status": "live",
+ "category": "ai",
+ "icon": "🧠",
+ "description": "LLM model serving endpoint"
+ },
+ {
+ "id": "daveai-openhands",
+ "name": "OpenHands",
+ "domain": "openhands.daveai.tech",
+ "url": "https://openhands.daveai.tech",
+ "port": 3333,
+ "status": "live",
+ "category": "ai",
+ "icon": "🖐️",
+ "description": "AI agent IDE"
+ },
+ {
+ "id": "daveai-diy",
+ "name": "Bolt.DIY",
+ "domain": "diy.daveai.tech",
+ "url": "https://diy.daveai.tech",
+ "port": 3300,
+ "status": "auth",
+ "category": "dev",
+ "icon": "🔧",
+ "description": "Protected project scaffold generator"
+ },
+ {
+ "id": "daveai-dev",
+ "name": "Dev",
+ "domain": "dev.daveai.tech",
+ "url": "https://dev.daveai.tech",
+ "port": 3398,
+ "status": "auth",
+ "category": "dev",
+ "icon": "💻",
+ "description": "Protected live dependency-health console"
+ },
+ {
+ "id": "daveai-staging",
+ "name": "Staging",
+ "domain": "staging.daveai.tech",
+ "url": "https://staging.daveai.tech",
+ "port": 3399,
+ "status": "auth",
+ "category": "dev",
+ "icon": "🧪",
+ "description": "Protected production release-readiness gate"
+ },
+ {
+ "id": "daveai-docs",
+ "name": "Docs",
+ "domain": "docs.daveai.tech",
+ "url": "https://docs.daveai.tech",
+ "port": 3100,
+ "status": "live",
+ "category": "docs",
+ "icon": "📚",
+ "description": "Documentation site"
+ },
+ {
+ "id": "daveai-git",
+ "name": "Git",
+ "domain": "git.daveai.tech",
+ "url": "https://git.daveai.tech",
+ "port": 3301,
+ "status": "auth",
+ "category": "dev",
+ "icon": "📦",
+ "description": "Protected persistent Gitea repository service"
+ },
+ {
+ "id": "daveai-db",
+ "name": "Database",
+ "domain": "db.daveai.tech",
+ "url": "https://db.daveai.tech",
+ "port": 8080,
+ "status": "auth",
+ "category": "infra",
+ "icon": "🗄️",
+ "description": "Protected Adminer console for the Bolt PostgreSQL service"
+ },
+ {
+ "id": "daveai-monitor",
+ "name": "Monitor",
+ "domain": "monitor.daveai.tech",
+ "url": "https://monitor.daveai.tech",
+ "port": 3030,
+ "status": "auth",
+ "category": "infra",
+ "icon": "📊",
+ "description": "Protected Grafana dashboard backed by Prometheus host metrics"
+ },
+ {
+ "id": "daveai-hermestv",
+ "name": "HermesTV",
+ "domain": "hermestv.daveai.tech",
+ "url": "https://hermestv.daveai.tech",
+ "port": 3080,
+ "status": "live",
+ "category": "media",
+ "icon": "📺",
+ "description": "TV streaming interface"
+ },
+ {
+ "id": "daveai-iptv",
+ "name": "IPTV",
+ "domain": "iptv.daveai.tech",
+ "url": "https://iptv.daveai.tech",
+ "port": 3103,
+ "status": "live",
+ "category": "media",
+ "icon": "📡",
+ "description": "IPTV restreaming"
+ },
+ {
+ "id": "daveai-hermes",
+ "name": "Hermes",
+ "domain": "hermes-webui.daveai.tech",
+ "url": "https://hermes-webui.daveai.tech",
+ "port": 8787,
+ "status": "live",
+ "category": "bots",
+ "icon": "🤖",
+ "description": "Discord bots WebUI"
+ },
+ {
+ "id": "daveai-apps",
+ "name": "Apps",
+ "domain": "apps.daveai.tech",
+ "url": "https://apps.daveai.tech",
+ "port": 3200,
+ "status": "live",
+ "category": "apps",
+ "icon": "📱",
+ "description": "Apps portal (DaveTV)"
+ },
+ {
+ "id": "daveai-fleet",
+ "name": "Fleet",
+ "domain": "fleet.daveai.tech",
+ "url": "https://fleet.daveai.tech",
+ "port": 3400,
+ "status": "auth",
+ "category": "infra",
+ "icon": "🚀",
+ "description": "Protected redacted PM2 and Docker runtime inventory"
+ },
+ {
+ "id": "daveai-ws",
+ "name": "WebSocket",
+ "domain": "ws.daveai.tech",
+ "url": "https://ws.daveai.tech",
+ "port": 3401,
+ "status": "live",
+ "category": "infra",
+ "icon": "🔌",
+ "description": "Public secure WebSocket echo and test console"
+ },
+ {
+ "id": "daveai-game",
+ "name": "Game",
+ "domain": "game.daveai.tech",
+ "url": "https://game.daveai.tech",
+ "port": 3500,
+ "status": "auth",
+ "category": "game",
+ "icon": "🎮",
+ "description": "Game server (coming soon)"
+ },
+ {
+ "id": "daveai-3d",
+ "name": "3D",
+ "domain": "hermes3d.daveai.tech",
+ "url": "https://hermes3d.daveai.tech",
+ "port": 3600,
+ "status": "live",
+ "category": "apps",
+ "icon": "🧊",
+ "description": "Interactive live DaveAI Brain and agent graph"
+ }
+ ],
+ "categories": {
+ "core": { "name": "Core", "color": "#00ff88" },
+ "ai": { "name": "AI/ML", "color": "#ff6b6b" },
+ "dev": { "name": "Development", "color": "#4ecdc4" },
+ "infra": { "name": "Infrastructure", "color": "#ffe66d" },
+ "media": { "name": "Media", "color": "#ff9f43" },
+ "bots": { "name": "Bots", "color": "#a29bfe" },
+ "apps": { "name": "Apps", "color": "#fd79a8" },
+ "game": { "name": "Gaming", "color": "#e17055" },
+ "future": { "name": "Future", "color": "#b2bec3" }
+ }
+}
diff --git a/vps/daveai-ui-v6.html b/vps/daveai-ui-v6.html
new file mode 100644
index 00000000000..bb23f5bf9c4
--- /dev/null
+++ b/vps/daveai-ui-v6.html
@@ -0,0 +1,12920 @@
+
+
+
+
+
+
+ DaveAI — Agentic Web Builder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ App Mode
+
+
+
+
+
+
+
+
+ Chat
+
+
+
+
+
+
+
+
+
Welcome to DaveAI
+
Choose your device to enter
+
+
+
+
Desktop
+
1280px+
+
RECOMMENDED
+
+
+
+
Tablet
+
768px
+
RECOMMENDED
+
+
+
+
Mobile
+
412px
+
RECOMMENDED
+
+
+
+
+ Click for Audio
+
+
+
+
+
+
+ 80%
+
+
+
Skip Intro
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
Intro Video
+
Choose which intro plays when you load or refresh the page. New videos can be added over
+ time.
+
+
Random
+
Male
+
Female
+
Off
+
+
Saved
+
+
+
+
Intro Volume
+
+
+ 60%
+
+
+
+
+
Display
+
+
+ Show file extensions
+ Display .html, .zip, etc. in project & file names
+
+
+
+
+
+
Admin
+
+
Saved
+
+
+
+ Auto-immersive on project load
+ Maximize preview when opening a project (game, app, web, other)
+
+
+
+
+
+
+
Saved
+
+
+
+ Edge-hover panels
+ Hover far left/right/top/bottom edges to reveal hidden UI in immersive mode
+
+
+
+
+
+
+
Saved
+
+
+
+ Show Thinking…
+ Show DaveAI's internal step events (e.g. 💭 Thinking…) inside the chat
+ bubble. Off by default — activity panel always logs them.
+
+
+
+
+
+
+
Saved
+
+
+
+
+ Showcase Demo
+ Show featured project (game/app) in the preview window on load. Auto-minimizes when you start
+ typing.
+
+
+
+
+
+
+
Saved
+
+
+
+
+
+
+ DaveAI Voice
+ Enable voice narration — agents speak their responses aloud
+
+
+
+
+
+
+
+ Open Voice Studio
+
+
+
+
+
Demo / Showcase Admin
+
+
Set a project to showcase in the Action Window by default. Visitors see the demo with
+ Play/Create buttons.
+
+
Select project to showcase
+
+ — No showcase (disabled) —
+
+
Custom title (optional)
+
+
+
+
Custom subtitle (optional)
+
+
+
+
+ Apply Showcase
+ Clear Demo
+
+
+
Saved
+
+
+
+
+
+ Waveform & Colors
+ Customize voice waveform style and colors for AI (female) and Human (male) voices
+
+
+
+
AI Voice Color (Female — default Hot Pink)
+
+
+
+ Custom color picker
+
+
+
+
+
Human Voice Color (Male — default Neon Blue)
+
+
+
+ Custom color picker
+
+
+
+
+
Waveform Visualization Type
+
+
+
+
+
+
+
+ Visual Effects & Transitions
+ Enable hover effects, particles, flames, and other visual artifacts on UI elements
+
+
+
+
+ Hover Lift & Glow
+ Buttons, cards, and panels lift with glow on hover
+
+
+
+
+
+
+
+
+
+ Card Shimmer
+ Light shimmer sweeps across cards and panels on hover
+
+
+
+
+
+
+
+
+
+ Click Particles
+ Burst of particles when clicking buttons and interactive elements
+
+
+
+
+
+
+
+
+
+ Flame Effects
+ Animated flames on active/highlighted elements (performance-intensive)
+
+
+
+
+
+
+
+
+
+ Static / Glitch
+ CRT static and glitch overlay on hover — retro AI aesthetic
+
+
+
+
+
+
+
+
+
+ Neon Text Glow
+ Labels and headings glow with neon effect on hover
+
+
+
+
+
+
+
+
+
+ Panel Transitions
+ Smooth slide/fade/scale animations when panels open/close
+
+
+
+
+
+
+
+
+
+ Electric Spark Borders
+ Rotating conic gradient spark around focused elements
+
+
+
+
+
+
+
+
+
+
+
+ Icon & Theme Studio
+ Load icon packs, change themes. Auto-backup every 10 min. Safe mode if icons break.
+
+
+
+
+
+
Auto-backup: Active
+
Last: never
+
+
+ Backup Now
+
+ Restore
+
+
+
+
+ Safe Mode
+ If an icon pack causes issues, auto-revert to stock icons within 30 seconds
+
+
+
+
+
+
+
+
+
+ Auto-Backup (10 min)
+ Automatically save working theme/icon state every 10 minutes
+
+
+
+
+
+
+
+
Installed Icon Packs
+
+
+
+
+ Import Icon Pack
+
+ Browse Packs
+
+
+
+
+
+
+ DaveAI Personality & Memory
+ Configure how DaveAI talks, remembers, and entertains
+
+
+
+
Personality Style
+
+ 🎭 Witty & Sassy (default)
+ 💼 Professional
+ 😂 Full Comedian
+ 📖 Storyteller
+ 🎵 Poet & Lyricist
+ 😏 Flirty & Playful
+ ✏️ Custom
+
+
+
Humor Level
+
+
+ 70%
+
+
+
+
+ 18+ Jokes Allowed
+ Include adult humor when appropriate (admin-controlled)
+
+
+
+
+
+
+
+
+
+ Long Stories & Poems
+ DaveAI can tell long stories, recite poems, or go on entertaining tangents
+
+
+
+
+
+
+
+
+
+ Conversation Memory
+ Remember past conversations, preferences, and context across sessions
+
+
+
+
+
+
+
+
Memory Size (conversations stored)
+
+
+ 100
+
+
+
+
+ Clear Memory
+
+ Undo Clear
+
+ Export
+
+
+
Custom Personality Prompt (optional)
+
+
+
+
+
+
+
+ Auto-Refresh Projects
+ Periodically scan for new, updated, or changed projects across all categories
+
+
+
+ Enable Auto-Refresh
+ Checks every 2 hours for changes in games, apps, web, and other categories
+
+
+
+
+
+
+
+ Interval
+
+ 30 min
+ 1 hour
+ 2 hours
+ 4 hours
+ 8 hours
+
+
+ Refresh Now
+ —
+
+
+
+
+
+
+ Local LLM (LM Studio)
+ Admin
+ Use your local RTX 3090 Ti to power DaveAI stories, voice, and chat via LM Studio
+
+
+
+
+ Enable Local LLM
+ Route DaveAI chat through LM Studio instead of cloud API
+
+
+
+
+
+
+
+
+
+ Auto-load LM Studio Models
+ Automatically select best available model (14B+ with 80k+ context)
+
+
+
+
+
+
+
+
+
LM Studio URL
+
+
+
Chat / Story Model
+
+ — Auto-detect from LM Studio —
+
+ Qwen3-30B-A3B-2507 ⭐ BEST — Jul 2025, 262K ctx, instant
+ Qwen3-30B-A3B — Original MOE, thinking+non-thinking
+ Qwen3-Coder-30B-A3B — MOE coding specialist
+
+
+ DeepSeek-R1-Distill-Qwen-14B (Q6) — Best
+ reasoning
+ Qwen3-14B — Dense, smart + fast
+ Mistral Small 3.1 24B — Great
+ storyteller
+ QwQ-32B (Q4) — Deep reasoning
+ GLM-4 32B — Large creative
+
+
+ DeepSeek
+ MOE 4x8B 24B — Uncensored thinker
+ Dark
+ Champion MOE 18B — Abliterated
+ Dirty Shirley Writer 9B —
+ Uncensored stories
+ Reka Flash 21B —
+ Uncensored reasoning
+ Dolphin 3.0 8B — Uncensored helpful
+
+ Qwen3-8B Jailbroken — No filters
+ Stheno 8B — RP/Creative
+
+
+ Qwen2.5 Coder 32B — Best local coder
+ Devstral Small — Dev assistant
+ Viper Coder 7B — Fast coding
+ OlympicCoder 7B — Competition coder
+
+
+ Phi-4 Reasoning Plus — Microsoft
+ DeepSeek R1-0528 Qwen3 8B — Latest
+
+ DeepSeek R1 Qwen 7B — Fast reasoning
+
+
+
+ Qwen2.5-VL 7B — Best vision
+ Llama 3.2 11B Vision — Meta
+ Pixtral 12B — Mistral vision
+
+
+
+
+
Auto Model Switch
+
Automatically swap between story and coding
+ models. Uses MOE for instant loads.
+
+
+
+ Enable Auto-Switch
+ Load story model for chat, coding model when idle
+
+
+
+
+
+
+
+
Coding Fallback Model (loads when idle)
+
+ Qwen3-Coder-30B-A3B (MOE) — instant, best coder
+ Qwen3-30B-A3B-2507 (MOE) — general purpose
+ Qwen2.5 Coder 32B — dense, powerful
+ GLM-4 32B — creative + code
+ Devstral Small — lightweight dev
+
+
+
Idle Timeout (switch to coding after)
+
+
+ 2 min
+
+
—
+
+
+ Download Qwen3-30B-A3B-2507 (SOTA) in LM
+ Studio
+
+
Local Voice Engine
+
+ None (DaveAI uses Azure Neural voices only)
+
+
+
+
+
+ DaveAI voice output is locked to Azure English Neural voices. Use Voice Studio to select Maisie or another
+ approved Azure voice.
+
+
+ Open Azure Voice Studio
+
Azure Neural only
+
+
+
Temperature
+
+
+ 0.8
+
+
+
Max Tokens
+
+
+ 2048
+
+
+
+
+ Test Connection
+
+ List Models
+
+
Not connected
+
+
+
+
+
+
+
+
+
+
+ DaveAI Chat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Voices
+
Tuning
+
Presets
+
Agents
+
Config
+
+
+
+
+
+
Filter Voices
+
+ All
+ UK
+ US
+ Female
+ Male
+
+
+
+
+
+
Preview
+
+
+
+ Test
+
+
+
+
+
+
+
Voice Parameters
+
+
+
+
+
Volume / Gain 100%
+
+
+
+
+
+
Clarity / Presence 50%
+
+
+
+
+
+
+
Voice Blend (Mix two voices)
+
+ A
+
+
+
+
+ B
+
+
+
+
Blend creates a custom voice by mixing two voices.
+ e.g. Maisie(2)+Ryan(1) = 67%/33%
+
+
+
Live Preview
+
+
+ Test
+
+
+
+
+
+
+
Built-in Presets
+
+
+
+
+
Your Saved Presets
+
+
No saved presets yet. Configure a voice and click
+ Save.
+
+
+
Import / Share
+
+ Export Current
+ Import JSON
+ Copy Share Link
+
+
+
+
+
+
Agent Voice Assignment
+
Assign different voices to each agent. DaveAI
+ Voice narrates agent activity, code changes, and build status in real-time.
+
+
+
+
+
+
+
+
Narration Events
+
+
+ Read agent chat responses
+ aloud
+
+
+ Announce build start / complete
+
+
+ Announce errors and warnings
+
+
+ Narrate activity feed events
+
+
+ Describe code changes being made
+
+
+
+
+
+
+
Azure Speech Engine
+
+ DaveAI voice is restricted to the approved English Azure Neural catalog. Keys stay server-side; all legacy
+ voice providers are disabled.
+
+
+
Engine
+
+ Azure Speech Neural (DaveAI only - Maisie UK default)
+
+
+
+
+
Output Format
+
+ MP3 24kHz Azure Neural
+
+
+
+
+
Guard Rails
+
+
Rate Limit (requests/min) 10
+
+
+
+
Timeout (seconds) 12
+
+
+
+
+
Max Text Length 800
+
+
+
+
+
+ Cache repeated Azure phrases (5min TTL)
+
+
+ Queue mode (don't interrupt current speech)
+
+
+
+
+
Moshi Instant Voice
+
+ Enable real-time voice conversation with Moshi. Switches from TTS playback to instant voice mode.
+
+
+
+
+
+
+ Enable Moshi Instant Voice
+
+
+ Connecting to Moshi
+ bridge...
+
+
+
+
+
System Diagnostics
+
+
Open Voice Studio to run diagnostics...
+
+
Re-run Diagnostics
+
+
+
Shortcuts
+
+ Ctrl+Shift+V — Open Voice Studio
+ Ctrl+Shift+M — Toggle voice mute
+ Ctrl+Shift+T — Test current voice
+
+
+
+
+
+
+
+
+
+
+
+
+
D
+
DaveAI
+
Agentic Web Builder — sign in to continue
+
+
+
+
+
Display Name
+
Email
+
Password
+
Confirm Password
+
Sign In
+
+
+
+
+
+
+
+
+
+
+
Choose Your Workspace Layout
+
Each preset changes the pane arrangement, working density, and colour system.
+
+
+
+
Balanced canvas, chat, tools, and Activity.
+
Cancel
+
Apply Horizons
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LIVE
+ ◌ ...
+
+
+
+ connecting…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pages
+
+
+
+
Projects
+
Pages
+
Sites
+
Tools
+
Agents
+
DB
+
Skills
+
+
+
+
+
+ Web 0
+ Apps
+ 0
+ Games
+ 0
+ Other
+ 0
+
+
+
+
+ Resume
+ Sync
+ Export
+
+
+
+ Add project
+
+
+
+
+
+
+
+
Loading deployed pages…
+
+
+
+ New page
+
+
+
+
+
+
+
+
+
+ All
+ Supervisor
+ Coder
+ QA
+ Asset
+
+
+ Runtime registry loading…
+
+
+
+
+
+
+
+
+
Supervisor connecting… —
+
+
+
+
+
+
+
+ Session
+
+
Brain: connecting...
+
Model: LiteLLM routed
+
Tools: loading runtime registry…
+
+
+
+
+
+
+
+ PostgreSQL · daveai
+
+
+
+ Quick Query
+
+
▶
+ Run Query
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Preview
+
Code
+
+
+
Desktop
+
Tablet
+
Android
+
Mobile
+
+
daveai.tech
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Waiting for build — start a conversation to preview your project
+
+
+
+
+
Game
+
Dave's Siege TD
+
Experience our latest tower defense game — built entirely by DaveAI
+ agents. Defend your castle with strategic tower placement!
+
+ Play Now
+ Start
+ Creating
+
+
+ by DaveAI Live Games
+
+
+
+ Admin
+ Edit
+ Change
+ Clear
+ Fullscreen
+
+
+
+
+
+
+
+
+ Playing
+
+
+
+
+
+
+
+
+
+ DaveAI speaking…
+
+
+
+
+
+
+
Idle
+
+
+
+
+
+
+
Loading build history…
+
+
+
+
+
+
+
+
+
Total Pages
+
5
+
2 published
+
+
+
Recent Builds
+
12
+
total logged
+
+
+
Commits
+
34
+
this week
+
+
+
API Status
+
—
+
checking
+
+
+
Events
+
1.2k
+
tracked
+
+
+
Registered Users
+
84k
+
accounts
+
+
+
+
+
+
+
+
+
+ Config
+ Users
+ Agents
+ Tools
+ Logs
+
+
+
Brain URL https://daveai.tech/api
+
LiteLLM :4000 (local)
+
ZeroClaw :3000 (gateway)
+
DB PostgreSQL (daveai)
+
Model LiteLLM routed
+
Tools loading…
+
VPS 187.77.30.206
+
+
+
+ Registered Users
+ +
+ New User
+
+
+
+
+
+
Supervisor s-think · analyzing
+
Coder s-work · building UI
+
QA s-idle
+
Asset s-idle
+
+
+
+
+
+ agent-brain
+ litellm
+ agentic-ui
+
+ Waiting…
+
+
+ Loading logs…
+
+
+
+
+
+
+
+
+
+
Chat Mode
+
DaveAI is ready in the dock below. Replies, memory, voice, and agent activity stay
+ live while this action surface remains focused on the conversation.
+
+ Clean final replies in chat
+ Activity steps stay in the right panel
+ Azure Neural voice reads final answers only
+
+
+
+ Start in chat
+
+
Prepares the chat composer without sending.
+
+
+
+
+
+
+
+
+
Browser Mode
+
Browser Mode sends pages to DaveAI's web tools for reading,
+ screenshots, and step-by-step follow-up inside this action window.
+
+
+ Go
+
+
+ Browse, read, and summarize any public page
+ Click + form-fill requests route through DaveAI's approved web tools
+ Capture screenshots into right-panel Activity
+
+
View current roadmap
+
+
+
+
+
+
+
+
+
Code Agent Mode
+
Code Agent mode keeps plans, file review, test output,
+ and deploy notes organized while DaveAI works.
+
+ Plan + assumptions panel (left)
+ File tree with inline diff (center)
+ Test output + deploy chip (right)
+
+
View current state
+
+
+
+
+
+
+
+
+
Research Mode
+
Research Agent mode keeps sources, citations,
+ and reading notes organized while DaveAI investigates.
+
+
+ Web + primary docs
+ Primary docs only
+ DaveAI workspace
+
+
+ Concise citations
+ Citation rich
+ Short source list
+
+ Prepare research
+
+
+
The prepared request stays editable in chat before sending.
+
+ Choose live-web, primary-doc, or workspace evidence
+ Choose the citation detail before sending
+ Sources and tool activity remain visible in the right panel
+
+
View proof index
+
+
+
+
+
+
+
+
+
Problem Solver Mode
+
Problem Solver mode turns the request into assumptions,
+ a step-by-step plan, and test results as DaveAI works.
+
+
+ Prepare diagnosis
+
+
Builds a diagnosis request with assumptions and proof requirements.
+
+ Problem statement and assumption list
+ Step-by-step plan with test results
+ Re-run tests on demand
+
+
View completion backlog
+
+
+
+
+
+
+
+
+
Hermes Team Mode
+
Hermes Team mode shows delegated work, worker status,
+ and approval gates from DaveAI's Hermes agents.
+
+
+ Prepare delegation
+
+
Current Brain status is shown in the global status bar; delegated writes remain approval-gated.
+
+ Supervisor routes work to the appropriate DaveAI agents
+ Agent state and traces appear in Activity
+ Approval gates remain attached to delegated writes
+
+
View file matrix
+
+
+
+
+
+
+
+
+
Select a saved project
+
+
File
+
Folder
+
+
+
+
+
+
+
+
Select a project file to edit.
+
+
+
+
+
+
+
+
+
+
+
+
+
DaveAI Chat
+
+
+
+
+
+
+
+
+
+
Supervisor: I'm having @Coder update the hero section. @QA will
+ verify contrast ratios.
+
+
+
+
+
+
💬 Chat
+
+
Azure Neural
+
+
Maisie
+
+
+
Auto
+
+ Silent
+
Text
+
+
type /voice to switch
+
+
+
+
+
+
hero-title
+
+
+
+
+
+
+
+
+
Make button purple gradient
+
Add dark mode toggle
+
Check mobile layout
+
Generate logo
+
Add pricing section
+
Run lighthouse audit
+
Screenshot the site
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vps/patches/agent-runtime-completion-20260724-src/daveai-ui-v6.html b/vps/patches/agent-runtime-completion-20260724-src/daveai-ui-v6.html
new file mode 100644
index 00000000000..bb23f5bf9c4
--- /dev/null
+++ b/vps/patches/agent-runtime-completion-20260724-src/daveai-ui-v6.html
@@ -0,0 +1,12920 @@
+
+
+
+
+
+
+ DaveAI — Agentic Web Builder
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ App Mode
+
+
+
+
+
+
+
+
+ Chat
+
+
+
+
+
+
+
+
+
Welcome to DaveAI
+
Choose your device to enter
+
+
+
+
Desktop
+
1280px+
+
RECOMMENDED
+
+
+
+
Tablet
+
768px
+
RECOMMENDED
+
+
+
+
Mobile
+
412px
+
RECOMMENDED
+
+
+
+
+ Click for Audio
+
+
+
+
+
+
+ 80%
+
+
+
Skip Intro
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+
Intro Video
+
Choose which intro plays when you load or refresh the page. New videos can be added over
+ time.
+
+
Random
+
Male
+
Female
+
Off
+
+
Saved
+
+
+
+
Intro Volume
+
+
+ 60%
+
+
+
+
+
Display
+
+
+ Show file extensions
+ Display .html, .zip, etc. in project & file names
+
+
+
+
+
+
Admin
+
+
Saved
+
+
+
+ Auto-immersive on project load
+ Maximize preview when opening a project (game, app, web, other)
+
+
+
+
+
+
+
Saved
+
+
+
+ Edge-hover panels
+ Hover far left/right/top/bottom edges to reveal hidden UI in immersive mode
+
+
+
+
+
+
+
Saved
+
+
+
+ Show Thinking…
+ Show DaveAI's internal step events (e.g. 💭 Thinking…) inside the chat
+ bubble. Off by default — activity panel always logs them.
+
+
+
+
+
+
+
Saved
+
+
+
+
+ Showcase Demo
+ Show featured project (game/app) in the preview window on load. Auto-minimizes when you start
+ typing.
+
+
+
+
+
+
+
Saved
+
+
+
+
+
+
+ DaveAI Voice
+ Enable voice narration — agents speak their responses aloud
+
+
+
+
+
+
+
+ Open Voice Studio
+
+
+
+
+
Demo / Showcase Admin
+
+
Set a project to showcase in the Action Window by default. Visitors see the demo with
+ Play/Create buttons.
+
+
Select project to showcase
+
+ — No showcase (disabled) —
+
+
Custom title (optional)
+
+
+
+
Custom subtitle (optional)
+
+
+
+
+ Apply Showcase
+ Clear Demo
+
+
+
Saved
+
+
+
+
+
+ Waveform & Colors
+ Customize voice waveform style and colors for AI (female) and Human (male) voices
+
+
+
+
AI Voice Color (Female — default Hot Pink)
+
+
+
+ Custom color picker
+
+
+
+
+
Human Voice Color (Male — default Neon Blue)
+
+
+
+ Custom color picker
+
+
+
+
+
Waveform Visualization Type
+
+
+
+
+
+
+
+ Visual Effects & Transitions
+ Enable hover effects, particles, flames, and other visual artifacts on UI elements
+
+
+
+
+ Hover Lift & Glow
+ Buttons, cards, and panels lift with glow on hover
+
+
+
+
+
+
+
+
+
+ Card Shimmer
+ Light shimmer sweeps across cards and panels on hover
+
+
+
+
+
+
+
+
+
+ Click Particles
+ Burst of particles when clicking buttons and interactive elements
+
+
+
+
+
+
+
+
+
+ Flame Effects
+ Animated flames on active/highlighted elements (performance-intensive)
+
+
+
+
+
+
+
+
+
+ Static / Glitch
+ CRT static and glitch overlay on hover — retro AI aesthetic
+
+
+
+
+
+
+
+
+
+ Neon Text Glow
+ Labels and headings glow with neon effect on hover
+
+
+
+
+
+
+
+
+
+ Panel Transitions
+ Smooth slide/fade/scale animations when panels open/close
+
+
+
+
+
+
+
+
+
+ Electric Spark Borders
+ Rotating conic gradient spark around focused elements
+
+
+
+
+
+
+
+
+
+
+
+ Icon & Theme Studio
+ Load icon packs, change themes. Auto-backup every 10 min. Safe mode if icons break.
+
+
+
+
+
+
Auto-backup: Active
+
Last: never
+
+
+ Backup Now
+
+ Restore
+
+
+
+
+ Safe Mode
+ If an icon pack causes issues, auto-revert to stock icons within 30 seconds
+
+
+
+
+
+
+
+
+
+ Auto-Backup (10 min)
+ Automatically save working theme/icon state every 10 minutes
+
+
+
+
+
+
+
+
Installed Icon Packs
+
+
+
+
+ Import Icon Pack
+
+ Browse Packs
+
+
+
+
+
+
+ DaveAI Personality & Memory
+ Configure how DaveAI talks, remembers, and entertains
+
+
+
+
Personality Style
+
+ 🎭 Witty & Sassy (default)
+ 💼 Professional
+ 😂 Full Comedian
+ 📖 Storyteller
+ 🎵 Poet & Lyricist
+ 😏 Flirty & Playful
+ ✏️ Custom
+
+
+
Humor Level
+
+
+ 70%
+
+
+
+
+ 18+ Jokes Allowed
+ Include adult humor when appropriate (admin-controlled)
+
+
+
+
+
+
+
+
+
+ Long Stories & Poems
+ DaveAI can tell long stories, recite poems, or go on entertaining tangents
+
+
+
+
+
+
+
+
+
+ Conversation Memory
+ Remember past conversations, preferences, and context across sessions
+
+
+
+
+
+
+
+
Memory Size (conversations stored)
+
+
+ 100
+
+
+
+
+ Clear Memory
+
+ Undo Clear
+
+ Export
+
+
+
Custom Personality Prompt (optional)
+
+
+
+
+
+
+
+ Auto-Refresh Projects
+ Periodically scan for new, updated, or changed projects across all categories
+
+
+
+ Enable Auto-Refresh
+ Checks every 2 hours for changes in games, apps, web, and other categories
+
+
+
+
+
+
+
+ Interval
+
+ 30 min
+ 1 hour
+ 2 hours
+ 4 hours
+ 8 hours
+
+
+ Refresh Now
+ —
+
+
+
+
+
+
+ Local LLM (LM Studio)
+ Admin
+ Use your local RTX 3090 Ti to power DaveAI stories, voice, and chat via LM Studio
+
+
+
+
+ Enable Local LLM
+ Route DaveAI chat through LM Studio instead of cloud API
+
+
+
+
+
+
+
+
+
+ Auto-load LM Studio Models
+ Automatically select best available model (14B+ with 80k+ context)
+
+
+
+
+
+
+
+
+
LM Studio URL
+
+
+
Chat / Story Model
+
+ — Auto-detect from LM Studio —
+
+ Qwen3-30B-A3B-2507 ⭐ BEST — Jul 2025, 262K ctx, instant
+ Qwen3-30B-A3B — Original MOE, thinking+non-thinking
+ Qwen3-Coder-30B-A3B — MOE coding specialist
+
+
+ DeepSeek-R1-Distill-Qwen-14B (Q6) — Best
+ reasoning
+ Qwen3-14B — Dense, smart + fast
+ Mistral Small 3.1 24B — Great
+ storyteller
+ QwQ-32B (Q4) — Deep reasoning
+ GLM-4 32B — Large creative
+
+
+ DeepSeek
+ MOE 4x8B 24B — Uncensored thinker
+ Dark
+ Champion MOE 18B — Abliterated
+ Dirty Shirley Writer 9B —
+ Uncensored stories
+ Reka Flash 21B —
+ Uncensored reasoning
+ Dolphin 3.0 8B — Uncensored helpful
+
+ Qwen3-8B Jailbroken — No filters
+ Stheno 8B — RP/Creative
+
+
+ Qwen2.5 Coder 32B — Best local coder
+ Devstral Small — Dev assistant
+ Viper Coder 7B — Fast coding
+ OlympicCoder 7B — Competition coder
+
+
+ Phi-4 Reasoning Plus — Microsoft
+ DeepSeek R1-0528 Qwen3 8B — Latest
+
+ DeepSeek R1 Qwen 7B — Fast reasoning
+
+
+
+ Qwen2.5-VL 7B — Best vision
+ Llama 3.2 11B Vision — Meta
+ Pixtral 12B — Mistral vision
+
+
+
+
+
Auto Model Switch
+
Automatically swap between story and coding
+ models. Uses MOE for instant loads.
+
+
+
+ Enable Auto-Switch
+ Load story model for chat, coding model when idle
+
+
+
+
+
+
+
+
Coding Fallback Model (loads when idle)
+
+ Qwen3-Coder-30B-A3B (MOE) — instant, best coder
+ Qwen3-30B-A3B-2507 (MOE) — general purpose
+ Qwen2.5 Coder 32B — dense, powerful
+ GLM-4 32B — creative + code
+ Devstral Small — lightweight dev
+
+
+
Idle Timeout (switch to coding after)
+
+
+ 2 min
+
+
—
+
+
+ Download Qwen3-30B-A3B-2507 (SOTA) in LM
+ Studio
+
+
Local Voice Engine
+
+ None (DaveAI uses Azure Neural voices only)
+
+
+
+
+
+ DaveAI voice output is locked to Azure English Neural voices. Use Voice Studio to select Maisie or another
+ approved Azure voice.
+
+
+ Open Azure Voice Studio
+
Azure Neural only
+
+
+
Temperature
+
+
+ 0.8
+
+
+
Max Tokens
+
+
+ 2048
+
+
+
+
+ Test Connection
+
+ List Models
+
+
Not connected
+
+
+
+
+
+
+
+
+
+
+ DaveAI Chat
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Voices
+
Tuning
+
Presets
+
Agents
+
Config
+
+
+
+
+
+
Filter Voices
+
+ All
+ UK
+ US
+ Female
+ Male
+
+
+
+
+
+
Preview
+
+
+
+ Test
+
+
+
+
+
+
+
Voice Parameters
+
+
+
+
+
Volume / Gain 100%
+
+
+
+
+
+
Clarity / Presence 50%
+
+
+
+
+
+
+
Voice Blend (Mix two voices)
+
+ A
+
+
+
+
+ B
+
+
+
+
Blend creates a custom voice by mixing two voices.
+ e.g. Maisie(2)+Ryan(1) = 67%/33%
+
+
+
Live Preview
+
+
+ Test
+
+
+
+
+
+
+
Built-in Presets
+
+
+
+
+
Your Saved Presets
+
+
No saved presets yet. Configure a voice and click
+ Save.
+
+
+
Import / Share
+
+ Export Current
+ Import JSON
+ Copy Share Link
+
+
+
+
+
+
Agent Voice Assignment
+
Assign different voices to each agent. DaveAI
+ Voice narrates agent activity, code changes, and build status in real-time.
+
+
+
+
+
+
+
+
Narration Events
+
+
+ Read agent chat responses
+ aloud
+
+
+ Announce build start / complete
+
+
+ Announce errors and warnings
+
+
+ Narrate activity feed events
+
+
+ Describe code changes being made
+
+
+
+
+
+
+
Azure Speech Engine
+
+ DaveAI voice is restricted to the approved English Azure Neural catalog. Keys stay server-side; all legacy
+ voice providers are disabled.
+
+
+
Engine
+
+ Azure Speech Neural (DaveAI only - Maisie UK default)
+
+
+
+
+
Output Format
+
+ MP3 24kHz Azure Neural
+
+
+
+
+
Guard Rails
+
+
Rate Limit (requests/min) 10
+
+
+
+
Timeout (seconds) 12
+
+
+
+
+
Max Text Length 800
+
+
+
+
+
+ Cache repeated Azure phrases (5min TTL)
+
+
+ Queue mode (don't interrupt current speech)
+
+
+
+
+
Moshi Instant Voice
+
+ Enable real-time voice conversation with Moshi. Switches from TTS playback to instant voice mode.
+
+
+
+
+
+
+ Enable Moshi Instant Voice
+
+
+ Connecting to Moshi
+ bridge...
+
+
+
+
+
System Diagnostics
+
+
Open Voice Studio to run diagnostics...
+
+
Re-run Diagnostics
+
+
+
Shortcuts
+
+ Ctrl+Shift+V — Open Voice Studio
+ Ctrl+Shift+M — Toggle voice mute
+ Ctrl+Shift+T — Test current voice
+
+
+
+
+
+
+
+
+
+
+
+
+
D
+
DaveAI
+
Agentic Web Builder — sign in to continue
+
+
+
+
+
Display Name
+
Email
+
Password
+
Confirm Password
+
Sign In
+
+
+
+
+
+
+
+
+
+
+
Choose Your Workspace Layout
+
Each preset changes the pane arrangement, working density, and colour system.
+
+
+
+
Balanced canvas, chat, tools, and Activity.
+
Cancel
+
Apply Horizons
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LIVE
+ ◌ ...
+
+
+
+ connecting…
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pages
+
+
+
+
Projects
+
Pages
+
Sites
+
Tools
+
Agents
+
DB
+
Skills
+
+
+
+
+
+ Web 0
+ Apps
+ 0
+ Games
+ 0
+ Other
+ 0
+
+
+
+
+ Resume
+ Sync
+ Export
+
+
+
+ Add project
+
+
+
+
+
+
+
+
Loading deployed pages…
+
+
+
+ New page
+
+
+
+
+
+
+
+
+
+ All
+ Supervisor
+ Coder
+ QA
+ Asset
+
+
+ Runtime registry loading…
+
+
+
+
+
+
+
+
+
Supervisor connecting… —
+
+
+
+
+
+
+
+ Session
+
+
Brain: connecting...
+
Model: LiteLLM routed
+
Tools: loading runtime registry…
+
+
+
+
+
+
+
+ PostgreSQL · daveai
+
+
+
+ Quick Query
+
+
▶
+ Run Query
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Preview
+
Code
+
+
+
Desktop
+
Tablet
+
Android
+
Mobile
+
+
daveai.tech
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Waiting for build — start a conversation to preview your project
+
+
+
+
+
Game
+
Dave's Siege TD
+
Experience our latest tower defense game — built entirely by DaveAI
+ agents. Defend your castle with strategic tower placement!
+
+ Play Now
+ Start
+ Creating
+
+
+ by DaveAI Live Games
+
+
+
+ Admin
+ Edit
+ Change
+ Clear
+ Fullscreen
+
+
+
+
+
+
+
+
+ Playing
+
+
+
+
+
+
+
+
+
+ DaveAI speaking…
+
+
+
+
+
+
+
Idle
+
+
+
+
+
+
+
Loading build history…
+
+
+
+
+
+
+
+
+
Total Pages
+
5
+
2 published
+
+
+
Recent Builds
+
12
+
total logged
+
+
+
Commits
+
34
+
this week
+
+
+
API Status
+
—
+
checking
+
+
+
Events
+
1.2k
+
tracked
+
+
+
Registered Users
+
84k
+
accounts
+
+
+
+
+
+
+
+
+
+ Config
+ Users
+ Agents
+ Tools
+ Logs
+
+
+
Brain URL https://daveai.tech/api
+
LiteLLM :4000 (local)
+
ZeroClaw :3000 (gateway)
+
DB PostgreSQL (daveai)
+
Model LiteLLM routed
+
Tools loading…
+
VPS 187.77.30.206
+
+
+
+ Registered Users
+ +
+ New User
+
+
+
+
+
+
Supervisor s-think · analyzing
+
Coder s-work · building UI
+
QA s-idle
+
Asset s-idle
+
+
+
+
+
+ agent-brain
+ litellm
+ agentic-ui
+
+ Waiting…
+
+
+ Loading logs…
+
+
+
+
+
+
+
+
+
+
Chat Mode
+
DaveAI is ready in the dock below. Replies, memory, voice, and agent activity stay
+ live while this action surface remains focused on the conversation.
+
+ Clean final replies in chat
+ Activity steps stay in the right panel
+ Azure Neural voice reads final answers only
+
+
+
+ Start in chat
+
+
Prepares the chat composer without sending.
+
+
+
+
+
+
+
+
+
Browser Mode
+
Browser Mode sends pages to DaveAI's web tools for reading,
+ screenshots, and step-by-step follow-up inside this action window.
+
+
+ Go
+
+
+ Browse, read, and summarize any public page
+ Click + form-fill requests route through DaveAI's approved web tools
+ Capture screenshots into right-panel Activity
+
+
View current roadmap
+
+
+
+
+
+
+
+
+
Code Agent Mode
+
Code Agent mode keeps plans, file review, test output,
+ and deploy notes organized while DaveAI works.
+
+ Plan + assumptions panel (left)
+ File tree with inline diff (center)
+ Test output + deploy chip (right)
+
+
View current state
+
+
+
+
+
+
+
+
+
Research Mode
+
Research Agent mode keeps sources, citations,
+ and reading notes organized while DaveAI investigates.
+
+
+ Web + primary docs
+ Primary docs only
+ DaveAI workspace
+
+
+ Concise citations
+ Citation rich
+ Short source list
+
+ Prepare research
+
+
+
The prepared request stays editable in chat before sending.
+
+ Choose live-web, primary-doc, or workspace evidence
+ Choose the citation detail before sending
+ Sources and tool activity remain visible in the right panel
+
+
View proof index
+
+
+
+
+
+
+
+
+
Problem Solver Mode
+
Problem Solver mode turns the request into assumptions,
+ a step-by-step plan, and test results as DaveAI works.
+
+
+ Prepare diagnosis
+
+
Builds a diagnosis request with assumptions and proof requirements.
+
+ Problem statement and assumption list
+ Step-by-step plan with test results
+ Re-run tests on demand
+
+
View completion backlog
+
+
+
+
+
+
+
+
+
Hermes Team Mode
+
Hermes Team mode shows delegated work, worker status,
+ and approval gates from DaveAI's Hermes agents.
+
+
+ Prepare delegation
+
+
Current Brain status is shown in the global status bar; delegated writes remain approval-gated.
+
+ Supervisor routes work to the appropriate DaveAI agents
+ Agent state and traces appear in Activity
+ Approval gates remain attached to delegated writes
+
+
View file matrix
+
+
+
+
+
+
+
+
+
Select a saved project
+
+
File
+
Folder
+
+
+
+
+
+
+
+
Select a project file to edit.
+
+
+
+
+
+
+
+
+
+
+
+
+
DaveAI Chat
+
+
+
+
+
+
+
+
+
+
Supervisor: I'm having @Coder update the hero section. @QA will
+ verify contrast ratios.
+
+
+
+
+
+
💬 Chat
+
+
Azure Neural
+
+
Maisie
+
+
+
Auto
+
+ Silent
+
Text
+
+
type /voice to switch
+
+
+
+
+
+
hero-title
+
+
+
+
+
+
+
+
+
Make button purple gradient
+
Add dark mode toggle
+
Check mobile layout
+
Generate logo
+
Add pricing section
+
Run lighthouse audit
+
Screenshot the site
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vps/patches/daveai-production-runtime-20260731/README.txt b/vps/patches/daveai-production-runtime-20260731/README.txt
new file mode 100644
index 00000000000..31775f0e2fa
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/README.txt
@@ -0,0 +1,29 @@
+DaveAI production runtime patch bundle - 2026-07-31
+
+Purpose
+-------
+This bundle persists the production-only fixes made during the DaveAI.tech E2E repair audit so they are no longer trapped as manual VPS edits.
+
+Contents
+--------
+- brain_llm.py
+ Production backend brain file containing the native Ollama direct-chat/streaming repair used by the main chat path.
+
+- nginx/
+ Production Nginx files after the API/login rate-limit, health endpoint, brain vhost, voice health, API health, and StreamHub provider-vault fixes.
+
+- windsurf.html
+ Production Windsurf/Ops page after adding the unauthenticated auth gate that prevents protected admin API polling.
+
+- td2/index-BEGIhVuy.js
+ Production TD2 bundle after adding the asset cache-bust query string.
+
+- td2/generated-assets-manifest.txt
+ List of generated TD2 placeholder VFX PNG and silent MP3 assets that were added to stop missing-resource failures.
+
+- apply-production-runtime-fixes.sh
+ Idempotent deployment helper. It backs up current production files, installs the persisted files, recreates the TD2 generated assets, validates Nginx, and reloads Nginx.
+
+Notes
+-----
+This bundle intentionally does not reboot the VPS, run apt upgrades, or restart unrelated processes. Those are maintenance-window operations.
diff --git a/vps/patches/daveai-production-runtime-20260731/apply-production-runtime-fixes.sh b/vps/patches/daveai-production-runtime-20260731/apply-production-runtime-fixes.sh
new file mode 100644
index 00000000000..2b840c2daaf
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/apply-production-runtime-fixes.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
+BACKUP_DIR="/opt/daveai/backups/${STAMP}-source-controlled-runtime-restore"
+
+AGENT_BRAIN_DIR="/opt/agent-brain"
+SITE_ROOT="/var/www/agentic-website"
+TD2_ROOT="${SITE_ROOT}/games/daveai-td2"
+
+mkdir -p "${BACKUP_DIR}/nginx" "${BACKUP_DIR}/td2"
+
+backup_if_exists() {
+ local src="$1"
+ local dest="$2"
+ if [ -e "$src" ]; then
+ mkdir -p "$(dirname "$dest")"
+ cp -a "$src" "$dest"
+ fi
+}
+
+install_file() {
+ local src="$1"
+ local dest="$2"
+ local mode="${3:-0644}"
+ mkdir -p "$(dirname "$dest")"
+ cp -a "$src" "$dest"
+ chmod "$mode" "$dest"
+}
+
+echo "Backing up current production files to ${BACKUP_DIR}"
+backup_if_exists "${AGENT_BRAIN_DIR}/brain_llm.py" "${BACKUP_DIR}/brain_llm.py"
+backup_if_exists "${SITE_ROOT}/windsurf.html" "${BACKUP_DIR}/windsurf.html"
+backup_if_exists "${TD2_ROOT}/assets/index-BEGIhVuy.js" "${BACKUP_DIR}/td2/index-BEGIhVuy.js"
+
+for name in daveai.tech iptv.daveai.tech voice.daveai.tech brain.daveai.tech stories.daveai.tech; do
+ backup_if_exists "/etc/nginx/sites-enabled/${name}" "${BACKUP_DIR}/nginx/${name}"
+done
+backup_if_exists "/etc/nginx/conf.d/00-kilo-bot-protection.conf" "${BACKUP_DIR}/nginx/00-kilo-bot-protection.conf"
+
+echo "Installing backend brain and production HTML"
+install_file "${SCRIPT_DIR}/brain_llm.py" "${AGENT_BRAIN_DIR}/brain_llm.py"
+install_file "${SCRIPT_DIR}/windsurf.html" "${SITE_ROOT}/windsurf.html"
+install_file "${SCRIPT_DIR}/td2/index-BEGIhVuy.js" "${TD2_ROOT}/assets/index-BEGIhVuy.js"
+
+echo "Installing Nginx configuration"
+for name in daveai.tech iptv.daveai.tech voice.daveai.tech brain.daveai.tech stories.daveai.tech; do
+ install_file "${SCRIPT_DIR}/nginx/${name}" "/etc/nginx/sites-enabled/${name}"
+done
+install_file "${SCRIPT_DIR}/nginx/00-kilo-bot-protection.conf" "/etc/nginx/conf.d/00-kilo-bot-protection.conf"
+
+echo "Recreating TD2 generated assets from manifest"
+python3 - <<'PY' "${SCRIPT_DIR}/td2/generated-assets-manifest.txt" "${TD2_ROOT}"
+from pathlib import Path
+import base64
+import sys
+
+manifest = Path(sys.argv[1])
+td2_root = Path(sys.argv[2])
+
+png_payload = base64.b64decode(
+ "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAFElEQVR42mP4TyJgGNUwqmH4agAAr639H23ooMoAAAAASUVORK5CYII="
+)
+mp3_payload = base64.b64decode(
+ "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjE2LjEwMAAAAAAAAAAAAAAA//tAwAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAFAAACvgBoaGhoaGhoaGhoaGhoaGhoaGhojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo60tLS0tLS0tLS0tLS0tLS0tLS0tNra2tra2tra2tra2tra2tra2tra//////////////////////////8AAAAATGF2YzYwLjMxAAAAAAAAAAAAAAAAJAMGAAAAAAAAAr4QurGFAAAAAAD/+xDEAAPAAAGkAAAAIAAANIAAAARMQU1FMy4xMDBVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMQpg8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sQxFMDwAABpAAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+xDEfIPAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/7EMSmA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"
+)
+
+written = {"png": 0, "mp3": 0}
+for raw in manifest.read_text(encoding="utf-8-sig").splitlines():
+ rel = raw.strip()
+ if not rel:
+ continue
+ target = td2_root / rel
+ target.parent.mkdir(parents=True, exist_ok=True)
+ if rel.endswith(".png"):
+ target.write_bytes(png_payload)
+ written["png"] += 1
+ elif rel.endswith(".mp3"):
+ target.write_bytes(mp3_payload)
+ written["mp3"] += 1
+ else:
+ raise SystemExit(f"Unsupported generated TD2 asset: {rel}")
+
+print(written)
+PY
+
+echo "Validating Python and Nginx syntax"
+python3 -m py_compile "${AGENT_BRAIN_DIR}/brain_llm.py"
+nginx -t
+
+echo "Reloading Nginx and restarting agent-brain"
+systemctl reload nginx
+if command -v pm2 >/dev/null 2>&1; then
+ pm2 restart agent-brain --update-env
+fi
+
+echo "Done. Backup: ${BACKUP_DIR}"
diff --git a/vps/patches/daveai-production-runtime-20260731/brain_llm.py b/vps/patches/daveai-production-runtime-20260731/brain_llm.py
new file mode 100644
index 00000000000..7c48bc68295
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/brain_llm.py
@@ -0,0 +1,430 @@
+"""brain_llm.py — LLM abstraction: 4-stage fallback, streaming, JSON mode, caching.
+
+Stage chain (no cloud-provider dependency — local-first):
+ 1. LiteLLM streaming (model → heavy-coder / fast-agent / vision via /opt/litellm/config.yaml)
+ 2. LiteLLM non-stream (same model, no stream)
+ 3. LiteLLM fast-agent (if HEAVY failed, retry with FAST)
+ 4. Direct LM Studio (bypass LiteLLM proxy entirely — final safety net)
+
+No Anthropic SDK dependency. Removed 2026-05-25 per Dave's directive: "anthropic isn't
+one of the api keys in the list to be usable". If a cloud provider key (Anthropic/MiniMax/
+SiliconFlow/OpenRouter) is rotated and re-enabled later, the LiteLLM config can route
+brain aliases through it transparently — no code change required here.
+"""
+import os, json, re, time, hashlib, threading, urllib.request, urllib.error, sys
+from brain_core import LLM_BASE, HEAVY, FAST, VISION
+from brain_events import agent_set, emit
+
+
+# ── ReAct control-token stream filter ─────────────────────────────────────────
+# Defense-in-depth (Wave 2 ReAct leak fix, 2026-05-25):
+# Even if a build-keyword prompt slips past is_website_change() narrowing,
+# the agent's ReAct scaffolding (THOUGHT: / ACTION: / OBSERVATION: / FINAL:)
+# MUST NOT reach the user-visible chat bubble. This filter buffers stream
+# tokens line-by-line and:
+# - drops complete lines that start with THOUGHT:/ACTION:/OBSERVATION:
+# - for FINAL: lines, emits only the body after the marker
+# - passes through everything else unchanged
+#
+# Used by chat / reply / narrator labels where the assistant text is shown to
+# the user. ReAct internal labels ("coder", "asset") should not stream at all
+# (brain_graph.py _react now passes stream_label="" for the LLM call), but this
+# filter is a belt-and-braces guarantee.
+
+_REACT_DROP_PREFIXES = ("THOUGHT:", "ACTION:", "OBSERVATION:")
+_REACT_FINAL_RE = re.compile(r"\bFINAL\s*:\s*", re.IGNORECASE)
+
+
+class _ReActStreamFilter:
+ """Strip ReAct control tokens from streaming text, line by line.
+
+ Tokens arrive in arbitrary chunks, so we buffer until a newline and then
+ classify the line. Partial trailing text (no newline yet) is held until
+ either we see more tokens or `flush()` is called. The filter is permissive
+ on the wire: only well-formed control-token lines are dropped — normal
+ chat text passes through with no change.
+ """
+
+ __slots__ = ("_buf", "_after_final", "_seen_any_final")
+
+ def __init__(self):
+ self._buf: str = ""
+ self._after_final: bool = False
+ self._seen_any_final: bool = False
+
+ def feed(self, token: str) -> str:
+ """Return the portion of `token` that should be emitted downstream."""
+ if not token:
+ return ""
+ self._buf += token
+ out_parts: list[str] = []
+ # Emit any complete lines, leave a tail in the buffer for next chunk.
+ while True:
+ nl = self._buf.find("\n")
+ if nl < 0:
+ # No complete line yet. If we are past a FINAL: marker and
+ # the partial buffer doesn't look like a fresh directive, we
+ # can still emit it incrementally so the UI streams cleanly.
+ if self._after_final and self._buf and not self._buf.lstrip().startswith(
+ _REACT_DROP_PREFIXES + ("FINAL:",)):
+ out_parts.append(self._buf)
+ self._buf = ""
+ break
+ line = self._buf[: nl + 1] # include trailing \n
+ self._buf = self._buf[nl + 1:]
+ stripped = line.lstrip()
+ if stripped.startswith(_REACT_DROP_PREFIXES):
+ # Drop the whole line, including its trailing newline.
+ self._after_final = False
+ continue
+ m = _REACT_FINAL_RE.match(stripped) if stripped else None
+ if m:
+ body = stripped[m.end():] # text after "FINAL:"
+ out_parts.append(body)
+ self._after_final = True
+ self._seen_any_final = True
+ continue
+ # Normal line — emit as-is.
+ out_parts.append(line)
+ return "".join(out_parts)
+
+ def flush(self) -> str:
+ """Return any remaining buffered text (called at stream end)."""
+ if not self._buf:
+ return ""
+ stripped = self._buf.lstrip()
+ if stripped.startswith(_REACT_DROP_PREFIXES):
+ self._buf = ""
+ return ""
+ m = _REACT_FINAL_RE.match(stripped) if stripped else None
+ if m:
+ body = stripped[m.end():]
+ self._buf = ""
+ self._seen_any_final = True
+ return body
+ out = self._buf
+ self._buf = ""
+ return out
+
+try:
+ from litellm import completion as _litellm_completion
+ _LITELLM_OK = True
+except ImportError:
+ _LITELLM_OK = False
+
+# ── Local-direct fallback config (overridable via /opt/agent-brain/.env) ──────
+# Used as Stage 4: bypasses LiteLLM proxy and calls LM Studio directly via HTTP.
+# Default points at the Tailnet LM Studio endpoint that the brain aliases already use.
+LOCAL_DIRECT_URL = os.getenv(
+ "LOCAL_LLM_DIRECT_URL",
+ "http://127.0.0.1:11434/v1/chat/completions",
+)
+LOCAL_DIRECT_MODEL = os.getenv(
+ "LOCAL_LLM_DIRECT_MODEL",
+ "qwen2.5-coder:3b",
+)
+LOCAL_OLLAMA_CHAT_URL = os.getenv(
+ "LOCAL_OLLAMA_CHAT_URL",
+ "http://127.0.0.1:11434/api/chat",
+)
+LOCAL_CHAT_MAX_TOKENS = int(os.getenv("LOCAL_CHAT_MAX_TOKENS", "512"))
+
+# User-facing message shown when EVERY LLM path failed. Friendly, voice-safe (no
+# stack traces, no tokens, no URLs).
+_NO_LLM_USER_MSG = (
+ "I'm having trouble reaching my language model right now. "
+ "Please try again in a moment."
+)
+
+# ── Simple response cache (LRU-ish, 128 slots) ─────────────────────────────────
+_cache: dict = {}
+_cache_lock = threading.Lock()
+_CACHE_MAX = 128
+_CACHE_TTL = 300 # seconds
+
+
+def _cache_key(model: str, prompt: str, system: str) -> str:
+ # NOTE: hash FULL system prompt — quick_reply/invoke splice session
+ # history into system prompt after byte 80; truncating caused cross-session
+ # cache collisions (Wave 1g finding 2026-05-25). Prompt truncation kept
+ # since user msgs are bounded by chat UI.
+ sys_hash = hashlib.md5(system.encode(errors='replace')).hexdigest()[:16]
+ raw = f"{model}|{sys_hash}|{prompt[:200]}"
+ return hashlib.md5(raw.encode()).hexdigest()
+
+
+def _cache_get(key: str) -> str | None:
+ with _cache_lock:
+ entry = _cache.get(key)
+ if entry and (time.monotonic() - entry["ts"]) < _CACHE_TTL:
+ return entry["value"]
+ return None
+
+
+def _cache_put(key: str, value: str):
+ with _cache_lock:
+ if len(_cache) >= _CACHE_MAX:
+ oldest = min(_cache, key=lambda k: _cache[k]["ts"])
+ del _cache[oldest]
+ _cache[key] = {"value": value, "ts": time.monotonic()}
+
+
+# ── Core litellm call (single attempt) ────────────────────────────────────────
+def _litellm_call(model: str, msgs: list, stream: bool = False,
+ timeout: int = 300, q=None, stream_label: str = "") -> str:
+ if not _LITELLM_OK:
+ return ""
+ if stream:
+ collected = []
+ reasoning_collected = []
+ # ReAct sanitizer — strips THOUGHT:/ACTION:/OBSERVATION:/FINAL: prefixes
+ # from user-visible token stream. The `collected` buffer still gets the
+ # RAW tokens so the caller's full-text return value (used by _react to
+ # parse ACTION lines / FINAL bodies) is unaffected.
+ react_filter = _ReActStreamFilter() if (q and stream_label) else None
+ r = _litellm_completion(
+ model=f"openai/{model}", messages=msgs,
+ api_base=LLM_BASE, api_key="local", timeout=timeout,
+ stream=True, max_tokens=4096)
+ for chunk in r:
+ delta = chunk.choices[0].delta if chunk.choices else None
+ if delta:
+ token = getattr(delta, "content", "") or ""
+ reasoning = getattr(delta, "reasoning_content", "") or ""
+ if token:
+ collected.append(token)
+ if react_filter is not None:
+ clean = react_filter.feed(token)
+ if clean:
+ emit(q, "token", token=clean, msg=clean, label=stream_label)
+ elif reasoning:
+ reasoning_collected.append(reasoning)
+ # Flush any trailing buffered text (last partial line w/o newline).
+ if react_filter is not None:
+ tail = react_filter.flush()
+ if tail:
+ emit(q, "token", token=tail, msg=tail, label=stream_label)
+ result = "".join(collected).strip()
+ if not result:
+ result = "".join(reasoning_collected).strip()
+ return result
+ else:
+ r = _litellm_completion(
+ model=f"openai/{model}", messages=msgs,
+ api_base=LLM_BASE, api_key="local", timeout=timeout,
+ max_tokens=4096)
+ msg_obj = r.choices[0].message
+ text = (msg_obj.content or "").strip()
+ if not text:
+ text = (getattr(msg_obj, "reasoning_content", "") or "").strip()
+ return text
+
+
+# ── Stage 4: direct local model call (no LiteLLM) ─────────────────────────────
+def _local_direct_call(prompt: str, system: str = "", timeout: int = 60,
+ max_tokens: int | None = None, *, q=None,
+ stream_label: str = "") -> str:
+ """Final safety net: bypass LiteLLM proxy and call the local model directly.
+
+ Production note (2026-07-31): normal chat must stream visible tokens from
+ Ollama's native /api/chat. The VPS model is slow per token; waiting for the
+ full response makes the UI look frozen and lets upstream clients time out.
+ """
+ msgs = []
+ if system:
+ msgs.append({"role": "system", "content": system})
+ msgs.append({"role": "user", "content": prompt})
+ token_budget = int(max_tokens or LOCAL_CHAT_MAX_TOKENS)
+ should_stream = bool(q is not None and stream_label)
+
+ native_payload = json.dumps({
+ "model": LOCAL_DIRECT_MODEL,
+ "messages": msgs,
+ "stream": should_stream,
+ "keep_alive": os.getenv("LOCAL_OLLAMA_KEEP_ALIVE", "10m"),
+ "options": {"num_predict": token_budget, "temperature": 0.5},
+ }).encode("utf-8")
+ collected: list[str] = []
+ try:
+ req = urllib.request.Request(
+ LOCAL_OLLAMA_CHAT_URL,
+ data=native_payload,
+ headers={"Content-Type": "application/json"},
+ method="POST",
+ )
+ with urllib.request.urlopen(req, timeout=timeout) as r:
+ if should_stream:
+ for raw in r:
+ if not raw or not raw.strip():
+ continue
+ try:
+ data = json.loads(raw.decode("utf-8", errors="replace"))
+ except Exception:
+ continue
+ token = ((data.get("message") or {}).get("content")
+ or data.get("response") or "")
+ if token:
+ collected.append(token)
+ emit(q, "token", token=token, msg=token,
+ label=stream_label)
+ if data.get("done"):
+ break
+ text = "".join(collected).strip()
+ else:
+ data = json.loads(r.read().decode("utf-8", errors="replace"))
+ text = ((data.get("message") or {}).get("content")
+ or data.get("response") or "").strip()
+ if text:
+ return text
+ except Exception as e:
+ partial = "".join(collected).strip()
+ if partial:
+ sys.stderr.write(
+ f"[brain_llm ollama-native stream partial after {type(e).__name__}: {e}]\n")
+ return partial
+ sys.stderr.write(
+ f"[brain_llm ollama-native direct failed: {type(e).__name__}: {e}]\n")
+
+ payload = json.dumps({
+ "model": LOCAL_DIRECT_MODEL,
+ "messages": msgs,
+ "max_tokens": token_budget,
+ "temperature": 0.5,
+ }).encode("utf-8")
+ try:
+ req = urllib.request.Request(
+ LOCAL_DIRECT_URL,
+ data=payload,
+ headers={"Content-Type": "application/json"},
+ method="POST",
+ )
+ with urllib.request.urlopen(req, timeout=timeout) as r:
+ data = json.loads(r.read().decode("utf-8", errors="replace"))
+ text = ((data.get("choices") or [{}])[0].get("message", {}).get("content") or "").strip()
+ text = text or _NO_LLM_USER_MSG
+ if q is not None and stream_label and text:
+ emit(q, "token", token=text, msg=text, label=stream_label)
+ return text
+ except Exception as e:
+ sys.stderr.write(f"[brain_llm local-direct fallback failed: {type(e).__name__}: {e}]\n")
+ if q is not None and stream_label:
+ emit(q, "token", token=_NO_LLM_USER_MSG, msg=_NO_LLM_USER_MSG,
+ label=stream_label)
+ return _NO_LLM_USER_MSG
+
+
+def llm_chat_direct(prompt: str, system: str = "", *, q=None,
+ stream_label: str = "", agent_name: str = "",
+ timeout: int = 75, max_tokens: int = 96) -> str:
+ """Fast path for user-facing conversational chat.
+
+ Streams native Ollama tokens into the SSE queue when one is provided. This
+ keeps the UI visibly alive even on the current slower VPS model.
+ """
+ if agent_name:
+ agent_set(agent_name, "working", prompt[:60], 50, FAST)
+ result = _local_direct_call(prompt, system, timeout=timeout,
+ max_tokens=max_tokens, q=q,
+ stream_label=stream_label)
+ result = result or _NO_LLM_USER_MSG
+ if agent_name:
+ status = "done" if result and result != _NO_LLM_USER_MSG else "error"
+ agent_set(agent_name, status, result[:60], 100, FAST)
+ return result
+
+
+# ── Public LLM interface ───────────────────────────────────────────────────────
+def llm(model: str, prompt: str, system: str = "", *,
+ q=None, stream_label: str = "", agent_name: str = "",
+ use_cache: bool = False, timeout: int = 300) -> str:
+ """
+ 4-stage fallback (no Anthropic dependency):
+ 1. LiteLLM streaming (model → heavy-coder / fast-agent / vision)
+ 2. LiteLLM non-stream (same model, no stream)
+ 3. LiteLLM fast-agent (if HEAVY failed, retry with FAST)
+ 4. Direct LM Studio (bypass LiteLLM proxy entirely)
+ """
+ if agent_name:
+ agent_set(agent_name, "working", prompt[:60], 50, model)
+
+ if use_cache:
+ ck = _cache_key(model, prompt, system)
+ cached = _cache_get(ck)
+ if cached:
+ if agent_name:
+ agent_set(agent_name, "done", cached[:60], 100, model)
+ return cached
+
+ msgs = []
+ if system:
+ msgs.append({"role": "system", "content": system})
+ msgs.append({"role": "user", "content": prompt})
+
+ text = ""
+
+ # Stage 1 — litellm streaming
+ try:
+ text = _litellm_call(model, msgs, stream=True, timeout=timeout,
+ q=q, stream_label=stream_label)
+ except Exception as e:
+ sys.stderr.write(f"[brain_llm stage1 stream failed model={model}: {type(e).__name__}: {e}]\n")
+
+ # Stage 2 — litellm non-streaming
+ if not text:
+ try:
+ text = _litellm_call(model, msgs, stream=False, timeout=timeout)
+ except Exception as e:
+ sys.stderr.write(f"[brain_llm stage2 nonstream failed model={model}: {type(e).__name__}: {e}]\n")
+
+ # Stage 3 — try fast-agent if heavy-coder failed
+ if not text and model == HEAVY:
+ try:
+ text = _litellm_call(FAST, msgs, stream=False, timeout=60)
+ except Exception as e:
+ sys.stderr.write(f"[brain_llm stage3 fast-retry failed: {type(e).__name__}: {e}]\n")
+
+ # Stage 4 — direct LM Studio fallback (bypasses LiteLLM proxy)
+ if not text:
+ text = _local_direct_call(prompt, system, timeout=60)
+
+ result = text or _NO_LLM_USER_MSG
+
+ if use_cache and result and result != _NO_LLM_USER_MSG:
+ _cache_put(ck, result)
+
+ if agent_name:
+ status = "done" if result and result != _NO_LLM_USER_MSG else "error"
+ agent_set(agent_name, status, result[:60], 100, model)
+
+ return result
+
+
+def llm_json(model: str, prompt: str, system: str = "", **kwargs) -> dict:
+ """LLM call that forces JSON output and parses it."""
+ json_sys = (system + "\n\nRespond ONLY with valid JSON. No prose, no markdown."
+ if system else "Respond ONLY with valid JSON. No prose, no markdown.")
+ raw = llm(model, prompt, json_sys, **kwargs)
+ # try to extract JSON from response
+ for start in [raw.find("{"), raw.find("[")]:
+ if start >= 0:
+ try:
+ return json.loads(raw[start:])
+ except Exception as e:
+ sys.stderr.write(f"[brain_llm llm_json parse failed: {type(e).__name__}]\n")
+ # fallback: wrap raw in a dict
+ return {"raw": raw}
+
+
+def llm_fast(prompt: str, system: str = "", **kwargs) -> str:
+ # W2: pin English + brevity by default for chat-fast path. Caller-supplied system overrides.
+ if not system:
+ system = "Respond in English. Be concise and friendly."
+ # W3: enable response cache by default for fast path (cache infra already exists, was disabled).
+ kwargs.setdefault("use_cache", True)
+ return llm(FAST, prompt, system, **kwargs)
+
+
+def llm_heavy(prompt: str, system: str = "", **kwargs) -> str:
+ if not system:
+ system = "Respond in English."
+ return llm(HEAVY, prompt, system, **kwargs)
diff --git a/vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh b/vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh
new file mode 100644
index 00000000000..66fd74bb6aa
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/check-litellm-health.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+LITELLM_BASE="${LITELLM_BASE:-http://127.0.0.1:4000}"
+MODEL="${LITELLM_HEALTH_MODEL:-fast-agent}"
+ENV_FILE="${LITELLM_ENV_FILE:-/opt/litellm/.env}"
+
+if [ -f "$ENV_FILE" ]; then
+ set -a
+ # shellcheck disable=SC1090
+ . "$ENV_FILE"
+ set +a
+fi
+
+echo "Checking LiteLLM readiness at ${LITELLM_BASE}/health/readiness"
+curl -fsS --max-time 10 "${LITELLM_BASE}/health/readiness" >/dev/null
+
+echo "Checking LiteLLM liveliness at ${LITELLM_BASE}/health/liveliness"
+curl -fsS --max-time 10 "${LITELLM_BASE}/health/liveliness" >/dev/null
+
+echo "Checking LiteLLM model list at ${LITELLM_BASE}/v1/models"
+curl -fsS --max-time 10 "${LITELLM_BASE}/v1/models" >/dev/null
+
+echo "Checking LiteLLM completion path with model ${MODEL}"
+AUTH_ARGS=()
+if [ -n "${LITELLM_MASTER_KEY:-}" ]; then
+ AUTH_ARGS=(-H "Authorization: Bearer ${LITELLM_MASTER_KEY}")
+else
+ echo "LITELLM_MASTER_KEY is not set; probing unauthenticated local completion path."
+fi
+
+curl -fsS --max-time 60 "${LITELLM_BASE}/v1/chat/completions" \
+ "${AUTH_ARGS[@]}" \
+ -H "Content-Type: application/json" \
+ -d "{\"model\":\"${MODEL}\",\"messages\":[{\"role\":\"user\",\"content\":\"reply pong only\"}],\"max_tokens\":8,\"stream\":false}" \
+ | python3 -c "import json,sys; data=json.load(sys.stdin); assert data.get('choices'), data; print(data['choices'][0]['message'].get('content','')[:80])"
+
+echo "LiteLLM functional health passed."
diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/00-kilo-bot-protection.conf b/vps/patches/daveai-production-runtime-20260731/nginx/00-kilo-bot-protection.conf
new file mode 100644
index 00000000000..e5e50282448
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/nginx/00-kilo-bot-protection.conf
@@ -0,0 +1,31 @@
+# Kilo/DaveAI origin bot guard.
+# Cloudflare should remain the primary WAF. These nginx guards are a
+# reversible origin fallback for obvious scanner probes and login floods.
+
+map $http_cf_connecting_ip $kilo_client_key {
+ default $http_cf_connecting_ip;
+ "" $binary_remote_addr;
+}
+
+map $uri $kilo_bad_probe {
+ default 0;
+ ~*^/(?:\.env|\.git|\.svn|\.hg|\.DS_Store|\.codex|auth\.json)$ 1;
+ ~*^/(?:wp-admin|wp-content|wp-includes|wp-json|wp-login\.php|xmlrpc\.php)(?:/|$) 1;
+ ~*^/(?:phpmyadmin|pma|adminer)(?:/|$) 1;
+ ~*^/(?:v1/messages|v1/chat/completions|v1/models|v1beta/models)$ 1;
+}
+
+map $http_user_agent $kilo_bad_ua {
+ default 0;
+ ~*(?:sqlmap|nikto|masscan|zgrab|nuclei|LLMGatewayScan) 1;
+}
+
+map $arg_return_to $kilo_bad_return_to {
+ default 0;
+ ~*(?:/\.env|/\.git|wp-admin|wp-content|wp-includes|wp-json|wp-login\.php|xmlrpc\.php) 1;
+ ~*(?:mysql|mongodb|redis|database|db|backup|dump|passwd|shadow) 1;
+ ~*\.(?:sql|sqlite|db|bak|old|tar|tgz|gz|bz2|xz|zst|zip|rar|7z)(?:$|[?#]) 1;
+}
+
+limit_req_zone $kilo_client_key zone=kilo_login_guard:10m rate=60r/m;
+limit_req_status 429;
diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/brain.daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/brain.daveai.tech
new file mode 100644
index 00000000000..ca1406157d9
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/nginx/brain.daveai.tech
@@ -0,0 +1,40 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name brain.daveai.tech;
+
+ location ^~ /.well-known/acme-challenge/ {
+ root /var/www/letsencrypt;
+ default_type text/plain;
+ }
+
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name brain.daveai.tech;
+
+ ssl_certificate /etc/nginx/ssl/daveai.tech/cloudflare-origin.pem;
+ ssl_certificate_key /etc/nginx/ssl/daveai.tech/cloudflare-origin.key;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+ access_log /var/log/nginx/brain-access.log combined;
+ error_log /var/log/nginx/brain-error.log warn;
+
+ location / {
+ proxy_pass http://127.0.0.1:8888;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_buffering off;
+ }
+}
diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/daveai.tech
new file mode 100644
index 00000000000..25564f7e889
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/nginx/daveai.tech
@@ -0,0 +1,348 @@
+# daveai.tech nginx reverse proxy
+# Cloudflare handles SSL (Full mode) this listens on port 80
+# If you add Let's Encrypt: run certbot --nginx -d daveai.tech
+
+# Rate limiting: 10 requests/sec per IP for API, 2/sec for admin login
+limit_req_zone $kilo_client_key zone=api:10m rate=80r/s;
+limit_req_zone $kilo_client_key zone=login:10m rate=2r/s;
+
+server {
+ if ($kilo_bad_probe) { return 444; }
+ if ($kilo_bad_ua) { return 444; }
+ if ($kilo_bad_return_to) { return 444; }
+
+ location ^~ /.well-known/acme-challenge/ {
+ root /var/www/letsencrypt;
+ default_type text/plain;
+ }
+
+ listen 80;
+ listen 443 ssl;
+ server_name daveai.tech;
+
+ # SSL certs for Cloudflare Full mode
+ ssl_certificate /etc/letsencrypt/live/daveai-root/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/daveai-root/privkey.pem;
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers HIGH:!aNULL:!MD5;
+
+ client_max_body_size 50M;
+ proxy_read_timeout 300s;
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 300s;
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Referrer-Policy "no-referrer" always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+ add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com https://cdnjs.cloudflare.com; connect-src 'self' https://daveai.tech wss://daveai.tech https://api-inference.huggingface.co https://worldtimeapi.org; media-src 'self' blob:;" always;
+ add_header Permissions-Policy "geolocation=(), microphone=(self), camera=()" always;
+
+ # Block private Playwright artifact / reference store from public access
+ # (per wave2-handoffs/16-PLAYWRIGHT-HERMES-VISUAL-E2E.md §6).
+ # All paths beginning with __ (double-underscore) are admin-only and must
+ # only be served via /api/playwright/artifact/* with admin JWT.
+ location ^~ /__ {
+ return 444;
+ }
+
+ # Import preview assets are API responses even when they end in .html/.css.
+ # Keep this ^~ block above regex static locations so tokenized preview files
+ # reach FastAPI instead of being treated as public website assets.
+ location ^~ /api/imports/preview-asset/ {
+ limit_req zone=api burst=160 nodelay;
+ proxy_pass http://127.0.0.1:8888/imports/preview-asset/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_read_timeout 60s;
+ proxy_hide_header Content-Security-Policy;
+ proxy_hide_header X-Frame-Options;
+ proxy_hide_header X-Content-Type-Options;
+ add_header Cache-Control "no-store" always;
+ add_header Content-Security-Policy "default-src 'none'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; media-src 'self' data:; script-src 'none'; connect-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ }
+
+ # Published DaveAI help and proof documents.
+ location ^~ /docs/ {
+ root /var/www/agentic-website;
+ try_files $uri =404;
+ default_type text/plain;
+ }
+
+ # Public previews for isolated DaveAI projects. Only a project landing
+ # route and browser-safe static asset extensions are served; dotfiles,
+ # Git data, source secrets, and arbitrary workspace files fall through to
+ # the 404 location below.
+ location ~ ^/user-projects/user-[0-9]+/[0-9]+-[a-z0-9-]+/?$ {
+ root /var/www/agentic-website;
+ try_files $uri/index.html =404;
+ expires -1;
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
+ }
+
+ location ~* ^/user-projects/user-[0-9]+/[0-9]+-[a-z0-9-]+/(?:[A-Za-z0-9_-]+/)*[A-Za-z0-9_-]+\.(?:html?|css|js|mjs|cjs|json|map|png|jpe?g|gif|ico|svg|webp|avif|woff2?|ttf|wasm|txt|mp3|wav|ogg|mp4|webm)$ {
+ root /var/www/agentic-website;
+ try_files $uri =404;
+ expires -1;
+ add_header Cache-Control "no-cache, must-revalidate";
+ }
+
+ location /user-projects/ {
+ return 404;
+ }
+
+ # HTML files: no-cache so updates deploy instantly
+ location ~* \.html$ {
+ root /var/www/agentic-website;
+ try_files $uri $uri/ @nextjs;
+ expires -1;
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
+ add_header Pragma "no-cache";
+ }
+
+ # Other static assets (CSS, JS, images, fonts, media) — cached 1h
+ location ~* \.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|webp|mp3|wav|mp4|webm|ogg)$ {
+ root /var/www/agentic-website;
+ try_files $uri $uri/ @nextjs;
+ expires 1h;
+ add_header Cache-Control "public, must-revalidate";
+ access_log off;
+ }
+
+ # Named fallback to Next.js
+ location @nextjs {
+ proxy_pass http://127.0.0.1:3001;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ }
+
+ # Plain /login was receiving high bot/scanner volume. Keep it routed to
+ # the app, but throttle by CF-Connecting-IP when Cloudflare is present.
+ location = /login {
+ limit_req zone=kilo_login_guard burst=60 nodelay;
+ proxy_pass http://127.0.0.1:3001;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_cache_bypass $http_upgrade;
+ }
+
+ # Static game files (served from /var/www/agentic-website/games/)
+ location /games/ {
+ root /var/www/agentic-website;
+ index index.html;
+ try_files $uri $uri/ $uri/index.html =404;
+ expires 1h;
+ add_header Cache-Control "public, must-revalidate";
+ }
+
+ # Chat attachment uploads (Wave B 2026-05-25 — brain_uploads_api).
+ # Stored by POST /api/uploads/image -> randomized uuid4. filenames.
+ # See /etc/nginx/conf.d/upload-static.conf for the canonical reference.
+ # STOP-4: nosniff + only image extensions served (everything else 404).
+ location /uploads/ {
+ alias /var/www/agentic-website/uploads/;
+ autoindex off;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Cache-Control "public, max-age=86400" always;
+ add_header Referrer-Policy "no-referrer" always;
+ location ~* ^/uploads/[A-Za-z0-9]+\.(png|jpg|jpeg|webp|gif)$ {
+ try_files $uri =404;
+ }
+ location ~ /uploads/ {
+ return 404;
+ }
+ }
+
+ # Root path: serve index.html (redirects to daveai-ui-v6.html with video intro)
+ location = / {
+ root /var/www/agentic-website;
+ index index.html;
+ try_files /index.html =404;
+ expires -1;
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
+ }
+
+ # Open WebUI - Hermes Chat Interface
+ location /hermes/ {
+ proxy_pass http://localhost:7860/;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_read_timeout 300s;
+ client_max_body_size 100m;
+ proxy_buffering off;
+ sub_filter '' ' ';
+ sub_filter_once on;
+ }
+ location ~* ^/(?!hermes)(_app|static|assets|favicon)/ {
+ proxy_pass http://localhost:7860;
+ }
+
+ # Chat UI Next.js on :3001
+ location / {
+ proxy_pass http://127.0.0.1:3001;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_cache_bypass $http_upgrade;
+ }
+
+ # Admin login — strict rate limit (2 req/sec burst 5)
+ location = /api/admin/login {
+ limit_req zone=login burst=5 nodelay;
+ proxy_pass http://127.0.0.1:8888/admin/login;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ # Auth login/register — rate limited (2 req/sec burst 5)
+ location = /api/auth/login {
+ limit_req zone=login burst=5 nodelay;
+ proxy_pass http://127.0.0.1:8888/auth/login;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ location = /api/auth/register {
+ limit_req zone=login burst=5 nodelay;
+ proxy_pass http://127.0.0.1:8888/auth/register;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
+
+ # SSE streaming endpoint — must come before /api/ block
+ location = /api/stream {
+ proxy_pass http://127.0.0.1:8888/stream;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Connection "";
+ proxy_buffering off;
+ proxy_cache off;
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
+ add_header X-Accel-Buffering no;
+ chunked_transfer_encoding on;
+ }
+
+ # Edge TTS — voice synthesis proxy (port 5050)
+ # Prefix match so /api/tts, /api/tts/health, /api/edge-tts/health etc all route here
+ location = /api/tts {
+ default_type application/json;
+ add_header Cache-Control "no-store" always;
+ return 410 '{"error":"voice_route_retired","replacement":"/api/azure-tts"}';
+ }
+
+ location = /api/edge-tts {
+ default_type application/json;
+ add_header Cache-Control "no-store" always;
+ return 410 '{"error":"voice_route_retired","replacement":"/api/azure-tts"}';
+ }
+
+ location = /api/voices {
+ proxy_pass http://127.0.0.1:8888/voices;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+
+ location = /api/agents/voices {
+ default_type application/json;
+ add_header Cache-Control "no-store" always;
+ return 410 '{"error":"voice_route_retired","replacement":"/api/voices"}';
+ }
+
+
+ # Public health endpoint for frontend liveness checks — intentionally not API-rate-limited.
+ location = /api/health {
+ proxy_pass http://127.0.0.1:8888/health;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ add_header Cache-Control "no-store" always;
+ }
+
+ # Agent Brain REST API strips /api prefix
+ # /api/chat :8888/chat | /api/health :8888/health
+ location /api/ {
+ limit_req zone=api burst=160 nodelay;
+ proxy_pass http://127.0.0.1:8888/;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_read_timeout 300s;
+ }
+
+ # WebSocket real-time chat
+ location /ws {
+ proxy_pass http://127.0.0.1:8888/ws;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "Upgrade";
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_read_timeout 86400s;
+ proxy_send_timeout 86400s;
+ }
+
+ # ZeroClaw gateway localhost only (internal tooling)
+ location /gateway/ {
+ allow 127.0.0.1;
+ deny all;
+ proxy_pass http://127.0.0.1:3000/;
+ }
+
+ # Health check endpoint
+ location = /health {
+ proxy_pass http://127.0.0.1:8888/health;
+ }
+
+ # Next.js static assets cache aggressively
+ location /_next/static/ {
+ proxy_pass http://127.0.0.1:3001;
+ add_header Cache-Control "public, max-age=31536000, immutable";
+ }
+
+ # Custom error pages
+ error_page 404 /404.html;
+ location = /404.html {
+ root /var/www/agentic-website;
+ internal;
+ }
+
+ error_page 502 503 504 /50x.html;
+ location = /50x.html {
+ root /var/www/agentic-website;
+ internal;
+ }
+}
diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/iptv.daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/iptv.daveai.tech
new file mode 100644
index 00000000000..dce51014e2a
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/nginx/iptv.daveai.tech
@@ -0,0 +1,143 @@
+# Wave 8 — iptv.daveai.tech (new vhost, fronts iptv-restream Docker on :3103)
+# Backend: 127.0.0.1:3103 (Docker container iptv-restream-iptv_restream_nginx-1)
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name iptv.daveai.tech;
+
+ location ^~ /.well-known/acme-challenge/ {
+ root /var/www/letsencrypt;
+ default_type text/plain;
+ }
+
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name iptv.daveai.tech;
+
+ ssl_certificate /etc/nginx/ssl/daveai.tech/cloudflare-origin.pem;
+ ssl_certificate_key /etc/nginx/ssl/daveai.tech/cloudflare-origin.key;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+ access_log /var/log/nginx/iptv-access.log combined;
+ error_log /var/log/nginx/iptv-error.log warn;
+
+ client_max_body_size 50M;
+
+ location = /auth/check {
+ internal;
+ proxy_pass http://davetv_auth_gate/auth/check;
+ proxy_pass_request_body off;
+ proxy_set_header Content-Length "";
+ proxy_set_header Cookie $http_cookie;
+ proxy_set_header X-Original-URI $request_uri;
+ proxy_set_header X-Original-Host $host;
+ }
+
+ error_page 401 = @davetv_login_redirect;
+
+ location @davetv_login_redirect {
+ return 302 https://apps.daveai.tech/login?return_to=$scheme://$host$request_uri;
+ }
+
+ location = /login {
+ proxy_pass http://davetv_auth_gate;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+ location = /logout {
+ proxy_pass http://davetv_auth_gate;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+ location = /session {
+ proxy_pass http://davetv_auth_gate;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+ location = /auth/session {
+ proxy_pass http://davetv_auth_gate;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ }
+
+
+ location = /api/provider-vault/providers {
+ auth_request /auth/check;
+ auth_request_set $davetv_user $upstream_http_x_daveai_user;
+ auth_request_set $davetv_email $upstream_http_x_daveai_email;
+ error_page 401 403 = @provider_vault_empty_providers;
+ proxy_pass http://127.0.0.1:3102;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-DaveTV-Authenticated 1;
+ proxy_set_header X-DaveAI-User $davetv_user;
+ proxy_set_header X-DaveAI-Email $davetv_email;
+ }
+
+ location @provider_vault_empty_providers {
+ default_type application/json;
+ add_header Cache-Control "no-store" always;
+ return 200 '{"providers":[]}';
+ }
+
+ location ^~ /api/provider-vault/ {
+ auth_request /auth/check;
+ auth_request_set $davetv_user $upstream_http_x_daveai_user;
+ auth_request_set $davetv_email $upstream_http_x_daveai_email;
+ proxy_pass http://127.0.0.1:3102;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-DaveTV-Authenticated 1;
+ proxy_set_header X-DaveAI-User $davetv_user;
+ proxy_set_header X-DaveAI-Email $davetv_email;
+ proxy_set_header Range $http_range;
+ proxy_set_header If-Range $http_if_range;
+ proxy_buffering off;
+ proxy_read_timeout 600s;
+ proxy_send_timeout 600s;
+ }
+
+ location / {
+ proxy_pass http://127.0.0.1:3103;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+ proxy_buffering off;
+ }
+}
diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/stories.daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/stories.daveai.tech
new file mode 100644
index 00000000000..58e0a53a2e8
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/nginx/stories.daveai.tech
@@ -0,0 +1,136 @@
+map $http_upgrade $stories_connection_upgrade {
+ default upgrade;
+ '' close;
+}
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name stories.daveai.tech;
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name api.daveai.tech;
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name stories.daveai.tech;
+ client_max_body_size 50m;
+ proxy_read_timeout 3600s;
+
+ ssl_certificate /etc/letsencrypt/live/stories.daveai.tech/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/stories.daveai.tech/privkey.pem;
+
+ location ~ ^/(auth|rest)/v1/ {
+ proxy_pass http://127.0.0.1:18000;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ }
+
+ location = /graphql/v1 {
+ proxy_pass http://127.0.0.1:18000;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ }
+
+ location /storage/v1/ {
+ proxy_pass http://127.0.0.1:18000;
+ proxy_http_version 1.1;
+ proxy_request_buffering off;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ }
+
+ location /functions/v1/ {
+ proxy_pass http://127.0.0.1:18000;
+ proxy_http_version 1.1;
+ proxy_buffering off;
+ proxy_read_timeout 3600s;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ }
+
+ location /realtime/v1/ {
+ proxy_pass http://127.0.0.1:18000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $stories_connection_upgrade;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_read_timeout 3600s;
+ }
+
+ location /local-rig/ {
+ if ($http_x_bridge_token != "552e5787c1030d4b56077adfc504897c116287c59dc7314b") { return 403; }
+ proxy_pass https://127.0.0.1:18788/;
+ proxy_ssl_verify off;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_read_timeout 900s;
+ }
+
+ location / {
+ proxy_pass http://127.0.0.1:18080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ }
+}
+
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name api.daveai.tech;
+ client_max_body_size 50m;
+ proxy_read_timeout 3600s;
+
+ ssl_certificate /etc/letsencrypt/live/stories.daveai.tech/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/stories.daveai.tech/privkey.pem;
+
+
+ location = /health {
+ default_type application/json;
+ add_header Cache-Control "no-store" always;
+ return 200 '{"status":"ok","service":"api.daveai.tech","public_health":true}';
+ }
+
+ location / {
+ proxy_pass http://127.0.0.1:18000;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $stories_connection_upgrade;
+ }
+}
diff --git a/vps/patches/daveai-production-runtime-20260731/nginx/voice.daveai.tech b/vps/patches/daveai-production-runtime-20260731/nginx/voice.daveai.tech
new file mode 100644
index 00000000000..107cd784885
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/nginx/voice.daveai.tech
@@ -0,0 +1,107 @@
+# Wave 8 — voice.daveai.tech (HTTPS via Cloudflare Origin wildcard)
+# Backends: edge-tts on 127.0.0.1:5050, model server on 127.0.0.1:8080
+
+limit_req_zone $binary_remote_addr zone=voice_synth_limit:10m rate=10r/s;
+limit_req_zone $binary_remote_addr zone=voice_api_limit:10m rate=30r/s;
+
+upstream voice_edge_tts_backend {
+ server 127.0.0.1:5050;
+}
+
+upstream voice_model_backend {
+ server 127.0.0.1:8080;
+}
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name voice.daveai.tech;
+
+ location ^~ /.well-known/acme-challenge/ {
+ root /var/www/letsencrypt;
+ default_type text/plain;
+ }
+
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name voice.daveai.tech;
+
+ ssl_certificate /etc/nginx/ssl/daveai.tech/cloudflare-origin.pem;
+ ssl_certificate_key /etc/nginx/ssl/daveai.tech/cloudflare-origin.key;
+ ssl_protocols TLSv1.2 TLSv1.3;
+
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+ add_header Access-Control-Allow-Origin "*" always;
+ add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
+ add_header Access-Control-Allow-Headers "*" always;
+
+ access_log /var/log/nginx/voice-access.log combined;
+ error_log /var/log/nginx/voice-error.log warn;
+
+ # CORS preflight
+ location @cors_preflight {
+ add_header Access-Control-Allow-Origin "*";
+ add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
+ add_header Access-Control-Allow-Headers "*";
+ add_header Content-Length 0;
+ add_header Content-Type text/plain;
+ return 204;
+ }
+
+
+ location = /health {
+ proxy_pass http://voice_edge_tts_backend;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_read_timeout 30s;
+ }
+
+ # Synthesize gets tighter rate limit (rewrite /api/X -> /X for upstream)
+ location = /api/synthesize {
+ limit_req zone=voice_synth_limit burst=5 nodelay;
+ proxy_pass http://voice_edge_tts_backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_read_timeout 30s;
+ }
+
+ # API endpoints -> edge-tts-server (rewrite /api/X -> /X for upstream)
+ location /api/ {
+ limit_req zone=voice_api_limit burst=20 nodelay;
+ proxy_pass http://voice_edge_tts_backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_read_timeout 60s;
+ }
+
+ # Model downloads -> model-server
+ location /models/ {
+ limit_req zone=voice_api_limit burst=10 nodelay;
+ proxy_pass http://voice_model_backend;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_read_timeout 300s;
+ }
+
+ # Root: voice landing page
+ location = / {
+ root /var/www/agentic-website;
+ index voice-landing.html;
+ try_files /voice-landing.html =404;
+ }
+}
diff --git a/vps/patches/daveai-production-runtime-20260731/td2/generated-assets-manifest.txt b/vps/patches/daveai-production-runtime-20260731/td2/generated-assets-manifest.txt
new file mode 100644
index 00000000000..7de39e34d11
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/td2/generated-assets-manifest.txt
@@ -0,0 +1,150 @@
+assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Battle_theme_Halloween_looped.mp3
+assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Battle_theme_exclusion_zone_looped.mp3
+assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_Halloween_looped.mp3
+assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_exclusion_zone_alternative_looped.mp3
+assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_exclusion_zone_looped.mp3
+assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Alternative_theme_Chinese_Street.mp3
+assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Battle_theme_Chinese_Street.mp3
+assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Calm_theme_Chinese_Street.mp3
+assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Main_theme_Chinese_Street.mp3
+assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Stealthy_theme_loopable.mp3
+assets/vfx/damage/1_effect_damage_000.png
+assets/vfx/damage/1_effect_damage_001.png
+assets/vfx/damage/1_effect_damage_002.png
+assets/vfx/damage/1_effect_damage_003.png
+assets/vfx/damage/1_effect_damage_004.png
+assets/vfx/damage/1_effect_damage_005.png
+assets/vfx/damage/1_effect_damage_006.png
+assets/vfx/damage/1_effect_damage_007.png
+assets/vfx/damage/1_effect_damage_008.png
+assets/vfx/damage/1_effect_damage_009.png
+assets/vfx/def/1_effect_def_000.png
+assets/vfx/def/1_effect_def_001.png
+assets/vfx/def/1_effect_def_002.png
+assets/vfx/def/1_effect_def_003.png
+assets/vfx/def/1_effect_def_004.png
+assets/vfx/def/1_effect_def_005.png
+assets/vfx/def/1_effect_def_006.png
+assets/vfx/def/1_effect_def_007.png
+assets/vfx/def/1_effect_def_008.png
+assets/vfx/def/1_effect_def_009.png
+assets/vfx/fire/1_effect_fire_000.png
+assets/vfx/fire/1_effect_fire_001.png
+assets/vfx/fire/1_effect_fire_002.png
+assets/vfx/fire/1_effect_fire_003.png
+assets/vfx/fire/1_effect_fire_004.png
+assets/vfx/fire/1_effect_fire_005.png
+assets/vfx/fire/1_effect_fire_006.png
+assets/vfx/fire/1_effect_fire_007.png
+assets/vfx/fire/1_effect_fire_008.png
+assets/vfx/fire/1_effect_fire_009.png
+assets/vfx/fire/1_effect_fire_010.png
+assets/vfx/fire/1_effect_fire_011.png
+assets/vfx/fire/1_effect_fire_012.png
+assets/vfx/fire/1_effect_fire_013.png
+assets/vfx/fire/1_effect_fire_014.png
+assets/vfx/fire/1_effect_fire_015.png
+assets/vfx/fire/1_effect_fire_016.png
+assets/vfx/fire/1_effect_fire_017.png
+assets/vfx/fire/1_effect_fire_018.png
+assets/vfx/freeze/1_effect_freeze_000.png
+assets/vfx/freeze/1_effect_freeze_001.png
+assets/vfx/freeze/1_effect_freeze_002.png
+assets/vfx/freeze/1_effect_freeze_003.png
+assets/vfx/freeze/1_effect_freeze_004.png
+assets/vfx/freeze/1_effect_freeze_005.png
+assets/vfx/freeze/1_effect_freeze_006.png
+assets/vfx/freeze/1_effect_freeze_007.png
+assets/vfx/freeze/1_effect_freeze_008.png
+assets/vfx/freeze/1_effect_freeze_009.png
+assets/vfx/freeze/1_effect_freeze_010.png
+assets/vfx/freeze/1_effect_freeze_011.png
+assets/vfx/freeze/1_effect_freeze_012.png
+assets/vfx/freeze/1_effect_freeze_013.png
+assets/vfx/freeze/1_effect_freeze_014.png
+assets/vfx/freeze/1_effect_freeze_015.png
+assets/vfx/rain/1_effect_rain_001.png
+assets/vfx/rain/1_effect_rain_002.png
+assets/vfx/rain/1_effect_rain_003.png
+assets/vfx/rain/1_effect_rain_004.png
+assets/vfx/rain/1_effect_rain_005.png
+assets/vfx/rain/1_effect_rain_006.png
+assets/vfx/rain/1_effect_rain_007.png
+assets/vfx/rain/1_effect_rain_008.png
+assets/vfx/rain/1_effect_rain_009.png
+assets/vfx/rain/1_effect_rain_010.png
+assets/vfx/rain/1_effect_rain_011.png
+assets/vfx/rain/1_effect_rain_012.png
+assets/vfx/rain/1_effect_rain_013.png
+assets/vfx/rain/1_effect_rain_014.png
+assets/vfx/rain/1_effect_rain_015.png
+assets/vfx/rain/1_effect_rain_016.png
+assets/vfx/rain/1_effect_rain_017.png
+assets/vfx/rain/1_effect_rain_018.png
+assets/vfx/rain/1_effect_rain_019.png
+assets/vfx/rain/1_effect_rain_020.png
+assets/vfx/rain/1_effect_rain_021.png
+assets/vfx/rain/1_effect_rain_022.png
+assets/vfx/rain/1_effect_rain_023.png
+assets/vfx/rain/1_effect_rain_024.png
+assets/vfx/rain/1_effect_rain_025.png
+assets/vfx/rain/1_effect_rain_026.png
+assets/vfx/rain/1_effect_rain_027.png
+assets/vfx/rain/1_effect_rain_028.png
+assets/vfx/rain/1_effect_rain_029.png
+assets/vfx/rain/1_effect_rain_030.png
+assets/vfx/rain/1_effect_rain_031.png
+assets/vfx/rain/1_effect_rain_032.png
+assets/vfx/rain/1_effect_rain_033.png
+assets/vfx/rain/1_effect_rain_034.png
+assets/vfx/rain/1_effect_rain_035.png
+assets/vfx/rain/1_effect_rain_036.png
+assets/vfx/rain/1_effect_rain_037.png
+assets/vfx/rain/1_effect_rain_038.png
+assets/vfx/rain/1_effect_rain_039.png
+assets/vfx/stone/1_effect_stone_000.png
+assets/vfx/stone/1_effect_stone_001.png
+assets/vfx/stone/1_effect_stone_002.png
+assets/vfx/stone/1_effect_stone_003.png
+assets/vfx/stone/1_effect_stone_004.png
+assets/vfx/stone/1_effect_stone_005.png
+assets/vfx/stone/1_effect_stone_006.png
+assets/vfx/stone/1_effect_stone_007.png
+assets/vfx/stone/1_effect_stone_008.png
+assets/vfx/stone/1_effect_stone_009.png
+assets/vfx/stone/1_effect_stone_010.png
+assets/vfx/stone/1_effect_stone_011.png
+assets/vfx/stone/1_effect_stone_012.png
+assets/vfx/stone/1_effect_stone_013.png
+assets/vfx/stone/1_effect_stone_014.png
+assets/vfx/stone/1_effect_stone_015.png
+assets/vfx/stone/1_effect_stone_016.png
+assets/vfx/stone/1_effect_stone_017.png
+assets/vfx/time/1_effect_time_000.png
+assets/vfx/time/1_effect_time_001.png
+assets/vfx/time/1_effect_time_002.png
+assets/vfx/time/1_effect_time_003.png
+assets/vfx/time/1_effect_time_004.png
+assets/vfx/time/1_effect_time_005.png
+assets/vfx/time/1_effect_time_006.png
+assets/vfx/time/1_effect_time_007.png
+assets/vfx/time/1_effect_time_008.png
+assets/vfx/time/1_effect_time_009.png
+assets/vfx/time/1_effect_time_010.png
+assets/vfx/time/1_effect_time_011.png
+assets/vfx/time/1_effect_time_012.png
+assets/vfx/time/1_effect_time_013.png
+assets/vfx/zip/1_effect_zip_000.png
+assets/vfx/zip/1_effect_zip_001.png
+assets/vfx/zip/1_effect_zip_002.png
+assets/vfx/zip/1_effect_zip_003.png
+assets/vfx/zip/1_effect_zip_004.png
+assets/vfx/zip/1_effect_zip_005.png
+assets/vfx/zip/1_effect_zip_006.png
+assets/vfx/zip/1_effect_zip_007.png
+assets/vfx/zip/1_effect_zip_008.png
+assets/vfx/zip/1_effect_zip_009.png
+assets/vfx/zip/1_effect_zip_010.png
+assets/vfx/zip/1_effect_zip_011.png
+assets/vfx/zip/1_effect_zip_012.png
+assets/vfx/zip/1_effect_zip_013.png
diff --git a/vps/patches/daveai-production-runtime-20260731/td2/index-BEGIhVuy.js b/vps/patches/daveai-production-runtime-20260731/td2/index-BEGIhVuy.js
new file mode 100644
index 00000000000..2bb238abaaf
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/td2/index-BEGIhVuy.js
@@ -0,0 +1,4 @@
+var dt=Object.defineProperty;var ht=(d,t,e)=>t in d?dt(d,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):d[t]=e;var r=(d,t,e)=>ht(d,typeof t!="symbol"?t+"":t,e);import{P as u}from"./phaser-Cm2tabnc.js";(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function e(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function s(i){if(i.ep)return;i.ep=!0;const o=e(i);fetch(i.href,o)}})();class ct extends u.Scene{constructor(){super({key:"BootScene"})}preload(){this.load.setBaseURL("/"),this.createFallbackLoadingBar()}create(){this.scene.start("PreloadScene")}createFallbackLoadingBar(){const{width:t,height:e}=this.cameras.main;this.add.rectangle(t/2,e/2,400,40,2236962),this.add.rectangle(t/2-198,e/2,4,36,65416),this.add.text(t/2,e/2+40,"LOADING...",{fontSize:"18px",color:"#ffffff",fontFamily:"monospace"}).setOrigin(.5)}}const We="daveai-td2-save",pt={musicVolume:.7,sfxVolume:1,quality:"high",uiSkin:"default"},ae={version:"1.0.0",worlds:{"world-01":{unlocked:!0,levels:{}},"world-02":{unlocked:!1,levels:{}},"world-03":{unlocked:!1,levels:{}},"world-04":{unlocked:!1,levels:{}},"world-05":{unlocked:!1,levels:{}},"world-06":{unlocked:!1,levels:{}},"world-07":{unlocked:!0,levels:{}},"world-08":{unlocked:!1,levels:{}},"world-09":{unlocked:!1,levels:{}},"world-10":{unlocked:!1,levels:{}},"world-11":{unlocked:!1,levels:{}},"world-12":{unlocked:!1,levels:{}},"world-13":{unlocked:!1,levels:{}},"world-14":{unlocked:!1,levels:{}},"world-15":{unlocked:!1,levels:{}},"world-16":{unlocked:!1,levels:{}},"world-17":{unlocked:!1,levels:{}},"world-18":{unlocked:!1,levels:{}},"world-19":{unlocked:!1,levels:{}},"world-20":{unlocked:!1,levels:{}},"world-21":{unlocked:!1,levels:{}},"world-22":{unlocked:!1,levels:{}},"world-23":{unlocked:!1,levels:{}},"world-24":{unlocked:!1,levels:{}},"world-25":{unlocked:!1,levels:{}},"world-26":{unlocked:!1,levels:{}},"world-27":{unlocked:!1,levels:{}},"world-28":{unlocked:!1,levels:{}},"world-29":{unlocked:!1,levels:{}},"world-30":{unlocked:!1,levels:{}}},totalGold:0,achievements:[],settings:pt},G=class G{constructor(){r(this,"currentWorldId",1);r(this,"currentLevelId","");r(this,"gold",150);r(this,"lives",20);r(this,"score",0);r(this,"isPaused",!1);r(this,"abilityCharges",{});r(this,"saveData",ae)}static getInstance(){return G.instance||(G.instance=new G),G.instance}load(){try{const t=localStorage.getItem(We);t&&(this.saveData={...ae,...JSON.parse(t)})}catch{this.saveData={...ae}}}save(){try{localStorage.setItem(We,JSON.stringify(this.saveData))}catch{console.warn("Failed to save game state")}}get settings(){return this.saveData.settings}isWorldUnlocked(t){var e;return((e=this.saveData.worlds[t])==null?void 0:e.unlocked)??!1}getLevelData(t,e){var s;return((s=this.saveData.worlds[t])==null?void 0:s.levels[e])??{completed:!1,stars:0,bestTime:0}}completeLevel(t,e,s,i){this.saveData.worlds[t]||(this.saveData.worlds[t]={unlocked:!0,levels:{}});const o=this.saveData.worlds[t].levels[e];this.saveData.worlds[t].levels[e]={completed:!0,stars:Math.max((o==null?void 0:o.stars)??0,s),bestTime:o!=null&&o.bestTime?Math.min(o.bestTime,i):i},this.save()}unlockWorld(t){this.saveData.worlds[t]||(this.saveData.worlds[t]={unlocked:!1,levels:{}}),this.saveData.worlds[t].unlocked=!0,this.save()}reset(){this.saveData={...ae},this.save()}};r(G,"instance");let Ee=G;const _=Ee.getInstance(),je=["idle","walk","run","attack","hurt","die","jump"],ge=10,Ne=20;class H{static preload(t){for(let e=1;e<=ge;e++)for(const s of je)for(let i=0;i0){const a=s==="hurt"?16:s==="die"?12:10,n=s==="walk"||s==="run"||s==="idle"?-1:0;t.anims.create({key:i,frames:o,frameRate:a,repeat:n})}}}static getAnimKey(t,e){return`enemy-${t}-${e}`}static getSpriteTypeFromId(t){const e=t.match(/(\d+)/);return((e?parseInt(e[1],10):1)-1)%ge+1}}const He={archer:76,magic:31,stone:61,support:17};class he{static preload(t){for(const[e,s]of Object.entries(He))for(let i=1;i<=s;i++){const o=`tower-${e}-${String(i).padStart(3,"0")}`;t.load.image(o,`assets/towers/${e}/${i}.png?v=20260731td2assets2`)}}static registerAnimations(t){for(const[e,s]of Object.entries(He)){const i=`tower-${e}-idle`;if(t.anims.exists(i))continue;const o=[];for(let a=1;a<=s;a++){const n=`tower-${e}-${String(a).padStart(3,"0")}`;t.textures.exists(n)&&o.push({key:n})}o.length>0&&t.anims.create({key:i,frames:o,frameRate:8,repeat:-1})}}static getIdleAnimKey(t){return`tower-${t}-idle`}static getFirstFrameKey(t){return`tower-${t}-001`}}var p=(d=>(d.WAVE_STARTED="wave:started",d.WAVE_COMPLETED="wave:completed",d.ALL_WAVES_COMPLETED="wave:all_completed",d.ENEMY_SPAWN_REQUEST="enemy:spawn-request",d.BOSS_SPAWN_REQUEST="boss:spawn-request",d.ENEMY_SPAWNED="enemy:spawned",d.ENEMY_DIED="enemy:died",d.ENEMY_REACHED_END="enemy:reached_end",d.BOSS_PHASE_CHANGED="boss:phase_changed",d.BOSS_DIED="boss:died",d.TOWER_PLACED="tower:placed",d.TOWER_UPGRADED="tower:upgraded",d.TOWER_SOLD="tower:sold",d.TOWER_SELECTED="tower:selected",d.TOWER_DESELECTED="tower:deselected",d.PROJECTILE_SPLASH="projectile:splash",d.GOLD_CHANGED="economy:gold_changed",d.LIVES_CHANGED="economy:lives_changed",d.DAMAGE_DEALT="damage:dealt",d.LEVEL_COMPLETED="level:completed",d.LEVEL_FAILED="level:failed",d.PLAY_MUSIC="audio:play_music",d.PLAY_SFX="audio:play_sfx",d.SET_MUSIC_VOLUME="audio:set_music_volume",d.SET_SFX_VOLUME="audio:set_sfx_volume",d))(p||{});const U=class U extends u.Events.EventEmitter{static getInstance(){return U.instance||(U.instance=new U),U.instance}emit(t,...e){return super.emit(t,...e)}on(t,e,s){return super.on(t,e,s)}off(t,e,s){return super.off(t,e,s)}};r(U,"instance");let Se=U;const l=Se.getInstance(),mt={menu:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_exclusion_zone_looped.mp3?v=20260731td2assets2",world01:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_Halloween_looped.mp3?v=20260731td2assets2",world02:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Main_theme_Chinese_Street.mp3?v=20260731td2assets2",world03:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Calm_theme_Chinese_Street.mp3?v=20260731td2assets2",world04:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Main_theme_exclusion_zone_alternative_looped.mp3?v=20260731td2assets2",world05:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Alternative_theme_Chinese_Street.mp3?v=20260731td2assets2",world06:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Stealthy_theme_loopable.mp3?v=20260731td2assets2",endless:"assets/audio/craftpix/free_cyberpunk_music_and_sound_effects_pack_6/Music/MP3/Battle_theme_Chinese_Street.mp3?v=20260731td2assets2",bossA:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Battle_theme_exclusion_zone_looped.mp3?v=20260731td2assets2",bossB:"assets/audio/craftpix/free_cyberpunk_audio_pack_4_for_pixel_games/Music/MP3/Battle_theme_Halloween_looped.mp3?v=20260731td2assets2"},ut="music-",ft=1500;function Ge(d){return`${ut}${d}`}function gt(d){if(d===7)return"endless";const t=["world01","world02","world03","world04","world05","world06"],e=(d-1)%t.length;return t[e]}class _t{constructor(t){r(this,"scene");r(this,"volume");r(this,"currentTrackId",null);r(this,"currentSound",null);r(this,"crossfadeTweens",[]);r(this,"_onSetVolume");this.scene=t,this.volume=_.settings.musicVolume,this._onSetVolume=e=>this.setVolume(e.volume),l.on(p.SET_MUSIC_VOLUME,this._onSetVolume,this)}play(t,e=ft){if(t===this.currentTrackId)return;const s=Ge(t);if(!this.isTrackLoaded(s)){console.warn(`[MusicController] Track "${t}" (key: ${s}) not loaded — skipping playback.`);return}const i=this.createSound(s);if(i){if(this.currentSound&&this.currentTrackId!==null)this.crossfade(this.currentSound,i,e);else{this.setWebAudioVolume(i,this.volume);try{i.play()}catch(o){console.warn(`[MusicController] Failed to play track "${t}":`,o);return}}this.currentTrackId=t,this.currentSound=i}}hardCut(t){if(t===this.currentTrackId)return;this.killCrossfadeTweens(),this.currentSound&&(this.currentSound.stop(),this.currentSound.destroy(),this.currentSound=null,this.currentTrackId=null);const e=Ge(t);if(!this.isTrackLoaded(e)){console.warn(`[MusicController] Track "${t}" (key: ${e}) not loaded — skipping playback.`);return}const s=this.createSound(e);if(s){this.setWebAudioVolume(s,this.volume);try{s.play()}catch(i){console.warn(`[MusicController] Failed to play track "${t}":`,i);return}this.currentTrackId=t,this.currentSound=s}}setVolume(t){this.volume=Math.max(0,Math.min(1,t)),this.currentSound&&this.setWebAudioVolume(this.currentSound,this.volume)}stop(){this.killCrossfadeTweens(),this.currentSound&&(this.currentSound.stop(),this.currentSound.destroy(),this.currentSound=null,this.currentTrackId=null)}destroy(){this.stop(),l.off(p.SET_MUSIC_VOLUME,this._onSetVolume,this)}isTrackLoaded(t){return this.scene.cache.audio.has(t)}createSound(t){try{return this.scene.sound.add(t,{loop:!0,volume:0})}catch(e){return console.warn(`[MusicController] Failed to create sound for key "${t}":`,e),null}}crossfade(t,e,s){this.killCrossfadeTweens(),this.setWebAudioVolume(e,0);try{e.play()}catch(a){console.warn("[MusicController] Failed to start new track during crossfade:",a);return}const i=this.scene.tweens.add({targets:t,volume:0,duration:s,ease:"Linear",onComplete:()=>{t.stop(),t.destroy()}}),o=this.scene.tweens.add({targets:e,volume:this.volume,duration:s,ease:"Linear"});this.crossfadeTweens=[i,o]}killCrossfadeTweens(){for(const t of this.crossfadeTweens)t.isPlaying()&&t.stop();this.crossfadeTweens=[]}setWebAudioVolume(t,e){t.volume=e}}class yt extends u.Scene{constructor(){super({key:"PreloadScene"})}preload(){const{width:t,height:e}=this.cameras.main;this.add.rectangle(t/2,e/2,400,30,3355443);const s=this.add.rectangle(t/2-198,e/2,4,26,4521864);this.add.text(t/2,e/2-40,"DaveAI TD2",{fontSize:"32px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(.5);const i=this.add.text(t/2,e/2+40,"0%",{fontSize:"16px",color:"#aaaaaa",fontFamily:"monospace"}).setOrigin(.5);this.load.on("progress",o=>{s.width=4+392*o,i.setText(`${Math.floor(o*100)}%`)}),this.loadUIAssets(),this.loadVFXAssets(),H.preload(this),he.preload(this),this.loadMusicTracks(),_.load()}create(){this.scene.start("MainMenuScene")}loadUIAssets(){this.load.image("world-01-bg","assets/tilesets/world-01/layers/main_bg.png?v=20260731td2assets2");for(let t=1;t<=10;t++)this.load.image(`road-tile-${t}`,`assets/tilesets/world-01/layers/road_${t}.png?v=20260731td2assets2`);this.load.image("hud-heart","assets/ui/td-gui/interface_game/heart.png?v=20260731td2assets2"),this.load.image("hud-star","assets/ui/td-gui/interface_game/star.png?v=20260731td2assets2"),this.load.image("hud-bar-bg","assets/ui/td-gui/interface_game/bg_bar.png?v=20260731td2assets2");for(let t=1;t<=4;t++)this.load.image(`hud-bar-${t}`,`assets/ui/td-gui/interface_game/bar_${t}.png?v=20260731td2assets2`);for(let t=1;t<=8;t++)this.load.image(`hud-ico-${t}`,`assets/ui/td-gui/interface_game/ico_${t}.png?v=20260731td2assets2`);for(let t=1;t<=7;t++){const e=String(t).padStart(2,"0");this.load.image(`health-bar-${t}`,`assets/ui/health-bars/health_bar-${e}.png?v=20260731td2assets2`)}}loadMusicTracks(){for(const[t,e]of Object.entries(mt))this.load.audio(`music-${t}`,e)}loadVFXAssets(){const t=[{key:"vfx-damage",folder:"damage",prefix:"1_effect_damage_",count:10,fileStart:0},{key:"vfx-fire",folder:"fire",prefix:"1_effect_fire_",count:19,fileStart:0},{key:"vfx-freeze",folder:"freeze",prefix:"1_effect_freeze_",count:16,fileStart:0},{key:"vfx-lightning",folder:"zip",prefix:"1_effect_zip_",count:14,fileStart:0},{key:"vfx-poison",folder:"rain",prefix:"1_effect_rain_",count:39,fileStart:1},{key:"vfx-stone",folder:"stone",prefix:"1_effect_stone_",count:18,fileStart:0},{key:"vfx-time",folder:"time",prefix:"1_effect_time_",count:14,fileStart:0},{key:"vfx-defense",folder:"def",prefix:"1_effect_def_",count:10,fileStart:0}];for(const e of t)for(let s=0;s{this.scene.scene.start(t,e)})}addScene(t,e){this.scene.scene.launch(t,e)}stopScene(t){this.scene.scene.stop(t)}}class wt extends u.Scene{constructor(){super({key:"MainMenuScene"});r(this,"sceneManager")}create(){this.sceneManager=new ee(this);const{width:e,height:s}=this.cameras.main;this.add.rectangle(e/2,s/2,e,s,1710638),this.add.text(e/2,s*.2,"DAVEAI TD2",{fontSize:"64px",color:"#ffdd44",fontFamily:"sans-serif",fontStyle:"bold",stroke:"#000000",strokeThickness:6}).setOrigin(.5),this.add.text(e/2,s*.3,"Tower Defense",{fontSize:"28px",color:"#aaaaff",fontFamily:"sans-serif"}).setOrigin(.5),this.createButton(e/2,s*.5,"PLAY",()=>{this.sceneManager.transitionTo("WorldSelectScene")}),this.createButton(e/2,s*.62,"ENDLESS MODE",()=>{this.sceneManager.transitionTo("EndlessScene",{worldId:7})}),this.createButton(e/2,s*.74,"SETTINGS",()=>{this.scene.launch("SettingsScene")}),this.cameras.main.fadeIn(300,0,0,0)}createButton(e,s,i,o){const a=this.add.rectangle(e,s,280,52,2250154,1).setInteractive({useHandCursor:!0});this.add.text(e,s,i,{fontSize:"22px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(.5),a.on("pointerover",()=>{a.setFillStyle(3373004)}),a.on("pointerout",()=>{a.setFillStyle(2250154)}),a.on("pointerup",o)}}const vt=[{id:1,key:"world-01",name:"W1: Cursed Forest",color:2250018,lockedColor:3355443},{id:2,key:"world-02",name:"W2: Dusty Badlands",color:6702114,lockedColor:3355443},{id:3,key:"world-03",name:"W3: Frozen Wastes",color:2245734,lockedColor:3355443},{id:4,key:"world-04",name:"W4: Volcanic Depths",color:6693410,lockedColor:3355443},{id:5,key:"world-05",name:"W5: Haunted Marshes",color:3359795,lockedColor:3355443},{id:6,key:"world-06",name:"W6: The Void Gate",color:2236996,lockedColor:3355443},{id:7,key:"world-07",name:"Endless Arena",color:5588002,lockedColor:3355443},{id:8,key:"world-08",name:"W8: Graveyard March",color:3351091,lockedColor:3355443},{id:9,key:"world-09",name:"W9: Sunken Depths",color:2245717,lockedColor:3355443},{id:10,key:"world-10",name:"W10: Steel Fortress",color:4473924,lockedColor:3355443},{id:11,key:"world-11",name:"W11: The Vault",color:4465220,lockedColor:3355443},{id:12,key:"world-12",name:"W12: Rampart Breach",color:5588019,lockedColor:3355443},{id:13,key:"world-13",name:"W13: Dark Tunnels",color:3351091,lockedColor:3355443},{id:14,key:"world-14",name:"W14: Crystal Caverns",color:2245734,lockedColor:3355443},{id:15,key:"world-15",name:"W15: Storm Summit",color:3359829,lockedColor:3355443},{id:16,key:"world-16",name:"W16: Infernal Gates",color:5579281,lockedColor:3355443},{id:17,key:"world-17",name:"W17: Arcane Sanctum",color:3351108,lockedColor:3355443},{id:18,key:"world-18",name:"W18: Cursed Sands",color:4469538,lockedColor:3355443},{id:19,key:"world-19",name:"W19: Nether Gate",color:2232627,lockedColor:3355443},{id:20,key:"world-20",name:"W20: The Fracture",color:4469606,lockedColor:3355443},{id:21,key:"world-21",name:"W21: Jungle Ruins",color:2245666,lockedColor:3355443},{id:22,key:"world-22",name:"W22: Ancient Temple",color:4469521,lockedColor:3355443},{id:23,key:"world-23",name:"W23: Phantom Spire",color:2241348,lockedColor:3355443},{id:24,key:"world-24",name:"W24: Time Vortex",color:2245700,lockedColor:3355443},{id:25,key:"world-25",name:"W25: Sunken Caves",color:1127270,lockedColor:3355443},{id:26,key:"world-26",name:"W26: Deep Abyss",color:1122884,lockedColor:3355443},{id:27,key:"world-27",name:"W27: Gravity's End",color:2232644,lockedColor:3355443},{id:28,key:"world-28",name:"W28: Chaos Nexus",color:4465169,lockedColor:3355443},{id:29,key:"world-29",name:"W29: Abyssal Rift",color:1114146,lockedColor:3355443},{id:30,key:"world-30",name:"W30: Surface Breach",color:2241365,lockedColor:3355443}];class Et extends u.Scene{constructor(){super({key:"WorldSelectScene"});r(this,"sceneManager")}create(){this.sceneManager=new ee(this);const{width:e,height:s}=this.cameras.main;this.add.rectangle(e/2,s/2,e,s,1118498),this.add.text(e/2,32,"SELECT WORLD",{fontSize:"30px",color:"#ffdd44",fontFamily:"sans-serif",fontStyle:"bold"}).setOrigin(.5);const i=this.add.text(55,32,"<- BACK",{fontSize:"18px",color:"#aaaaaa",fontFamily:"sans-serif"}).setOrigin(.5).setInteractive({useHandCursor:!0});i.on("pointerover",()=>i.setColor("#ffffff")),i.on("pointerout",()=>i.setColor("#aaaaaa")),i.on("pointerup",()=>this.sceneManager.transitionTo("MainMenuScene"));const o=6,a=185,n=88,c=12,m=12,f=o*(a+c)-c,g=(e-f)/2+a/2,y=80;vt.forEach((w,E)=>{const T=E%o,x=Math.floor(E/o),S=g+T*(a+c),R=y+x*(n+m),D=_.isWorldUnlocked(w.key),P=D?w.color:w.lockedColor,C=this.add.rectangle(S,R,a,n,P).setInteractive({useHandCursor:D}).setStrokeStyle(2,D?8947967:4473924);this.add.text(S,R-14,w.name,{fontSize:"13px",color:D?"#ffffff":"#666666",fontFamily:"sans-serif",fontStyle:"bold",wordWrap:{width:a-12},align:"center"}).setOrigin(.5),D?this.add.text(S,R+20,`World ${w.id}`,{fontSize:"11px",color:"#aaaacc",fontFamily:"monospace"}).setOrigin(.5):this.add.text(S,R+18,"🔒 LOCKED",{fontSize:"12px",color:"#666666",fontFamily:"sans-serif"}).setOrigin(.5),D&&(C.on("pointerover",()=>{C.setStrokeStyle(3,16768324),C.setFillStyle(u.Display.Color.ValueToColor(P).brighten(20).color)}),C.on("pointerout",()=>{C.setStrokeStyle(2,8947967),C.setFillStyle(P)}),C.on("pointerup",()=>{w.id===7?this.sceneManager.transitionTo("EndlessScene",{worldId:7}):this.sceneManager.transitionTo("LevelSelectScene",{worldId:w.id,worldKey:w.key})}))}),this.cameras.main.fadeIn(300,0,0,0)}}class St extends u.Scene{constructor(){super({key:"LevelSelectScene"});r(this,"sceneManager")}create(e){this.sceneManager=new ee(this);const{width:s,height:i}=this.cameras.main,{worldId:o,worldKey:a}=e;this.add.rectangle(s/2,i/2,s,i,1118498),this.add.text(s/2,40,`WORLD ${o} — SELECT LEVEL`,{fontSize:"32px",color:"#ffdd44",fontFamily:"sans-serif"}).setOrigin(.5),this.add.text(60,40,"<- BACK",{fontSize:"20px",color:"#aaaaaa",fontFamily:"sans-serif"}).setOrigin(.5).setInteractive({useHandCursor:!0}).on("pointerup",()=>this.sceneManager.transitionTo("WorldSelectScene"));const c=6,m=s/2-350,f=i/2;for(let g=1;g<=c;g++){const y=g===6,w=y?`${a}-boss`:`${a}-level-0${g}`,E=_.getLevelData(a,w),T=m+(g-1)*140,x=y?8921634:E.completed?2245666:2236996,S=this.add.rectangle(T,f,120,120,x).setInteractive({useHandCursor:!0}).setStrokeStyle(2,8947967),R=y?"BOSS":`LVL ${g}`;this.add.text(T,f-20,R,{fontSize:"18px",color:"#ffffff",fontFamily:"sans-serif",fontStyle:"bold"}).setOrigin(.5);const D=E.stars,P=3-E.stars,C="*".repeat(D)+"-".repeat(P);this.add.text(T,f+20,C,{fontSize:"16px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5),S.on("pointerup",()=>{this.sceneManager.transitionTo("GameScene",{worldId:o,worldKey:a,levelId:w,isBoss:y})})}this.cameras.main.fadeIn(300,0,0,0)}}class bt{constructor(t,e,s){r(this,"scene");r(this,"waves");r(this,"pathPoints");r(this,"currentWaveIndex",-1);r(this,"spawnTimers",[]);r(this,"enemiesAlive",0);r(this,"enemiesSpawned",0);r(this,"totalEnemiesInWave",0);r(this,"isComplete",!1);this.scene=t,this.waves=e,this.pathPoints=s,l.on(p.ENEMY_DIED,this.onEnemyDied,this),l.on(p.ENEMY_REACHED_END,this.onEnemyDied,this)}get totalWaves(){return this.waves.length}get currentWave(){return this.currentWaveIndex+1}get isAllWavesComplete(){return this.isComplete}getState(){return{currentWave:this.currentWave,totalWaves:this.totalWaves,isActive:this.currentWaveIndex>=0&&!this.isComplete,enemiesAlive:this.enemiesAlive,enemiesSpawned:this.enemiesSpawned,totalEnemiesInWave:this.totalEnemiesInWave}}startNextWave(){if(this.currentWaveIndex>=this.waves.length-1)return this.isComplete=!0,l.emit(p.ALL_WAVES_COMPLETED),!1;this.currentWaveIndex++,this.clearTimers();const t=this.waves[this.currentWaveIndex];this.enemiesSpawned=0,this.enemiesAlive=0,this.totalEnemiesInWave=t.spawnGroups.reduce((s,i)=>s+i.count,0),l.emit(p.WAVE_STARTED,{wave:this.currentWave,total:this.totalWaves,enemyCount:this.totalEnemiesInWave});let e=0;return t.spawnGroups.forEach(s=>{e+=s.delay??0,this.scheduleGroup(s,e),e+=s.count*s.interval}),!0}scheduleGroup(t,e){for(let s=0;s{this.spawnEnemy(t.enemy)});this.spawnTimers.push(o)}}spawnEnemy(t){this.enemiesSpawned++,this.enemiesAlive++,l.emit(p.ENEMY_SPAWN_REQUEST,{enemyId:t,pathPoints:this.pathPoints,waveNumber:this.currentWave})}onEnemyDied(){this.enemiesAlive=Math.max(0,this.enemiesAlive-1),this.enemiesSpawned>=this.totalEnemiesInWave&&this.enemiesAlive===0&&this.onWaveComplete()}onWaveComplete(){const t=this.waves[this.currentWaveIndex];l.emit(p.WAVE_COMPLETED,{wave:this.currentWave,reward:t.reward})}pause(){this.spawnTimers.forEach(t=>t.paused=!0)}resume(){this.spawnTimers.forEach(t=>t.paused=!1)}clearTimers(){this.spawnTimers.forEach(t=>t.destroy()),this.spawnTimers=[]}destroy(){this.clearTimers(),l.off(p.ENEMY_DIED,this.onEnemyDied,this),l.off(p.ENEMY_REACHED_END,this.onEnemyDied,this)}}class X{static extractPathFromMap(t){const e=t.getObjectLayer("Path");if(!e)return console.warn('[PathFinder] No "Path" object layer found in map. Using fallback.'),X.getFallbackPath(t);const s=e.objects[0];if(!s)return X.getFallbackPath(t);const i=s.x??0,o=s.y??0,a=s.polyline??[];return a.length===0?X.getFallbackPath(t):a.map(n=>new u.Math.Vector2(i+n.x,o+n.y))}static getFallbackPath(t){const e=t.widthInPixels,s=t.heightInPixels,i=s/2;return[new u.Math.Vector2(-50,i),new u.Math.Vector2(e*.25,i),new u.Math.Vector2(e*.25,s*.2),new u.Math.Vector2(e*.5,s*.2),new u.Math.Vector2(e*.5,s*.8),new u.Math.Vector2(e*.75,s*.8),new u.Math.Vector2(e*.75,i),new u.Math.Vector2(e+50,i)]}static getPathLength(t){let e=0;for(let s=1;s=i){const c=(i-o)/n;return new u.Math.Vector2(u.Math.Linear(t[a-1].x,t[a].x,c),u.Math.Linear(t[a-1].y,t[a].y,c))}o+=n}return t[t.length-1].clone()}}const Tt=[{id:"archer",name:"Archer Tower",description:"Fast-firing ranged tower. Excellent against fast ground & flying enemies.",spriteKey:"tower-archer",frameRanges:{idle:[1,4],attack:[5,22],upgrade:[23,38],special:[39,76]},cost:100,targets:["ground","flying"],levels:[{level:1,damage:18,fireRate:1.2,range:180,projectile:"arrow",frameRange:[1,22]},{level:2,damage:32,fireRate:1.5,range:200,upgradeCost:120,projectile:"arrow",frameRange:[23,38]},{level:3,upgradeCost:150,paths:{marksman:{name:"Marksman",damage:80,fireRate:.8,range:260,projectile:"piercing-arrow",ability:"pierce",frameRange:[39,56]},"rapid-fire":{name:"Rapid Fire",damage:28,fireRate:3.5,range:180,projectile:"arrow",ability:"multi-shot",frameRange:[57,76]}}}]},{id:"magic",name:"Magic Tower",description:"Slow-firing but high damage. Effective against armored enemies. Can slow targets.",spriteKey:"tower-magic",frameRanges:{idle:[1,8],attack:[9,24],special:[25,32]},cost:150,targets:["ground","flying"],levels:[{level:1,damage:45,fireRate:.6,range:160,projectile:"magic-bolt",ignoresArmor:10,frameRange:[1,16]},{level:2,damage:80,fireRate:.75,range:180,upgradeCost:175,projectile:"magic-bolt",ignoresArmor:18,effect:"slow-20",frameRange:[17,24]},{level:3,upgradeCost:200,paths:{"arcane-storm":{name:"Arcane Storm",damage:120,fireRate:.5,range:200,projectile:"arcane-storm",ability:"chain-lightning",ignoresArmor:30,frameRange:[25,28]},"frost-mage":{name:"Frost Mage",damage:60,fireRate:.7,range:190,projectile:"frost-bolt",ability:"freeze",effect:"freeze-2s",ignoresArmor:20,frameRange:[29,32]}}}]},{id:"stone",name:"Stone Tower",description:"Heavy area damage. Crushes groups with boulders. Best against large crowds.",spriteKey:"tower-stone",frameRanges:{idle:[1,10],attack:[11,42],special:[43,61]},cost:125,targets:["ground"],levels:[{level:1,damage:35,fireRate:.5,range:140,projectile:"boulder",splashRadius:60,frameRange:[1,28]},{level:2,damage:60,fireRate:.6,range:155,upgradeCost:140,projectile:"boulder",splashRadius:80,effect:"stun-0.5",frameRange:[29,42]},{level:3,upgradeCost:175,paths:{catapult:{name:"Siege Catapult",damage:120,fireRate:.4,range:220,projectile:"catapult-shot",splashRadius:120,ability:"barrage",frameRange:[43,52]},crusher:{name:"Earthquake",damage:80,fireRate:.5,range:140,projectile:"ground-slam",splashRadius:100,ability:"earthquake",effect:"slow-40",frameRange:[53,61]}}}]},{id:"support",name:"Support Tower",description:"Buffs adjacent towers. Provides utility: speed boost, gold income, healing.",spriteKey:"tower-support",frameRanges:{idle:[1,6],active:[7,14],special:[15,18]},cost:80,targets:[],levels:[{level:1,auraRadius:120,effect:"tower-speed-boost-15",frameRange:[1,9]},{level:2,auraRadius:150,upgradeCost:100,effect:"tower-speed-boost-25",bonus:"gold-per-wave-5",frameRange:[10,14]},{level:3,upgradeCost:130,paths:{"battle-drum":{name:"Battle Drum",auraRadius:200,effect:"tower-damage-boost-30",ability:"rage-burst",frameRange:[15,16]},"gold-vault":{name:"Gold Vault",auraRadius:150,effect:"tower-speed-boost-20",ability:"interest-income",bonus:"gold-per-wave-20",frameRange:[17,18]}}}]}],Be={towers:Tt},At=16737792,Ue=16777028;class be extends u.GameObjects.Arc{constructor(e){super(e,0,0,6,0,360,!1,Ue,1);r(this,"_damage",0);r(this,"_speed",280);r(this,"_target");r(this,"_splashRadius",0);r(this,"_ignoresArmor",0);r(this,"_piercing",!1);r(this,"_pierceCount",0);r(this,"_detonated",!1);this.setDepth(5),e.add.existing(this),this.setActive(!1).setVisible(!1)}fire(e,s,i,o,a=0,n=0,c=!1){this.x=e.x,this.y=e.y,this._damage=i,this._speed=o,this._target=s,this._splashRadius=a,this._ignoresArmor=n,this._piercing=c,this._pierceCount=0,this._detonated=!1;const m=a>0?At:Ue;this.setFillStyle(m);const f=a>0?8:5;this.setRadius(f),this.setActive(!0).setVisible(!0)}update(e){if(!this.active||this._detonated)return;if(!this._target||!this._target.active||this._target.hp<=0){this.despawn();return}const s=this._target.x,i=this._target.y,o=s-this.x,a=i-this.y,n=Math.sqrt(o*o+a*a),c=this._speed*e/1e3;n<=c+4?(this.x=s,this.y=i,this.detonate()):(this.x+=o/n*c,this.y+=a/n*c)}onHit(e){}onDetonate(e,s,i){}detonate(){this._detonated||(this._splashRadius>0?(this._detonated=!0,this.onDetonate(this.x,this.y,this._splashRadius),this.dealSplashDamage(),this.despawn()):this._piercing&&this._pierceCount===0?(this.dealDirectDamage(this._target),this._pierceCount++,this._detonated=!0,this.despawn()):(this._detonated=!0,this.dealDirectDamage(this._target),this.despawn()))}dealDirectDamage(e){!e.active||e.hp<=0||(e.takeDamage(this._damage,this._ignoresArmor),this.onHit(e))}dealSplashDamage(){l.emit(p.PROJECTILE_SPLASH,{x:this.x,y:this.y,radius:this._splashRadius,damage:this._damage,ignoresArmor:this._ignoresArmor})}despawn(){this.setActive(!1).setVisible(!1),this._target=void 0}}const Dt={archer:5614165,magic:5592575,stone:8947848,support:16755200},xt=new Map(Be.towers.map(d=>[d.id,d]));class oe extends u.GameObjects.Container{constructor(e,s,i,o){super(e,s,i);r(this,"towerId");r(this,"towerData");r(this,"currentLevel",1);r(this,"currentPath",null);r(this,"totalInvestment",0);r(this,"bodyRect");r(this,"bodySprite");r(this,"rangeCircle");r(this,"levelBadge");r(this,"cooldownElapsed",0);r(this,"projectiles",[]);this.towerId=o;const a=xt.get(o);if(!a)throw new Error(`[TowerSprite] Unknown tower id: "${o}"`);this.towerData=a,this.totalInvestment=a.cost,this.buildVisuals(),e.add.existing(this)}getRange(){return this.resolveStats().range}getDamage(){return this.resolveStats().damage}getFireRate(){return this.resolveStats().fireRate}getTowerId(){return this.towerId}getLevel(){return this.currentLevel}getPath(){return this.currentPath}getTotalInvestment(){return this.totalInvestment}showRange(){this.rangeCircle.setVisible(!0);const e=this.resolveStats().range;this.rangeCircle.setRadius(e)}hideRange(){this.rangeCircle.setVisible(!1)}upgrade(e){var a;if(this.currentLevel>=3)return!1;const s=this.currentLevel,i=this.towerData.levels[s];if(!i)return!1;if(this.currentLevel===2){if(!e||!((a=i.paths)!=null&&a[e]))return!1;this.currentPath=e}const o=i.upgradeCost??0;return this.totalInvestment+=o,this.currentLevel++,this.updateVisuals(),!0}getUpgradeCost(e){if(this.currentLevel>=3)return 0;const s=this.currentLevel,i=this.towerData.levels[s];return i?i.upgradeCost??0:0}getSellValue(){return Math.floor(this.totalInvestment*.6)}sell(){const e=Math.floor(this.totalInvestment*.6);return this.destroy(),e}update(e,s,i){if(this.towerData.targets.length===0)return;const o=this.resolveStats(),a=1e3/Math.max(o.fireRate,.01);if(this.cooldownElapsed+=s,this.cooldownElapsedc.active?!0:(c.destroy(),!1)))}destroy(e){this.projectiles.forEach(s=>{s.active&&s.destroy()}),this.projectiles=[],super.destroy(e)}buildVisuals(){const e=Dt[this.towerId]??13421772,s=this.resolveStats().range;this.rangeCircle=this.scene.add.arc(0,0,s,0,360,!1,16777215,.15),this.rangeCircle.setStrokeStyle(1,16777215,.4),this.rangeCircle.setVisible(!1),this.add(this.rangeCircle),this.bodyRect=this.scene.add.rectangle(0,0,48,48,e),this.add(this.bodyRect);const i=he.getFirstFrameKey(this.towerId),o=this.scene.textures.exists(i)?i:"__DEFAULT";this.bodySprite=this.scene.add.sprite(0,0,o),this.bodySprite.setDisplaySize(48,48),this.add(this.bodySprite),this.bodyRect.setVisible(!1);const a=he.getIdleAnimKey(this.towerId);this.scene.anims.exists(a)&&this.bodySprite.play(a),this.levelBadge=this.scene.add.text(16,-20,"L1",{fontSize:"10px",color:"#ffffff",backgroundColor:"#000000",padding:{x:2,y:1},fontFamily:"monospace"}).setOrigin(.5,.5),this.add(this.levelBadge),this.setSize(48,48),this.setInteractive()}updateVisuals(){const e=`L${this.currentLevel}`;this.levelBadge.setText(e);const s=this.resolveStats().range;this.rangeCircle.setRadius(s)}resolveStats(){var i;const e=this.towerData.levels;if(this.currentLevel<3){const o=e[this.currentLevel-1]??e[0];return{damage:o.damage??0,fireRate:o.fireRate??0,range:o.range??0,ignoresArmor:o.ignoresArmor??0,splashRadius:o.splashRadius??0,auraRadius:o.auraRadius??0}}const s=e[2];if(!s)return{damage:0,fireRate:0,range:0,ignoresArmor:0,splashRadius:0,auraRadius:0};if(this.currentPath&&((i=s.paths)!=null&&i[this.currentPath])){const o=s.paths[this.currentPath];return{damage:o.damage??0,fireRate:o.fireRate??0,range:o.range??0,ignoresArmor:o.ignoresArmor??0,splashRadius:o.splashRadius??0,auraRadius:o.auraRadius??0}}return{damage:0,fireRate:0,range:0,ignoresArmor:0,splashRadius:0,auraRadius:0}}findTarget(e,s){const i=s*s,o=this.x,a=this.y;let n=null,c=1/0;for(const m of e){if(!m.active||m.hp<=0)continue;const f=m.x-o,g=m.y-a,y=f*f+g*g;y<=i&&ya.destroy()});const n=new be(this.scene);n.fire(i,e,s.damage,320,s.splashRadius,s.ignoresArmor);const c=this.scene;c.projectileLayer&&c.projectileLayer.add(n),this.projectiles.push(n)}}class Pt extends oe{constructor(e,s,i){super(e,s,i,"archer");r(this,"prevCooldownElapsed",0)}update(e,s,i){const o=this.getFireRate(),a=o>0?1e3/o:1/0;if(this.prevCooldownElapsed+=s,super.update(e,s,i),this.prevCooldownElapsed>=a){this.prevCooldownElapsed=0;const n=this.getLevel(),c=this.getPath(),m=this.findNearestEnemy(i);if(!m)return;n===2?(this.fireFlanking(m,15),this.fireFlanking(m,-15)):n===3&&c==="marksman"&&this.firePiercing(m)}}findNearestEnemy(e){const s=this.getRange(),i=s*s;let o=null,a=1/0;for(const n of e){if(!n.active||n.hp<=0)continue;const c=n.x-this.x,m=n.y-this.y,f=c*c+m*m;f<=i&&f{const s=e.x-this.x,i=e.y-this.y,o=s*s+i*i,a=this.getRange()+e.radius;if(o>a*a)return;const n=this.getLevel(),c=this.getPath();n===3&&c==="frost-mage"?this.applyAoeStatus("freeze",0,2e3,e.x,e.y,e.radius):n>=2&&this.applyAoeStatus("slow",.4,2e3,e.x,e.y,e.radius)});l.on(p.PROJECTILE_SPLASH,this.onSplash,this)}update(e,s,i){this.lastEnemies=i;const o=this.getFireRate(),a=o>0?1e3/o:1/0;if(this.prevCooldownElapsed+=s,super.update(e,s,i),this.prevCooldownElapsed>=a){this.prevCooldownElapsed=0;const n=this.findNearestEnemy(i);if(!n)return;const c=this.getLevel(),m=this.getPath(),f=c===3&&m==="arcane-storm"?"arcane":"ice",g={x:n.x,y:n.y,element:f,radius:this.getRange()};l.emit(p.DAMAGE_DEALT,g),c===1&&this.applyStatusEffect(n,"slow",.4,2e3),c===2&&this.applyStatusEffect(n,"slow",.2,2e3)}}destroy(e){l.off(p.PROJECTILE_SPLASH,this.onSplash,this),super.destroy(e)}applyStatusEffect(e,s,i,o){l.emit("tower:status_effect",{target:e,effect:s,factor:i,durationMs:o})}applyAoeStatus(e,s,i,o,a,n){const c=n*n;for(const m of this.lastEnemies){if(!m.active||m.hp<=0)continue;const f=m.x-o,g=m.y-a;f*f+g*g<=c&&this.applyStatusEffect(m,e,s,i)}}findNearestEnemy(e){const s=this.getRange(),i=s*s;let o=null,a=1/0;for(const n of e){if(!n.active||n.hp<=0)continue;const c=n.x-this.x,m=n.y-this.y,f=c*c+m*m;f<=i&&f {const s=e.x-this.x,i=e.y-this.y,o=s*s+i*i,a=this.getRange()+e.radius;if(o>a*a)return;const n=this.getLevel(),c=this.getPath();if(n===3&&c==="crusher"){const m={x:e.x,y:e.y,element:"poison",radius:e.radius,lingersMs:3e3};l.emit(p.DAMAGE_DEALT,m),l.emit("tower:poison_cloud",{x:e.x,y:e.y,radius:e.radius,damage:e.damage*.25,durationMs:3e3})}else if(n===3&&c==="catapult")l.emit("tower:armored_bonus",{x:e.x,y:e.y,radius:e.radius,bonus:.5});else{const m={x:e.x,y:e.y,element:"stone",radius:e.radius};l.emit(p.DAMAGE_DEALT,m)}});l.on(p.PROJECTILE_SPLASH,this.onSplash,this)}update(e,s,i){const o=this.getFireRate(),a=o>0?1e3/o:1/0;if(this.prevCooldownElapsed+=s,super.update(e,s,i),this.prevCooldownElapsed>=a){this.prevCooldownElapsed=0;const n=this.getLevel(),c=this.getPath();if(n===3&&c==="catapult"){const m=this.findNearestEnemy(i);m&&l.emit("tower:barrage",{towerId:this.getTowerId(),towerX:this.x,towerY:this.y,targetX:m.x,targetY:m.y,damage:this.getDamage()})}}}destroy(e){l.off(p.PROJECTILE_SPLASH,this.onSplash,this),super.destroy(e)}findNearestEnemy(e){const s=this.getRange(),i=s*s;let o=null,a=1/0;for(const n of e){if(!n.active||n.hp<=0)continue;const c=n.x-this.x,m=n.y-this.y,f=c*c+m*m;f<=i&&f [d.id,d.cost]));class q{constructor(t){r(this,"scene");r(this,"towers",new Map);r(this,"pathCells",new Set);r(this,"selected",null);r(this,"onBgPointerDown");r(this,"slowedUntil",new Map);r(this,"frozenUntil",new Map);r(this,"onGorthakShockwave");r(this,"onZhalfrixFreeze");this.scene=t,this.buildPathCellSet(t.pathPoints),this.onBgPointerDown=e=>{this.deselectAll()},t.input.on("pointerdown",this.onBgPointerDown),this.onGorthakShockwave=this.handleGorthakShockwave.bind(this),this.onZhalfrixFreeze=this.handleZhalfrixFreeze.bind(this),this.setupListeners()}setupListeners(){l.on("boss:gorthak_shockwave",this.onGorthakShockwave),l.on("boss:zhalfrix_freeze",this.onZhalfrixFreeze)}handleGorthakShockwave(t){const{x:e,y:s,radius:i}=t,o=i*i,a=performance.now()+3e3;for(const n of this.towers.values()){const c=n.worldX-e,m=n.worldY-s;c*c+m*m<=o&&this.slowedUntil.set(n.id,a)}}handleZhalfrixFreeze(t){const{duration:e,x:s,y:i,radius:o}=t,a=performance.now()+e;for(const n of this.towers.values()){if(o!==void 0&&s!==void 0&&i!==void 0){const c=o*o,m=n.worldX-s,f=n.worldY-i;if(m*m+f*f>c)continue}this.frozenUntil.set(n.id,a)}}canPlaceTower(t,e){const s=O(t,e);return!this.pathCells.has(s)&&!this.towers.has(s)}static worldToGrid(t,e){return{gridX:Math.floor(t/j),gridY:Math.floor((e-Ke)/j)}}static gridToWorld(t,e){return{worldX:t*j+j/2,worldY:Ke+e*j+j/2}}placeTower(t,e,s){if(!this.canPlaceTower(e,s))return null;const i=It.get(t)??0;if(_.gold{g.event.stopPropagation(),this.selectTower(m)}),l.emit(p.GOLD_CHANGED,-i),l.emit(p.TOWER_PLACED,{towerId:t,gridX:e,gridY:s,worldX:o,worldY:a,cost:i}),m}removeTower(t,e){const s=O(t,e),i=this.towers.get(s);i&&(this.selected===i&&this.deselectAll(),i.tower.destroy(),this.towers.delete(s))}removeTowerBySprite(t){for(const[e,s]of this.towers.entries())if(s.tower===t){this.selected===s&&(this.selected=null),this.towers.delete(e);return}}selectTower(t){this.selected!==t&&(this.deselectAll(),this.selected=t,t.tower.showRange(),l.emit(p.TOWER_SELECTED,{placed:t,level:t.tower.getLevel(),path:t.tower.getPath(),stats:{damage:t.tower.getDamage(),fireRate:t.tower.getFireRate(),range:t.tower.getRange()}}))}deselectAll(){this.selected&&(this.selected.tower.hideRange(),this.selected=null,l.emit(p.TOWER_DESELECTED))}getSelected(){return this.selected}upgradeSelected(t){if(!this.selected)return!1;const e=this.selected,s=e.tower.getUpgradeCost(t);return _.gold0?this.pool.pop():this.create();return t.setActive(!0).setVisible(!0),this.active.add(t),t}release(t){this.active.has(t)&&(this.active.delete(t),t.setActive(!1).setVisible(!1),this.pool.push(t))}releaseAll(){this.active.forEach(t=>this.release(t))}get activeCount(){return this.active.size}get poolSize(){return this.pool.length}create(){const t=this.factory();return t.setActive(!1).setVisible(!1),t}destroy(){this.active.forEach(t=>t.destroy()),this.pool.forEach(t=>t.destroy()),this.active.clear(),this.pool=[]}}const Ct=[{id:"enemy-01",name:"Goblin Warrior",pack:"A",world:1,hp:80,speed:90,armor:0,reward:8,size:"small",type:"ground",abilities:[]},{id:"enemy-02",name:"Goblin Archer",pack:"A",world:1,hp:60,speed:110,armor:0,reward:9,size:"small",type:"ground",abilities:["ranged"]},{id:"enemy-03",name:"Orc Brute",pack:"A",world:1,hp:200,speed:60,armor:10,reward:14,size:"large",type:"ground",abilities:[]},{id:"enemy-04",name:"Troll Guard",pack:"A",world:1,hp:320,speed:50,armor:15,reward:18,size:"large",type:"ground",abilities:["regen"]},{id:"enemy-05",name:"Skeleton Archer",pack:"A",world:2,hp:90,speed:100,armor:5,reward:11,size:"small",type:"ground",abilities:["ranged","undead"]},{id:"enemy-06",name:"Skeleton Warrior",pack:"A",world:2,hp:140,speed:80,armor:8,reward:12,size:"medium",type:"ground",abilities:["undead"]},{id:"enemy-07",name:"Demon Scout",pack:"A",world:2,hp:110,speed:130,armor:0,reward:13,size:"small",type:"ground",abilities:["fast"]},{id:"enemy-08",name:"Dark Elf",pack:"A",world:2,hp:160,speed:110,armor:5,reward:16,size:"medium",type:"ground",abilities:["stealth"]},{id:"enemy-09",name:"Gargoyle",pack:"A",world:2,hp:130,speed:120,armor:0,reward:17,size:"medium",type:"flying",abilities:["flying"]},{id:"enemy-10",name:"Night Stalker",pack:"A",world:2,hp:220,speed:95,armor:12,reward:22,size:"large",type:"ground",abilities:["stealth","fast"]},{id:"enemy-11",name:"Forest Sprite",pack:"B",world:3,hp:100,speed:115,armor:0,reward:12,size:"small",type:"ground",abilities:["regen"]},{id:"enemy-12",name:"Cave Worm",pack:"B",world:3,hp:280,speed:45,armor:20,reward:18,size:"large",type:"ground",abilities:["burrow"]},{id:"enemy-13",name:"Crystal Golem",pack:"B",world:3,hp:400,speed:40,armor:25,reward:22,size:"large",type:"ground",abilities:["magic_resist"]},{id:"enemy-14",name:"Ice Troll",pack:"B",world:3,hp:350,speed:55,armor:18,reward:20,size:"large",type:"ground",abilities:["freeze_aura"]},{id:"enemy-15",name:"Fire Imp",pack:"B",world:4,hp:120,speed:140,armor:0,reward:14,size:"small",type:"flying",abilities:["flying","fire_aura"]},{id:"enemy-16",name:"Stone Colossus",pack:"B",world:4,hp:600,speed:35,armor:35,reward:30,size:"large",type:"ground",abilities:["immune_slow"]},{id:"enemy-17",name:"Shadow Beast",pack:"B",world:4,hp:180,speed:125,armor:5,reward:19,size:"medium",type:"ground",abilities:["stealth","fast"]},{id:"enemy-18",name:"Bog Walker",pack:"B",world:4,hp:250,speed:70,armor:10,reward:17,size:"medium",type:"ground",abilities:["poison_trail"]},{id:"enemy-19",name:"Iron Golem",pack:"B",world:4,hp:500,speed:40,armor:30,reward:28,size:"large",type:"ground",abilities:["immune_slow","immune_freeze"]},{id:"enemy-20",name:"Chaos Daemon",pack:"B",world:4,hp:300,speed:100,armor:15,reward:25,size:"medium",type:"ground",abilities:["magic_resist","flying"]},{id:"enemy-21",name:"Swamp Toad",pack:"C",world:5,hp:200,speed:80,armor:5,reward:15,size:"medium",type:"ground",abilities:["poison"]},{id:"enemy-22",name:"Vine Creeper",pack:"C",world:5,hp:160,speed:65,armor:0,reward:13,size:"medium",type:"ground",abilities:["slow_aura"]},{id:"enemy-23",name:"Sand Scorpion",pack:"C",world:5,hp:240,speed:90,armor:15,reward:18,size:"medium",type:"ground",abilities:["poison","burrow"]},{id:"enemy-24",name:"Frost Wolf",pack:"C",world:5,hp:180,speed:130,armor:5,reward:16,size:"medium",type:"ground",abilities:["freeze_aura","fast"]},{id:"enemy-25",name:"Lava Drake",pack:"C",world:5,hp:350,speed:75,armor:20,reward:22,size:"large",type:"flying",abilities:["flying","fire_aura"]},{id:"enemy-26",name:"Necro Knight",pack:"C",world:5,hp:300,speed:80,armor:20,reward:24,size:"large",type:"ground",abilities:["undead","regen"]},{id:"enemy-27",name:"Blood Bat",pack:"C",world:5,hp:140,speed:150,armor:0,reward:15,size:"small",type:"flying",abilities:["flying","fast","lifesteal"]},{id:"enemy-28",name:"Ancient Mummy",pack:"C",world:6,hp:420,speed:55,armor:25,reward:28,size:"large",type:"ground",abilities:["undead","immune_slow","curse"]},{id:"enemy-29",name:"Wyvern Rider",pack:"C",world:6,hp:280,speed:110,armor:10,reward:26,size:"large",type:"flying",abilities:["flying","ranged"]},{id:"enemy-30",name:"Cursed Knight",pack:"C",world:6,hp:380,speed:85,armor:22,reward:28,size:"large",type:"ground",abilities:["undead","curse","regen"]},{id:"enemy-31",name:"Rat Raider",pack:"D",world:1,hp:50,speed:160,armor:0,reward:7,size:"tiny",type:"ground",abilities:["fast","swarm"]},{id:"enemy-32",name:"Crystal Spider",pack:"D",world:2,hp:110,speed:105,armor:10,reward:13,size:"small",type:"ground",abilities:["web_slow"]},{id:"enemy-33",name:"Mud Golem",pack:"D",world:3,hp:360,speed:45,armor:20,reward:20,size:"large",type:"ground",abilities:["immune_slow","regen"]},{id:"enemy-34",name:"Storm Hawk",pack:"D",world:4,hp:190,speed:145,armor:0,reward:19,size:"medium",type:"flying",abilities:["flying","fast"]},{id:"enemy-35",name:"Void Serpent",pack:"D",world:6,hp:340,speed:90,armor:15,reward:27,size:"large",type:"ground",abilities:["magic_resist","poison"]},{id:"enemy-36",name:"Chaos Mage",pack:"D",world:6,hp:200,speed:95,armor:5,reward:25,size:"medium",type:"ground",abilities:["magic_resist","spell_shield"]},{id:"enemy-37",name:"Rune Guard",pack:"D",world:6,hp:460,speed:60,armor:28,reward:30,size:"large",type:"ground",abilities:["spell_shield","immune_slow"]},{id:"enemy-38",name:"Abyssal Fiend",pack:"D",world:6,hp:280,speed:115,armor:10,reward:26,size:"medium",type:"ground",abilities:["stealth","teleport"]},{id:"enemy-39",name:"Warp Demon",pack:"D",world:6,hp:320,speed:120,armor:8,reward:28,size:"medium",type:"ground",abilities:["teleport","magic_resist"]},{id:"enemy-40",name:"Eldritch Horror",pack:"D",world:6,hp:700,speed:65,armor:30,reward:40,size:"large",type:"ground",abilities:["immune_slow","magic_resist","curse","regen"]},{id:"enemy-41",name:"Void Crawler",pack:"E",world:8,hp:150,speed:145,armor:0,reward:16,size:"small",type:"ground",abilities:["fast","undead","swarm"]},{id:"enemy-42",name:"Bone Reaper",pack:"E",world:8,hp:380,speed:70,armor:28,reward:22,size:"large",type:"ground",abilities:["undead","armor"]},{id:"enemy-43",name:"Plague Walker",pack:"E",world:8,hp:220,speed:75,armor:5,reward:20,size:"medium",type:"ground",abilities:["undead","heal_nearby"]},{id:"enemy-44",name:"Rot Hulk",pack:"E",world:8,hp:900,speed:40,armor:35,reward:35,size:"large",type:"ground",abilities:["undead","immune_slow"]},{id:"enemy-45",name:"Death Wisp",pack:"E",world:8,hp:160,speed:135,armor:0,reward:18,size:"small",type:"flying",abilities:["flying","undead","bypass_ground"]},{id:"enemy-46",name:"Grave Titan",pack:"E",world:9,hp:1200,speed:50,armor:40,reward:45,size:"large",type:"ground",abilities:["undead","boss_hp","regen"]},{id:"enemy-47",name:"Crypt Fiend",pack:"E",world:9,hp:280,speed:90,armor:10,reward:24,size:"medium",type:"ground",abilities:["undead","burrow","immune_while_burrowed"]},{id:"enemy-48",name:"Soul Leech",pack:"E",world:9,hp:240,speed:100,armor:5,reward:22,size:"medium",type:"ground",abilities:["undead","drain_tower"]},{id:"enemy-49",name:"Lich Knight",pack:"E",world:10,hp:450,speed:80,armor:20,reward:32,size:"large",type:"ground",abilities:["undead","revive","magic_resist"]},{id:"enemy-50",name:"Abomination",pack:"E",world:10,hp:600,speed:60,armor:25,reward:38,size:"large",type:"ground",abilities:["undead","split_on_death"]},{id:"enemy-51",name:"Feral Beast",pack:"F",world:9,hp:300,speed:125,armor:5,reward:22,size:"medium",type:"ground",abilities:["fast","rage"]},{id:"enemy-52",name:"Stone Goliath",pack:"F",world:9,hp:700,speed:40,armor:40,reward:35,size:"large",type:"ground",abilities:["immune_slow","immune_freeze"]},{id:"enemy-53",name:"Tide Runner",pack:"F",world:9,hp:200,speed:150,armor:0,reward:19,size:"small",type:"ground",abilities:["fast","amphibious"]},{id:"enemy-54",name:"Coral Serpent",pack:"F",world:9,hp:350,speed:85,armor:15,reward:25,size:"large",type:"ground",abilities:["poison","amphibious"]},{id:"enemy-55",name:"Mire Drake",pack:"F",world:10,hp:400,speed:90,armor:20,reward:28,size:"large",type:"flying",abilities:["flying","poison","fire_aura"]},{id:"enemy-56",name:"Furnace Guard",pack:"F",world:10,hp:550,speed:60,armor:30,reward:32,size:"large",type:"ground",abilities:["immune_freeze","armor","fire_resist"]},{id:"enemy-57",name:"Forge Titan",pack:"F",world:10,hp:850,speed:45,armor:45,reward:40,size:"large",type:"ground",abilities:["immune_slow","immune_freeze","shield"]},{id:"enemy-58",name:"Circuit Fiend",pack:"F",world:10,hp:350,speed:110,armor:10,reward:27,size:"medium",type:"ground",abilities:["electric","drain_tower","fast"]},{id:"enemy-59",name:"Entropy Wraith",pack:"F",world:11,hp:500,speed:100,armor:15,reward:36,size:"medium",type:"flying",abilities:["flying","magic_resist","phase"]},{id:"enemy-60",name:"Nexus Horror",pack:"F",world:11,hp:1e3,speed:70,armor:35,reward:55,size:"large",type:"ground",abilities:["magic_resist","immune_slow","regen","curse"]},{id:"enemy-61",name:"Shambling Zombie",pack:"G",world:8,hp:500,speed:40,armor:5,reward:14,size:"large",type:"ground",abilities:["undead","slow","very_high_hp"]},{id:"enemy-62",name:"Mummy Raider",pack:"G",world:8,hp:260,speed:75,armor:15,reward:20,size:"medium",type:"ground",abilities:["undead","curse","debuff_tower"]},{id:"enemy-63",name:"Ghoul Sprinter",pack:"G",world:8,hp:160,speed:155,armor:0,reward:16,size:"small",type:"ground",abilities:["undead","fast","swarm"]},{id:"enemy-64",name:"Wraith Mummy",pack:"G",world:8,hp:220,speed:120,armor:0,reward:22,size:"medium",type:"flying",abilities:["undead","flying","bypass_ground","curse"]},{id:"enemy-65",name:"Deep Lurker",pack:"H",world:9,hp:480,speed:65,armor:35,reward:28,size:"large",type:"ground",abilities:["armored","amphibious"]},{id:"enemy-66",name:"Tidal Spawn",pack:"H",world:9,hp:120,speed:150,armor:0,reward:14,size:"small",type:"ground",abilities:["fast","swarm","amphibious"]},{id:"enemy-67",name:"Abyssal Crawler",pack:"H",world:9,hp:1100,speed:35,armor:50,reward:48,size:"large",type:"ground",abilities:["immune_slow","armored","amphibious","massive_hp"]},{id:"enemy-68",name:"Brine Specter",pack:"H",world:9,hp:280,speed:130,armor:0,reward:26,size:"medium",type:"flying",abilities:["flying","amphibious","stealth","phase"]},{id:"enemy-69",name:"Kraken Hatchling",pack:"H",world:14,hp:600,speed:80,armor:20,reward:35,size:"large",type:"ground",abilities:["amphibious","regen","poison_trail"]},{id:"enemy-70",name:"Leviathan Shard",pack:"H",world:14,hp:1400,speed:50,armor:55,reward:60,size:"large",type:"ground",abilities:["amphibious","immune_slow","immune_freeze","massive_hp","regen"]},{id:"enemy-i01",name:"Star Shard",pack:"I",world:16,hp:120,speed:160,armor:0,reward:15,size:"small",type:"flying",abilities:["flying","fast","cosmic"]},{id:"enemy-i02",name:"Void Drone",pack:"I",world:16,hp:200,speed:130,armor:5,reward:20,size:"small",type:"flying",abilities:["flying","cosmic","electric"]},{id:"enemy-i03",name:"Nebula Crawler",pack:"I",world:16,hp:350,speed:95,armor:10,reward:26,size:"medium",type:"flying",abilities:["flying","cosmic","magic_resist"]},{id:"enemy-i04",name:"Orbital Sentinel",pack:"I",world:16,hp:480,speed:80,armor:20,reward:32,size:"medium",type:"flying",abilities:["flying","cosmic","shield","electric"]},{id:"enemy-i05",name:"Pulsar Wraith",pack:"I",world:16,hp:300,speed:120,armor:0,reward:28,size:"medium",type:"flying",abilities:["flying","cosmic","phase","stealth"]},{id:"enemy-i06",name:"Solar Construct",pack:"I",world:16,hp:700,speed:60,armor:35,reward:40,size:"large",type:"flying",abilities:["flying","cosmic","immune_freeze","fire_aura"]},{id:"enemy-i07",name:"Astral Titan",pack:"I",world:16,hp:1100,speed:45,armor:45,reward:52,size:"large",type:"flying",abilities:["flying","cosmic","immune_slow","magic_resist","boss_hp"]},{id:"enemy-i08",name:"Celestial Destroyer",pack:"I",world:16,hp:1600,speed:55,armor:50,reward:65,size:"large",type:"flying",abilities:["flying","cosmic","immune_slow","immune_freeze","magic_resist","regen"]},{id:"enemy-j01",name:"Scarab Scout",pack:"J",world:18,hp:80,speed:180,armor:0,reward:8,size:"tiny",type:"ground",abilities:["fast","swarm","burrow"]},{id:"enemy-j02",name:"Sand Raider",pack:"J",world:18,hp:200,speed:120,armor:5,reward:16,size:"small",type:"ground",abilities:["fast","desert"]},{id:"enemy-j03",name:"Dune Stalker",pack:"J",world:18,hp:340,speed:95,armor:12,reward:22,size:"medium",type:"ground",abilities:["desert","burrow","stealth"]},{id:"enemy-j04",name:"Arena Beast",pack:"J",world:18,hp:520,speed:80,armor:18,reward:30,size:"large",type:"ground",abilities:["desert","rage","immune_slow"]},{id:"enemy-j05",name:"Gladiator Construct",pack:"J",world:18,hp:450,speed:90,armor:25,reward:32,size:"large",type:"ground",abilities:["desert","armor","immune_freeze"]},{id:"enemy-j06",name:"Sandstorm Elemental",pack:"J",world:18,hp:600,speed:100,armor:15,reward:38,size:"large",type:"flying",abilities:["flying","desert","slow_aura","immune_slow"]},{id:"enemy-j07",name:"Ancient Guardian",pack:"J",world:19,hp:900,speed:65,armor:40,reward:48,size:"large",type:"ground",abilities:["desert","immune_slow","immune_freeze","regen","armor"]},{id:"enemy-j08",name:"Colosseum Champion",pack:"J",world:19,hp:1300,speed:55,armor:50,reward:58,size:"large",type:"ground",abilities:["desert","immune_slow","immune_freeze","immune_poison","magic_resist","boss_hp"]},{id:"enemy-k01",name:"Cave Crawler",pack:"K",world:21,hp:280,speed:110,armor:10,reward:18,size:"small",type:"ground",abilities:["fast","swarm","wall_cling"]},{id:"enemy-k02",name:"Rock Golem",pack:"K",world:21,hp:1800,speed:30,armor:60,reward:90,size:"large",type:"ground",abilities:["armored","immune_slow","immune_freeze"]},{id:"enemy-k03",name:"Blind Bat",pack:"K",world:21,hp:160,speed:130,armor:5,reward:14,size:"small",type:"flying",abilities:["flying","erratic","fast"]},{id:"enemy-k04",name:"Spore Walker",pack:"K",world:22,hp:400,speed:70,armor:15,reward:28,size:"medium",type:"ground",abilities:["spore_trail","poison_aura"]},{id:"enemy-k05",name:"Deep Lurker",pack:"K",world:23,hp:900,speed:55,armor:30,reward:55,size:"large",type:"ground",abilities:["burrow","stealth_emerge"]},{id:"enemy-k06",name:"Crystal Scorpion",pack:"K",world:24,hp:650,speed:75,armor:45,reward:42,size:"large",type:"ground",abilities:["armored","immune_slow","armor_break_on_death"]},{id:"enemy-k07",name:"Lava Toad",pack:"K",world:25,hp:500,speed:80,armor:20,reward:35,size:"medium",type:"ground",abilities:["fire_immune","splash_death","immune_burn"]},{id:"enemy-k08",name:"Void Worm",pack:"K",world:26,hp:1600,speed:45,armor:55,reward:85,size:"large",type:"ground",abilities:["phase_shift","elite","regen"]}],Mt={enemies:Ct},Ye={tiny:{w:16,h:16,color:16720418,yOffset:0},small:{w:20,h:20,color:16729088,yOffset:0},medium:{w:24,h:24,color:16746496,yOffset:0},large:{w:32,h:32,color:13386752,yOffset:0},flying:{w:20,h:20,color:16776960,yOffset:-20},boss:{w:48,h:48,color:8913032,yOffset:0}},Bt=32;class W extends u.GameObjects.Container{constructor(e,s,i){const o=i[0]??new u.Math.Vector2(0,0);super(e,o.x,o.y);r(this,"enemyId");r(this,"poolKey");r(this,"hp");r(this,"maxHp");r(this,"speed");r(this,"armor");r(this,"reward");r(this,"type");r(this,"pathPoints");r(this,"currentSegment");r(this,"distanceOnSegment");r(this,"totalProgress");r(this,"position");r(this,"slowFactor");r(this,"slowSources",[]);r(this,"frozenTime");r(this,"stunTime");r(this,"burnDps");r(this,"poisonDps");r(this,"poisonTime");r(this,"healthBarGfx");r(this,"bodyRect");r(this,"bodySprite");r(this,"spriteTypeIndex");r(this,"totalPathLength");r(this,"sizeCfg");r(this,"_released");r(this,"spawnPoint");r(this,"slowImmune",!1);r(this,"skipPathMovement",!1);this.enemyId=s,this.poolKey=s;const a=Mt.enemies.find(E=>E.id===s);if(!a)throw new Error(`[EnemyBase] Unknown enemyId: "${s}"`);this.maxHp=a.hp,this.hp=a.hp,this.speed=a.speed,this.armor=a.armor,this.reward=a.reward,this.type=a.type==="flying"?"flying":"ground";const n=this.type==="flying"?"flying":a.size??"medium";this.sizeCfg=Ye[n]??Ye.medium,this.pathPoints=i,this.currentSegment=0,this.distanceOnSegment=0,this.totalProgress=0,this.position=new u.Math.Vector2(o.x,o.y),this.spawnPoint=new u.Math.Vector2(o.x,o.y),this.totalPathLength=this._calcTotalLength(i),this.slowFactor=1,this.slowSources=[],this.frozenTime=0,this.stunTime=0,this.burnDps=0,this.poisonDps=0,this.poisonTime=0,this._released=!1;const{w:c,h:m,color:f,yOffset:g}=this.sizeCfg;this.bodyRect=e.add.rectangle(0,g,c,m,f),this.add(this.bodyRect),this.bodyRect.setVisible(!1),this.spriteTypeIndex=H.getSpriteTypeFromId(s);const y=`enemy-${this.spriteTypeIndex}-idle-000`,w=e.textures.exists(y)?y:"__DEFAULT";this.bodySprite=e.add.sprite(0,g,w),this.bodySprite.setDisplaySize(c*1.5,m*1.5),this.add(this.bodySprite),this.healthBarGfx=e.add.graphics(),this.add(this.healthBarGfx),this.setActive(!1).setVisible(!1),e.add.existing(this)}spawn(e){this.pathPoints=e,this.currentSegment=0,this.distanceOnSegment=0,this.totalProgress=0,this.hp=this.maxHp,this.slowFactor=1,this.slowSources=[],this.frozenTime=0,this.stunTime=0,this.burnDps=0,this.poisonDps=0,this.poisonTime=0,this._released=!1;const s=e[0]??new u.Math.Vector2(0,0);this.x=s.x,this.y=s.y,this.position.set(s.x,s.y),this.spawnPoint.set(s.x,s.y),this.setActive(!0).setVisible(!0);const i=H.getAnimKey(this.spriteTypeIndex,"walk");this.scene.anims.exists(i)&&this.bodySprite.play(i),this.updateHealthBar()}takeDamage(e,s=0){if(!this.active||this._released)return;const i=Math.max(0,this.armor-s),o=Math.max(1,e-i);this.hp-=o,l.emit(p.DAMAGE_DEALT,{damage:o,isCrit:o>=150,x:this.x,y:this.y}),this.updateHealthBar();const a=H.getAnimKey(this.spriteTypeIndex,"hurt");this.scene.anims.exists(a)?(this.bodySprite.play(a),this.bodySprite.once(u.Animations.Events.ANIMATION_COMPLETE,()=>{if(this.active&&!this._released){const n=H.getAnimKey(this.spriteTypeIndex,"walk");this.scene.anims.exists(n)&&this.bodySprite.play(n)}})):(this.bodySprite.setTint(16777215),this.scene.time.delayedCall(75,()=>{this.active&&!this._released&&this.bodySprite.clearTint()})),this.hp<=0&&this.onDeath()}applyEffect(e,s,i){if(!(!this.active||this._released))switch(l.emit("vfx:status-applied",{type:e,value:s,duration:i,x:this.x,y:this.y,enemy:this}),e){case"slow":if(this.slowImmune)break;this.slowSources.push(s),this.slowFactor=Math.min(...this.slowSources),this.scene.time.delayedCall(i,()=>{if(this.active&&!this._released){const o=this.slowSources.indexOf(s);o!==-1&&this.slowSources.splice(o,1),this.slowFactor=this.slowSources.length>0?Math.min(...this.slowSources):1,this.slowSources.length===0&&l.emit("vfx:status-expired",{type:"slow",enemy:this})}});break;case"freeze":this.frozenTime=Math.max(this.frozenTime,i);break;case"stun":this.stunTime=Math.max(this.stunTime,i);break;case"burn":this.burnDps=Math.max(this.burnDps,s),this.scene.time.delayedCall(i,()=>{this.active&&!this._released&&(this.burnDps=0,l.emit("vfx:status-expired",{type:"burn",enemy:this}))});break;case"poison":this.poisonDps=Math.max(this.poisonDps,s),this.poisonTime=Math.max(this.poisonTime,i),this.scene.time.delayedCall(i,()=>{this.active&&!this._released&&(this.poisonDps=0,this.poisonTime=0,l.emit("vfx:status-expired",{type:"poison",enemy:this}))});break}}update(e){if(!this.active||this._released)return;if(this.frozenTime>0){const o=this.frozenTime;if(this.frozenTime=Math.max(0,this.frozenTime-e),o>0&&this.frozenTime<=0&&l.emit("vfx:status-expired",{type:"freeze",enemy:this}),this.updateHealthBar(),this.frozenTime>0)return}if(this.stunTime>0){const o=this.stunTime;if(this.stunTime=Math.max(0,this.stunTime-e),o>0&&this.stunTime<=0&&l.emit("vfx:status-expired",{type:"stun",enemy:this}),this.updateHealthBar(),this.stunTime>0)return}if(this.burnDps>0&&(this.hp-=this.burnDps*e/1e3,this.hp<=0)){this.onDeath();return}if(this.poisonDps>0&&(this.hp-=this.poisonDps*e/1e3,this.hp<=0)){this.onDeath();return}if(this.onUpdate(e),this.skipPathMovement){this.updateHealthBar();return}if(this.currentSegment>=this.pathPoints.length-1){this.onReachEnd();return}let i=this.speed*this.slowFactor*e/1e3;for(;i>0&&this.currentSegment=c){if(i-=c,this.currentSegment++,this.distanceOnSegment=0,this.currentSegment>=this.pathPoints.length-1){const m=this.pathPoints[this.pathPoints.length-1];this.x=m.x,this.y=m.y,this.position.set(m.x,m.y),this.totalProgress=1,this.onReachEnd();return}}else this.distanceOnSegment+=i,i=0}if(this.currentSegment0?this.distanceOnSegment/n:0;this.x=u.Math.Linear(o.x,a.x,c),this.y=u.Math.Linear(o.y,a.y,c),this.position.set(this.x,this.y),this.bodySprite.setFlipX(a.x.5?52224:a>.25?16755200:16720384;if(this.healthBarGfx.fillStyle(n,1),this.healthBarGfx.fillRect(o,i,Math.ceil(e*a),s),this.frozenTime>0){const{w:c,h:m,yOffset:f}=this.sizeCfg;this.healthBarGfx.fillStyle(8965375,.35),this.healthBarGfx.fillRect(-(c/2),-(m/2)+f,c,m)}}_calcTotalLength(e){let s=0;for(let i=1;i.5&&this.bodySprite.setFlipX(o<0),this.totalFlightDist>0){const m=u.Math.Distance.BetweenPoints(this.spawnPoint,{x:this.x,y:this.y});this.totalProgress=u.Math.Clamp(m/this.totalFlightDist,0,1)}}}const pe=class pe extends W{constructor(t,e,s){super(t,e,s),this.armor=this.armor+pe.ARMOR_BONUS}};r(pe,"ARMOR_BONUS",15);let De=pe;const V=class V extends W{constructor(e,s,i){super(e,s,i);r(this,"siblings",[])}setSiblings(e){this.siblings=e}onUpdate(e){if(!this.active)return;const s=e/1e3,i=this.maxHp*V.SELF_REGEN_RATE*s;this.hp=Math.min(this.maxHp,this.hp+i);const o=V.NEARBY_RADIUS*V.NEARBY_RADIUS;for(const a of this.siblings){if(a===this||!a.active||a.hp<=0)continue;const n=a.x-this.x,c=a.y-this.y;if(n*n+c*c<=o){const f=a.maxHp*V.ALLY_REGEN_RATE*s;a.hp=Math.min(a.maxHp,a.hp+f)}}}};r(V,"SELF_REGEN_RATE",.02),r(V,"ALLY_REGEN_RATE",.01),r(V,"NEARBY_RADIUS",80);let ce=V;const F=class F extends W{constructor(t,e,s){super(t,e,s),this.maxHp=Math.round(this.maxHp*F.HP_MULTIPLIER),this.hp=this.maxHp,this.speed=this.speed*F.SPEED_MULTIPLIER,this.armor=this.armor+F.ARMOR_BONUS}onDeath(){super.onDeath(),l.emit(p.ENEMY_DIED,{enemyId:this.enemyId,reward:this.reward,hp:this.maxHp,x:this.x,y:this.y,elite:!0})}};r(F,"HP_MULTIPLIER",2.2),r(F,"SPEED_MULTIPLIER",1.4),r(F,"ARMOR_BONUS",8);let xe=F;class Ve{constructor(t,e){r(this,"scene");r(this,"pathPoints");r(this,"pools",new Map);r(this,"activeEnemies",new Set);r(this,"onSpawnRequest",t=>{const e=t.pathPoints??this.pathPoints;e!==this.pathPoints&&(this.pathPoints=e);const{enemyId:s}=t;if(s.startsWith("boss:")){l.emit(p.BOSS_SPAWN_REQUEST,{bossId:s.slice(5),pathPoints:this.pathPoints});return}this.spawnEnemy(s)});r(this,"onEnemyDied",t=>{this.releaseByEnemyId(t.enemyId,t.x,t.y)});r(this,"onEnemyReachedEnd",()=>{});r(this,"onProjectileSplash",t=>{for(const e of this.activeEnemies){if(!e.active)continue;u.Math.Distance.Between(t.x,t.y,e.x,e.y)<=t.radius&&e.takeDamage(t.damage,t.ignoresArmor)}});this.scene=t,this.pathPoints=e}setupListeners(){l.on(p.ENEMY_SPAWN_REQUEST,this.onSpawnRequest,this),l.on(p.ENEMY_DIED,this.onEnemyDied,this),l.on(p.ENEMY_REACHED_END,this.onEnemyReachedEnd,this),l.on(p.PROJECTILE_SPLASH,this.onProjectileSplash,this)}spawnEnemy(t){const s=this.getOrCreatePool(t).get();return this.scene.enemyLayer.getAll().includes(s)||this.scene.enemyLayer.add(s),s.spawn(this.pathPoints),s instanceof ce&&s.setSiblings(Array.from(this.activeEnemies)),this.activeEnemies.add(s),l.emit(p.ENEMY_SPAWNED,{enemyId:t,x:s.x,y:s.y}),s}getActiveEnemies(){return Array.from(this.activeEnemies)}trackEnemy(t){this.activeEnemies.add(t)}getEnemiesByProgress(){return Array.from(this.activeEnemies).sort((t,e)=>e.totalProgress-t.totalProgress)}update(t,e){for(const s of this.activeEnemies)s.active&&s.update(e);for(const s of this.activeEnemies)s.active||this.activeEnemies.delete(s)}destroy(){l.off(p.ENEMY_SPAWN_REQUEST,this.onSpawnRequest,this),l.off(p.ENEMY_DIED,this.onEnemyDied,this),l.off(p.ENEMY_REACHED_END,this.onEnemyReachedEnd,this),l.off(p.PROJECTILE_SPLASH,this.onProjectileSplash,this);for(const t of this.pools.values())t.destroy();this.pools.clear(),this.activeEnemies.clear()}releaseByEnemyId(t,e,s){for(const i of this.activeEnemies)if(i.enemyId===t&&Math.abs(i.x-e)<2&&Math.abs(i.y-s)<2){this.releaseEnemy(i);return}for(const i of this.activeEnemies)if(i.enemyId===t&&!i.active){this.releaseEnemy(i);return}}releaseEnemy(t){this.activeEnemies.delete(t);const e=this.pools.get(t.poolKey);e&&e.release(t)}getOrCreatePool(t){if(!this.pools.has(t)){const e=this.scene,s=this.pathPoints,i=new kt(e,()=>{const o=Ve.createEnemy(e,t,s);return o.poolKey=t,o},5);this.pools.set(t,i)}return this.pools.get(t)}static createEnemy(t,e,s){const i=e.indexOf(":"),o=i!==-1?e.slice(0,i):"",a=i!==-1?e.slice(i+1):e;switch(o){case"fast":return new Te(t,a,s);case"tank":return new Ae(t,a,s);case"flying":return new Vt(t,a,s);case"armored":return new De(t,a,s);case"healer":return new ce(t,a,s);case"elite":return new xe(t,a,s);default:return new W(t,a,s)}}}const zt=[{id:"boss-01-gorthak",name:"Gorthak the Defiler",world:1,hp:2500,speed:55,armor:20,reward:200,spriteKey:"boss-01-gorthak",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["charge"],description:"Charges forward, knocking aside projectiles"},{phase:2,hpThreshold:.5,abilities:["charge","summon-minions"],summons:["enemy-01","enemy-02"],summonCount:5,description:"Enrages and begins summoning goblin warriors and archers"}]},{id:"boss-02-ssylvara",name:"Ssylvara the Venomous",world:2,hp:3200,speed:65,armor:25,reward:280,spriteKey:"boss-02-ssylvara",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["poison-nova"],description:"Emits poison cloud that damages and slows towers"},{phase:2,hpThreshold:.4,abilities:["poison-nova","tunnel"],description:"Tunnels underground briefly, reappearing mid-path"}]},{id:"boss-03-zhalfrix",name:"Zhalfrix the Frozen",world:3,hp:4e3,speed:50,armor:30,reward:350,spriteKey:"boss-03-zhalfrix",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["freeze-pulse"],description:"Pulses ice waves that freeze towers in range"},{phase:2,hpThreshold:.35,abilities:["freeze-pulse","ice-armor"],armorBonus:20,description:"Encases itself in ice armor, massively increasing armor"}]},{id:"boss-04-maldreth",name:"Maldreth the Lava Lord",world:4,hp:5e3,speed:45,armor:35,reward:450,spriteKey:"boss-04-maldreth",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["lava-pool","fire-aura"],description:"Leaves lava pools that damage towers. Fire aura burns nearby towers."},{phase:2,hpThreshold:.3,abilities:["lava-pool","fire-aura","summon-lava-spawn"],summons:["lava-spawn"],summonCount:8,description:"Erupts, spawning Lava Spawn minions that swarm towers"}],minions:[{id:"lava-spawn",name:"Lava Spawn",spriteKey:"boss-04-maldreth-minion-lava-spawn",hp:80,speed:120,armor:0,reward:5,type:"ground"}]},{id:"boss-05-toad-king",name:"The Toad King",world:8,hp:6e3,speed:50,armor:30,reward:500,spriteKey:"boss-05-toad-king",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["summon-zombies","tongue-grab"],summons:["enemy-61","enemy-63"],summonCount:6,description:"Summons zombie swarms; tongue grab stuns towers briefly"},{phase:2,hpThreshold:.5,abilities:["summon-zombies","leap-attack"],description:"Leaps to a random path segment, instantly moving forward"}]},{id:"boss-06-rhino-beetle",name:"Rhinoceros Beetle",world:9,hp:7500,speed:55,armor:40,reward:620,spriteKey:"boss-06-rhino-beetle",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["charge-attack"],description:"Charge attack damages towers directly in path"},{phase:2,hpThreshold:.45,abilities:["charge-attack","shell-armor"],armorBonus:25,description:"Becomes armored — takes 50% reduced damage from all sources"}]},{id:"boss-07-iron-troll",name:"Iron Troll",world:10,hp:9e3,speed:45,armor:35,reward:750,spriteKey:"boss-07-iron-troll",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["regenerate"],regenPerSecond:60,description:"Rapidly regenerates health — towers must maintain constant damage"},{phase:2,hpThreshold:.4,abilities:["regenerate","summon-pack-e"],summons:["enemy-41","enemy-42","enemy-44"],summonCount:4,description:"Calls Pack E undead to reinforce while regenerating"}]},{id:"boss-08-ancient-mech",name:"Ancient Mech",world:11,hp:12e3,speed:40,armor:50,reward:900,spriteKey:"boss-08-ancient-mech",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["shield"],description:"Shield phases — immune to all damage while shield is active. Destroy the shield!"},{phase:2,hpThreshold:.5,abilities:["beam-attack"],description:"Beam attack disables towers in line for 4 seconds"}]},{id:"boss-09-frost-ooze",name:"Frost Ooze",world:11,hp:8e3,speed:50,armor:20,reward:850,spriteKey:"boss-09-frost-ooze",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["split-on-damage"],description:"Splits into smaller oozes at 75%, 50%, and 25% HP"},{phase:2,hpThreshold:.25,abilities:["split-on-damage","freeze-all"],description:"Final split triggers a global tower freeze for 5 seconds"}]},{id:"boss-10-magic-bear",name:"Magic Bear",world:11,hp:1e4,speed:55,armor:25,reward:1e3,spriteKey:"boss-10-magic-bear",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["absorb-magic"],description:"Absorbs magic damage — heals instead of taking damage from magic towers"},{phase:2,hpThreshold:.5,abilities:["absorb-magic","reverse-targeting"],description:"Reverses tower targeting — towers attack the wrong end of the path"}]},{id:"boss-11-storm-titan",name:"Storm Titan",world:12,hp:14e3,speed:50,armor:45,reward:950,spriteKey:"boss-11-storm-titan",size:"boss",type:"flying",phases:[{phase:1,hpThreshold:1,abilities:["lightning-chain"],description:"Chains lightning between 3 towers, disabling them for 3 seconds each"},{phase:2,hpThreshold:.5,abilities:["lightning-chain","storm-shield","summon-circuit-fiends"],summons:["enemy-58","enemy-34"],summonCount:5,description:"Activates storm shield (50% damage reduction) and summons Circuit Fiends"},{phase:3,hpThreshold:.2,abilities:["lightning-chain","overcharge"],description:"Overcharges — moves 60% faster and discharges lightning every 2 seconds"}]},{id:"boss-12-dungeon-lord",name:"The Dungeon Lord",world:13,hp:16e3,speed:42,armor:55,reward:1100,spriteKey:"boss-12-dungeon-lord",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["darkness-pulse","summon-undead"],summons:["enemy-41","enemy-45"],summonCount:8,description:"Darkness pulse blinds towers (range -50%) and summons undead hordes"},{phase:2,hpThreshold:.55,abilities:["darkness-pulse","bone-cage","revive-minions"],description:"Cages 2 towers completely. Revives dead minions at 20% HP."},{phase:3,hpThreshold:.2,abilities:["lich-form","undying"],description:"Transforms into Lich — becomes immune to physical damage. Only magic towers can hurt it."}]},{id:"boss-13-kraken",name:"The Kraken",world:14,hp:18e3,speed:38,armor:40,reward:1250,spriteKey:"boss-13-kraken",size:"boss",type:"amphibious",phases:[{phase:1,hpThreshold:1,abilities:["tentacle-grab","ink-cloud"],description:"Tentacle grab destroys a random tower. Ink cloud reduces tower range in an area."},{phase:2,hpThreshold:.6,abilities:["tentacle-grab","summon-tide-spawn","whirlpool"],summons:["enemy-66","enemy-68"],summonCount:10,description:"Summons Tidal Spawn swarms and creates a whirlpool that pulls enemies toward it — giving them a shortcut"},{phase:3,hpThreshold:.25,abilities:["kraken-rage","tsunami"],description:"Enraged — double speed, triple tentacle attacks. Tsunami wave slows all towers for 8 seconds."}]},{id:"boss-14-world-ender",name:"The World Ender",world:15,hp:3e4,speed:45,armor:60,reward:2e3,spriteKey:"boss-14-world-ender",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["absorb-damage","curse-all-towers"],description:"Absorbs 30% of all damage dealt to it and heals. Curses all towers — reducing their damage by 25%."},{phase:2,hpThreshold:.65,abilities:["void-rift","summon-elite-pack"],summons:["enemy-40","enemy-60","enemy-67"],summonCount:3,description:"Opens void rifts that summon Eldritch Horrors, Nexus Horrors, and Abyssal Crawlers simultaneously."},{phase:3,hpThreshold:.35,abilities:["reality-break","immune-slow","immune-freeze","immune-magic"],description:"Reality Break — becomes immune to slow, freeze, AND magic damage. Only physical non-magic towers work."},{phase:4,hpThreshold:.1,abilities:["final-form","all-immunities","berserker"],description:"Final Form — triples speed, removes all immunities, takes full damage. An all-out race to finish it off."}]},{id:"boss-15-celestial-titan",name:"Celestial Titan",world:16,hp:18e3,speed:60,armor:50,reward:1400,spriteKey:"boss-15-celestial-titan",size:"boss",type:"flying",phases:[{phase:1,hpThreshold:1,abilities:["stellar-pulse","gravity-well"],description:"Stellar pulse blinds towers for 2 seconds. Gravity well pulls projectiles off-course."},{phase:2,hpThreshold:.6,abilities:["stellar-pulse","gravity-well","summon-star-shards"],summons:["enemy-i01","enemy-i02"],summonCount:6,description:"Summons Star Shards — small flying fragments that orbit the boss and block projectiles."},{phase:3,hpThreshold:.25,abilities:["nova-burst","immune-ground-towers"],description:"Nova burst damages all towers in range. Ascends — only aerial-capable towers can hit it."}]},{id:"boss-16-the-archivist",name:"The Archivist",world:17,hp:2e4,speed:48,armor:55,reward:1600,spriteKey:"boss-16-the-archivist",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["curse-tome","summon-animated-books"],summons:["enemy-41","enemy-45"],summonCount:5,description:"Hurls cursed tomes that silence towers. Summons animated book constructs to swarm."},{phase:2,hpThreshold:.55,abilities:["curse-tome","maze-shift","revive-all-minions"],description:"Reshuffles bookshelves — path reroutes mid-level. Revives all dead minions at 30% HP."},{phase:3,hpThreshold:.2,abilities:["forbidden-knowledge","immune-magic","time-rewind"],description:"Reads forbidden text — becomes magic-immune. Time Rewind resets its position to mid-path once."}]},{id:"boss-17-sand-colossus",name:"The Sand Colossus",world:18,hp:25e3,speed:40,armor:65,reward:1800,spriteKey:"boss-17-sand-colossus",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["sandstorm","summon-scarabs"],summons:["enemy-j01","enemy-j02"],summonCount:10,description:"Sandstorm reduces all tower range by 40%. Summons Scarab swarms that overwhelm by sheer number."},{phase:2,hpThreshold:.5,abilities:["sandstorm","burrow"],description:"Burrows underground — untargetable for 6 seconds, re-emerges halfway along the path."},{phase:3,hpThreshold:.2,abilities:["colossus-stomp","sand-shield","immune-slow"],armorBonus:30,description:"Stomp destroys all towers in a radius. Sand shield absorbs 50% of damage. Immune to slow."}],minions:[{id:"enemy-j01",name:"Scarab Swarm",spriteKey:"enemy-scarab-swarm",hp:60,speed:160,armor:0,reward:3,type:"ground"}]},{id:"boss-18-sugar-witch",name:"The Sugar Witch",world:19,hp:22e3,speed:70,armor:40,reward:1900,spriteKey:"boss-18-sugar-witch",size:"boss",type:"flying",phases:[{phase:1,hpThreshold:1,abilities:["candy-coat","summon-gumdrop-goons"],summons:["enemy-j03","enemy-j04"],summonCount:8,description:"Candy-coats herself — first 3000 damage is absorbed by candy shell. Summons Gumdrop Goons."},{phase:2,hpThreshold:.6,abilities:["sugar-rush","cotton-candy-cloud"],description:"Sugar rush triples her speed for 5 seconds. Cotton candy cloud slows all towers' fire rate."},{phase:3,hpThreshold:.25,abilities:["confection-overload","candy-clone","immune-physical"],description:"Splits into 3 candy clones — only the real one takes damage. Clones move at double speed. Immune to physical."}]},{id:"boss-19-nexus-core",name:"The Nexus Core",world:20,hp:5e4,speed:35,armor:70,reward:5e3,spriteKey:"boss-19-nexus-core",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["reality-echo","summon-all-packs"],summons:["enemy-01","enemy-21","enemy-41","enemy-57","enemy-i01"],summonCount:5,description:"Reality echoes — summons one enemy from every pack simultaneously. Towers fired last wave take 20% more damage."},{phase:2,hpThreshold:.7,abilities:["nexus-drain","tower-corruption"],description:"Nexus drain siphons gold from the player (50/sec). Tower corruption randomly reverses one tower's targeting for 10 seconds."},{phase:3,hpThreshold:.45,abilities:["dimension-collapse","immune-magic","immune-slow"],description:"Dimension collapse — all towers lose 30% range AND damage. Immune to magic and slow."},{phase:4,hpThreshold:.2,abilities:["core-overload","immune-physical","infinite-summons"],description:"Core overload — immune to physical now too. Only towers with BOTH physical+magic damage can hit it. Summons 2 elite enemies every 4 seconds."},{phase:5,hpThreshold:.05,abilities:["final-convergence","all-immunities-removed","berserker"],description:"Final Convergence — ALL immunities drop. Moves at 4x speed. One last desperate charge — defeat it before it reaches the end!"}]},{id:"boss-20-jungle-titan",name:"The Jungle Titan",world:21,hp:28e3,speed:40,armor:55,reward:2200,spriteKey:"boss-20-jungle-titan",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["vine-grasp","canopy-collapse"],description:"Vine grasp entangles and halves the fire rate of 2 random towers. Canopy collapse drops debris in an AoE, briefly silencing towers under it."},{phase:2,hpThreshold:.5,abilities:["vine-grasp","canopy-collapse","summon-canopy-crawlers"],summons:["enemy-k01"],summonCount:8,description:"Enrages — vine reach doubles, collapse AoE grows. Summons 8 Cave Crawlers from the canopy above."}]},{id:"boss-21-frost-warden",name:"The Frost Warden",world:22,hp:18e3,speed:45,armor:50,reward:1800,spriteKey:"boss-21-frost-warden",size:"boss",type:"ground",tagTeamPartner:"boss-21-blizzard-queen",enrageOnPartnerDeath:!0,phases:[{phase:1,hpThreshold:1,abilities:["freeze-lock"],description:"Channels an ice beam that freezes all towers in a straight line for 3 seconds."},{phase:2,hpThreshold:0,trigger:"partner-death",abilities:["freeze-lock","enrage-solo"],description:"Partner's death triggers berserker mode — speed ×2, freeze-lock beam doubles in width."}]},{id:"boss-21-blizzard-queen",name:"The Blizzard Queen",world:22,hp:18e3,speed:55,armor:35,reward:1800,spriteKey:"boss-21-blizzard-queen",size:"boss",type:"flying",tagTeamPartner:"boss-21-frost-warden",phases:[{phase:1,hpThreshold:1,abilities:["ice-storm-aoe"],description:"Ice storm AoE slows all towers within radius by 40% for 5 seconds."},{phase:2,hpThreshold:.5,abilities:["ice-storm-aoe","warden-shield"],description:"Erects a crystalline shield around the Frost Warden — absorbs 50% of damage dealt to it."}]},{id:"boss-22-siege-colossus",name:"The Siege Colossus",world:23,hp:35e3,speed:30,armor:65,reward:2600,spriteKey:"boss-22-siege-colossus",size:"boss",type:"ground",immunities:["slow"],phases:[{phase:1,hpThreshold:1,abilities:["wall-charge","mortar-barrage","immune-slow"],description:"Wall-charge instantly destroys one tower per charge. Mortar barrage rains AoE fire on tower clusters."},{phase:2,hpThreshold:.6,abilities:["wall-charge","mortar-barrage","immune-slow","reinforce-armor"],armorBonus:30,description:"Siege armor activates — gains +30 armor. Charge rate doubles."},{phase:3,hpThreshold:.25,abilities:["wall-charge","mortar-barrage","immune-slow","immune-freeze","berserker"],description:"Final siege — immune to slow and freeze. All abilities fire simultaneously. Unstoppable charge."}]},{id:"boss-23-crystal-leviathan",name:"The Crystal Leviathan",world:24,hp:32e3,speed:35,armor:60,reward:2800,spriteKey:"boss-23-crystal-leviathan",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["crystal-shield","shard-burst"],description:"Crystal shield reflects 25% of projectile damage back to the attacking tower. Shard burst fires crystal shards in a 360° AoE."},{phase:2,hpThreshold:.65,abilities:["crystal-shield","shard-burst","burrow"],description:"Burrows underground for 5 seconds — completely untargetable. Re-emerges at mid-path with full shields."},{phase:3,hpThreshold:.3,abilities:["shard-storm","immune-physical"],description:"Shield shatters in a shard storm, raining crystal spikes on all towers. Becomes immune to physical damage — magic only."}]},{id:"boss-24-tide-serpent",name:"The Tide Serpent",world:25,hp:22e3,speed:60,armor:40,reward:2e3,spriteKey:"boss-24-tide-serpent",size:"boss",type:"ground",tagTeamPartner:"boss-24-depth-caller",phases:[{phase:1,hpThreshold:1,abilities:["charge-sweep"],description:"Sweeping charge hits all towers along its path, knocking them offline for 2 seconds each."},{phase:2,hpThreshold:0,trigger:"partner-low-hp",triggerThreshold:.4,abilities:["charge-sweep","empowered-surge"],description:"When Depth-Caller drops to 40% HP, Serpent enters empowered surge — gains +50% speed and damage for 20 seconds."}]},{id:"boss-24-depth-caller",name:"The Depth-Caller",world:25,hp:22e3,speed:40,armor:50,reward:2e3,spriteKey:"boss-24-depth-caller",size:"boss",type:"ground",tagTeamPartner:"boss-24-tide-serpent",phases:[{phase:1,hpThreshold:1,abilities:["deep-summon"],summons:["enemy-k05"],summonCount:6,description:"Continuously summons Deep Lurkers from submerged tunnels every 20 seconds."},{phase:2,hpThreshold:.5,abilities:["deep-summon","empower-partner"],summons:["enemy-k05"],summonCount:6,description:"Empowers the Tide Serpent — partner triggers berserker mode when Depth-Caller reaches 40% HP."}]},{id:"boss-25-myconid-sovereign",name:"The Myconid Sovereign",world:26,hp:38e3,speed:42,armor:45,reward:3e3,spriteKey:"boss-25-myconid-sovereign",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["spore-cloud"],description:"Spore cloud blinds all towers within a large radius for 4 seconds — they cannot attack."},{phase:2,hpThreshold:.7,abilities:["spore-cloud","spore-trail"],description:"Leaves a persistent spore trail that deals poison damage to any tower it passes near."},{phase:3,hpThreshold:.4,abilities:["spore-cloud","spore-trail","clone-split"],description:"Splits into 2 identical clones — only the real one takes damage. Clones still use all abilities."},{phase:4,hpThreshold:.15,abilities:["myconid-fury","immune-poison","clone-merge"],description:"Clones merge back into one. Becomes immune to poison and activates all abilities simultaneously at double intensity."}]},{id:"boss-26-bone-general-A",name:"Bone General Karveth",world:27,hp:2e4,speed:55,armor:45,reward:1900,spriteKey:"boss-26-bone-general-A",size:"boss",type:"ground",tagTeamPartner:"boss-26-bone-general-B",enrageOnPartnerDeath:!1,phases:[{phase:1,hpThreshold:1,abilities:["charge-rush"],description:"Bone charge rush — instantly destroys any tower it impacts, then resumes path."},{phase:2,hpThreshold:0,trigger:"partner-low-hp",triggerThreshold:.3,abilities:["charge-rush","revive-partner"],description:"When General B drops to 30% HP, General A uses a one-time revive — restoring partner to 30% HP. Charge-rush cooldown halved."}]},{id:"boss-26-bone-general-B",name:"Bone General Ossara",world:27,hp:2e4,speed:40,armor:55,reward:1900,spriteKey:"boss-26-bone-general-B",size:"boss",type:"ground",tagTeamPartner:"boss-26-bone-general-A",phases:[{phase:1,hpThreshold:1,abilities:["summon-skeleton-army","bone-shield-A"],summons:["enemy-k02"],summonCount:5,description:"Summons Rock Golems as skeleton honor guard. Projects a bone shield onto General A, absorbing 40% of incoming damage."},{phase:2,hpThreshold:.5,abilities:["summon-skeleton-army","bone-shield-A","death-march"],summons:["enemy-k01","enemy-k02"],summonCount:6,description:"Death March activates — all summoned skeletons gain +50% movement speed. Shield transfers if General A dies."}]},{id:"boss-27-magma-drake",name:"The Magma Drake",world:28,hp:45e3,speed:35,armor:70,reward:3500,spriteKey:"boss-27-magma-drake",size:"boss",type:"ground",immunities:["fire","burn","slow","freeze"],weakness:"ice",phases:[{phase:1,hpThreshold:1,abilities:["lava-wave","fire-aura","immune-fire","immune-burn"],description:"Lava wave destroys tower build sites it passes through. Fire aura continuously burns all towers within 200px."},{phase:2,hpThreshold:.65,abilities:["lava-wave","firewall","immune-fire","immune-burn","immune-slow"],description:"Firewall seals sections of the path — towers behind the wall cannot shoot through it."},{phase:3,hpThreshold:.35,abilities:["lava-wave","firewall","immune-fire","immune-burn","immune-slow","immune-freeze","immune-magic"],description:"Thermal armor hardens — immune to fire, slow, freeze, AND magic. Only ice towers deal full damage now."},{phase:4,hpThreshold:.1,abilities:["lava-meltdown","berserker","all-immunities-removed"],description:"Thermal armor collapses in a lava meltdown — ALL immunities drop. Takes 2× damage from all sources. Final desperate charge."}]},{id:"boss-28-void-herald",name:"The Void Herald",world:29,hp:3e4,speed:45,armor:55,reward:2800,spriteKey:"boss-28-void-herald",size:"boss",type:"ground",tagTeamPartner:"boss-28-rift-walker",enrageOnPartnerDeath:!0,phases:[{phase:1,hpThreshold:1,abilities:["void-shriek"],description:"Void shriek partially phases the Herald — 50% of projectiles pass through without dealing damage."},{phase:2,hpThreshold:0,trigger:"partner-phase-sync",triggerThreshold:.5,abilities:["void-shriek","combined-void-field"],description:"When Rift Walker reaches 50% HP, both bosses synchronise phases — they must both be killed within the same 8-second damage window."}]},{id:"boss-28-rift-walker",name:"The Rift Walker",world:29,hp:3e4,speed:60,armor:45,reward:2800,spriteKey:"boss-28-rift-walker",size:"boss",type:"ground",tagTeamPartner:"boss-28-void-herald",enrageOnPartnerDeath:!0,phases:[{phase:1,hpThreshold:1,abilities:["dimension-hop"],description:"Teleports between random path segments — appears at a point further along the path, bypassing towers."},{phase:2,hpThreshold:.5,abilities:["dimension-hop","pull-partner-sync"],description:"Pulls the Void Herald into phase synchronisation — both bosses must now be killed in the same 8-second kill window."}]},{id:"boss-29-the-convergence",name:"The Convergence",world:30,hp:1e5,speed:25,armor:80,reward:1e4,spriteKey:"boss-29-the-convergence",size:"boss",type:"ground",phases:[{phase:1,hpThreshold:1,abilities:["reality-echo","summon-prior-bosses"],summons:["boss-20-jungle-titan","boss-22-siege-colossus","boss-25-myconid-sovereign"],summonHp:.5,description:"Reality echo summons shadow versions of prior world bosses at 50% HP as reinforcements."},{phase:2,hpThreshold:.8,abilities:["rotating-immunity"],immunityRotationSec:30,description:"Immunity rotation begins — switches between fire, ice, magic, and physical immunity every 30 seconds."},{phase:3,hpThreshold:.6,abilities:["rotating-immunity","path-collapse"],description:"Sections of the map begin collapsing — enemies take alternate routes, splitting tower coverage."},{phase:4,hpThreshold:.4,abilities:["rotating-immunity","path-collapse","convergence-drain"],goldDrainPerSec:100,description:"Convergence drain siphons 100 gold every 5 seconds directly from the player's reserves."},{phase:5,hpThreshold:.2,abilities:["all-immunities-off","berserker","convergence-surge"],description:"All immunities collapse simultaneously. Speed triples. One final convergence surge of every ability at once."},{phase:6,hpThreshold:.05,abilities:["final-collapse","summon-all-bosses"],summons:["boss-21-frost-warden","boss-21-blizzard-queen","boss-23-crystal-leviathan","boss-24-tide-serpent","boss-26-bone-general-A","boss-27-magma-drake","boss-28-void-herald"],summonHp:.1,description:"Final collapse — summons shadow versions of ALL remaining boss types at 10% HP. The true final stand. Everything at once."}]}],ot={bosses:zt};class at extends W{constructor(e,s,i){const o=ot.bosses.find(n=>n.id===s);if(!o)throw new Error(`[BossEnemy] Unknown bossId: "${s}"`);super(e,"enemy-01",i);r(this,"bossData");r(this,"currentPhaseIndex");r(this,"nameLabel");this.bossData=o,this.enemyId=s,this.maxHp=o.hp,this.hp=o.hp,this.speed=o.speed,this.armor=o.armor,this.reward=o.reward,this.type=o.type==="flying"?"flying":"ground";const a=this.getAt(0);a&&"setSize"in a&&(a.setSize(48,48),a.setFillStyle(8913032)),this.nameLabel=e.add.text(0,-36,o.name,{fontSize:"10px",color:"#ffffff",fontFamily:"monospace",stroke:"#000000",strokeThickness:2}).setOrigin(.5,1),this.add(this.nameLabel),this.currentPhaseIndex=0,o.phases.length>0&&this.activatePhase(o.phases[0],!1)}spawn(e){super.spawn(e),this.currentPhaseIndex=0,this.bossData.phases.length>0&&this.activatePhase(this.bossData.phases[0],!1)}takeDamage(e,s=0){const i=this.hp;super.takeDamage(e,s),this.active&&i>0&&(this.checkPhaseTransitions(),this.active&&l.emit("boss:hp-update",{bossId:this.enemyId,hp:this.hp}))}checkPhaseTransitions(){const e=this.bossData.phases,s=this.hp/this.maxHp;for(let i=this.currentPhaseIndex+1;ii.startsWith("summon-"))&&e.summons&&e.summonCount){const i=e.summonCount,o=e.summons;for(let a=0;a=I.SHOCKWAVE_INTERVAL_MS&&(this.shockwaveCooldown=0,this.specialtyAttack()))}onPhase2Enter(){this.speed*=I.PHASE2_SPEED_MULT,this.damageMultiplier=I.PHASE2_DAMAGE_MULT,this.shockwaveCooldown=0}specialtyAttack(){l.emit("boss:gorthak_shockwave",{x:this.x,y:this.y,radius:I.SHOCKWAVE_RADIUS})}};r(I,"SHOCKWAVE_INTERVAL_MS",8e3),r(I,"SHOCKWAVE_RADIUS",150),r(I,"PHASE2_SPEED_MULT",1.5),r(I,"PHASE2_DAMAGE_MULT",2);let Pe=I;class Ft extends ue{constructor(t,e){super(t,"boss-02-ssylvara",e)}onPhase2Enter(){l.emit("boss:ssylvara_split",{x:this.x,y:this.y,hp:this.maxHp*.25}),this.setActive(!1).setVisible(!1)}specialtyAttack(){}}const J=class J extends ue{constructor(e,s){super(e,"boss-03-zhalfrix",s);r(this,"freezeCooldown",0)}update(e){super.update(e),this.phase===2&&this.active&&(this.freezeCooldown+=e,this.freezeCooldown>=J.FREEZE_INTERVAL_MS&&(this.freezeCooldown=0,this.specialtyAttack()))}onPhase2Enter(){this.freezeCooldown=0,this.specialtyAttack()}specialtyAttack(){l.emit("boss:zhalfrix_freeze",{duration:J.FREEZE_DURATION_MS})}};r(J,"FREEZE_INTERVAL_MS",12e3),r(J,"FREEZE_DURATION_MS",3e3);let Le=J;const Z=class Z extends ue{constructor(e,s){super(e,"boss-04-maldreth",s);r(this,"minionSpawnCooldown",0);r(this,"fireImmune",!1)}update(e){super.update(e),this.phase===2&&this.active&&(this.minionSpawnCooldown+=e,this.minionSpawnCooldown>=Z.SPAWN_INTERVAL_MS&&(this.minionSpawnCooldown=0,this.specialtyAttack()))}onPhase2Enter(){this.fireImmune=!0,this.minionSpawnCooldown=0,this.specialtyAttack()}specialtyAttack(){l.emit("boss:maldreth_spawn_minion",{x:this.x,y:this.y})}takeDamage(e,s=0,i){const o=this.fireImmune&&i==="fire"?e*(1-Z.FIRE_REDUCTION):e;super.takeDamage(o,s)}};r(Z,"SPAWN_INTERVAL_MS",15e3),r(Z,"FIRE_REDUCTION",.9);let Re=Z;class ze{constructor(t,e){r(this,"scene");r(this,"enemySystem");r(this,"activeBosses",[]);r(this,"onBossSpawnRequest",t=>{this.spawnBoss(t.bossId,t.pathPoints)});this.scene=t,this.enemySystem=e}setupListeners(){l.on(p.BOSS_SPAWN_REQUEST,this.onBossSpawnRequest,this)}spawnBoss(t,e){let s;try{s=ze.createBoss(this.scene,t,e)}catch(o){return console.error("[BossSystem] Failed to create boss:",o),null}this.scene.enemyLayer.add(s),s.spawn(e),this.activeBosses.push(s),this.enemySystem.trackEnemy(s);const i=ot.bosses.find(o=>o.id===t);return l.emit("boss:spawned",{bossId:t,bossName:(i==null?void 0:i.name)??t,maxHp:s.maxHp}),l.emit(p.ENEMY_SPAWNED,{enemyId:t,x:s.x,y:s.y}),s}update(t,e){for(const s of this.activeBosses)s.active&&s.update(e);for(const s of this.activeBosses)s.active||s.destroy();this.activeBosses=this.activeBosses.filter(s=>s.active)}destroy(){l.off(p.BOSS_SPAWN_REQUEST,this.onBossSpawnRequest,this);for(const t of this.activeBosses)t.destroy();this.activeBosses=[]}static createBoss(t,e,s){switch(e){case"boss-01-gorthak":return new Pe(t,s);case"boss-02-ssylvara":return new Ft(t,s);case"boss-03-zhalfrix":return new Le(t,s);case"boss-04-maldreth":return new Re(t,s);default:return new at(t,e,s)}}}const Xe=100,M=101,re=102,te={fontSize:"20px",color:"#ffffff",fontFamily:"monospace"};class Wt{constructor(t,e,s=150,i=20){r(this,"scene");r(this,"goldText");r(this,"livesText");r(this,"goldIcon");r(this,"livesIcon");r(this,"waveText");r(this,"waveBtn");r(this,"waveBtnText");r(this,"speedBtn");r(this,"speedMultiplier",1);r(this,"abilitySlots",[]);r(this,"abilityBar");r(this,"expBarBg");r(this,"expBarFill");r(this,"expBarWidth",0);r(this,"expBarY",0);r(this,"expLabel");this.scene=t,this.build(e,s,i)}updateGold(t){this.goldText.setText(this.goldIcon?String(t):`💰 ${t}`)}updateLives(t){this.livesText.setText(this.livesIcon?String(Math.max(0,t)):`❤️ ${Math.max(0,t)}`)}updateWave(t,e){this.waveText.setText(`Wave ${t}/${e}`)}setWaveButtonVisible(t){this.waveBtn.setVisible(t),this.waveBtnText.setVisible(t)}buildAbilityBar(t){var n;if((n=this.abilityBar)==null||n.destroy(),this.abilitySlots.forEach(c=>{c.bg.destroy(),c.label.destroy(),c.badge.destroy()}),this.abilitySlots=[],t.length===0)return;const{width:e}=this.scene.cameras.main,s=52,i=110,o=36,a=8;this.abilityBar=this.scene.add.graphics(),this.abilityBar.fillStyle(0,.65),this.abilityBar.fillRect(0,s,e,o+8),this.abilityBar.setScrollFactor(0).setDepth(100),t.forEach((c,m)=>{const f=a+m*(i+4),g=s+4,y=this.scene.add.rectangle(f+i/2,g+o/2,i,o,2241348).setScrollFactor(0).setDepth(101).setInteractive({useHandCursor:!0}).setStrokeStyle(1,4482730);y.on("pointerup",()=>{c.charges>0&&this.scene.events.emit("ui:use-ability",c.id)}),y.on("pointerover",()=>y.setFillStyle(3359829)),y.on("pointerout",()=>y.setFillStyle(c.charges>0?2241348:1118498));const w=this.scene.add.text(f+4,g+4,`${c.icon} ${c.name}`,{fontSize:"11px",color:"#aaddff",fontFamily:"monospace"}).setScrollFactor(0).setDepth(102),E=this.scene.add.text(f+i-4,g+o-4,`×${c.charges}`,{fontSize:"13px",color:c.charges>0?"#ffdd44":"#555555",fontFamily:"monospace",fontStyle:"bold"}).setOrigin(1,1).setScrollFactor(0).setDepth(102);this.abilitySlots.push({bg:y,label:w,badge:E})})}updateAbilityCharges(t,e){this.abilitySlots.forEach((s,i)=>{const o=e[i];if(!o)return;const a=t[o.id]??0;o.charges=a,s.badge.setText(`×${a}`),s.badge.setColor(a>0?"#ffdd44":"#555555"),s.bg.setFillStyle(a>0?2241348:1118498)})}buildExpBar(t){var i,o,a;(i=this.expBarBg)==null||i.destroy(),(o=this.expBarFill)==null||o.destroy(),(a=this.expLabel)==null||a.destroy();const{width:e}=this.scene.cameras.main;this.expBarWidth=e,this.expBarY=t?96:52;const s=4;this.expBarBg=this.scene.add.graphics(),this.expBarBg.fillStyle(1122867,.9),this.expBarBg.fillRect(0,this.expBarY,e,s),this.expBarBg.setScrollFactor(0).setDepth(Xe),this.expBarFill=this.scene.add.graphics(),this.expBarFill.setScrollFactor(0).setDepth(M),this._redrawExpFill(0),this.expLabel=this.scene.add.text(e-4,this.expBarY-1,"EXP",{fontSize:"9px",color:"#6699bb",fontFamily:"monospace"}).setOrigin(1,1).setScrollFactor(0).setDepth(M)}updateExpBar(t){this.expBarFill&&this._redrawExpFill(u.Math.Clamp(t,0,1))}destroy(){var t,e,s,i,o,a;(t=this.goldIcon)==null||t.destroy(),(e=this.livesIcon)==null||e.destroy(),(s=this.abilityBar)==null||s.destroy(),this.abilitySlots.forEach(n=>{n.bg.destroy(),n.label.destroy(),n.badge.destroy()}),(i=this.expBarBg)==null||i.destroy(),(o=this.expBarFill)==null||o.destroy(),(a=this.expLabel)==null||a.destroy()}build(t,e,s){const{width:i}=this.scene.cameras.main,o=this.scene.add.graphics();o.fillStyle(0,.75),o.fillRect(0,0,i,52),o.setScrollFactor(0).setDepth(Xe),this.scene.textures.exists("hud-ico-1")?(this.goldIcon=this.scene.add.image(28,26,"hud-ico-1").setDisplaySize(28,28).setScrollFactor(0).setDepth(M),this.goldText=this.scene.add.text(46,14,String(e),te).setScrollFactor(0).setDepth(M)):this.goldText=this.scene.add.text(16,14,`💰 ${e}`,te).setScrollFactor(0).setDepth(M),this.scene.textures.exists("hud-heart")?(this.livesIcon=this.scene.add.image(180,26,"hud-heart").setDisplaySize(28,28).setScrollFactor(0).setDepth(M),this.livesText=this.scene.add.text(198,14,String(s),te).setScrollFactor(0).setDepth(M)):this.livesText=this.scene.add.text(160,14,`❤️ ${s}`,te).setScrollFactor(0).setDepth(M),this.waveText=this.scene.add.text(i/2,14,`Wave 0/${t}`,te).setOrigin(.5,0).setScrollFactor(0).setDepth(M),this.speedBtn=this.scene.add.text(i-240,26,"⚡1×",{fontSize:"18px",color:"#ffff88",fontFamily:"monospace"}).setOrigin(.5).setScrollFactor(0).setDepth(re).setInteractive({useHandCursor:!0}),this.speedBtn.on("pointerup",()=>this.toggleSpeed()),this.speedBtn.on("pointerover",()=>this.speedBtn.setColor("#ffffff")),this.speedBtn.on("pointerout",()=>this.speedBtn.setColor("#ffff88"));const a=this.scene.add.text(i-185,26,"⏸",{fontSize:"22px",color:"#ffff88",fontFamily:"monospace"}).setOrigin(.5).setScrollFactor(0).setDepth(re).setInteractive({useHandCursor:!0});a.on("pointerup",()=>this.scene.events.emit("ui:toggle-pause")),a.on("pointerover",()=>a.setColor("#ffffff")),a.on("pointerout",()=>a.setColor("#ffff88")),this.waveBtn=this.scene.add.rectangle(i-90,26,160,38,2254370).setScrollFactor(0).setDepth(re).setInteractive({useHandCursor:!0}),this.waveBtnText=this.scene.add.text(i-90,26,"NEXT WAVE ▶",{fontSize:"14px",color:"#aaffaa",fontFamily:"monospace"}).setOrigin(.5).setScrollFactor(0).setDepth(re+1),this.waveBtn.on("pointerup",()=>this.scene.events.emit("ui:send-wave")),this.waveBtn.on("pointerover",()=>this.waveBtn.setFillStyle(3377203)),this.waveBtn.on("pointerout",()=>this.waveBtn.setFillStyle(2254370))}_redrawExpFill(t){if(!this.expBarFill)return;this.expBarFill.clear();const e=Math.floor(this.expBarWidth*t);if(e<=0)return;const s=4,i=Math.round(0+68*t),o=Math.round(170+85*t),n=i<<16|o<<8|255;this.expBarFill.fillStyle(n,1),this.expBarFill.fillRect(0,this.expBarY,e,s)}toggleSpeed(){this.speedMultiplier=this.speedMultiplier===1?2:1,this.speedBtn.setText(`⚡${this.speedMultiplier}×`),this.scene.events.emit("ui:speed-toggle",this.speedMultiplier)}}const ne=340,se=140,jt=200;class Nt{constructor(t,e){r(this,"scene");r(this,"towerSystem");r(this,"container",null);r(this,"onSelected");r(this,"onDeselected");this.scene=t,this.towerSystem=e,this.onSelected=s=>this.show(s),this.onDeselected=()=>this.hide(),l.on(p.TOWER_SELECTED,this.onSelected,this),l.on(p.TOWER_DESELECTED,this.onDeselected,this)}show(t){this.hide();const{width:e,height:s}=this.scene.cameras.main,i=e/2,o=s-se/2-10,a=[],n=this.scene.add.rectangle(0,0,ne,se,1118498,.92).setStrokeStyle(2,8947967);a.push(n);const c=t.placed.towerId.replace("tower-","").toUpperCase(),m=t.path?`Lv${t.level} (Path ${t.path})`:`Level ${t.level}`;a.push(this.scene.add.text(-ne/2+12,-se/2+10,`${c} — ${m}`,{fontSize:"16px",color:"#ffdd44",fontFamily:"monospace"}));const f=`DMG ${t.stats.damage} RATE ${t.stats.fireRate.toFixed(1)}/s RNG ${t.stats.range}`;a.push(this.scene.add.text(-ne/2+12,-se/2+32,f,{fontSize:"13px",color:"#aaaacc",fontFamily:"monospace"}));const g=t.placed.tower.getUpgradeCost(),y=g>0&&t.level<3,w=y?`UPGRADE 💰${g}`:"MAX LEVEL",E=this.scene.add.rectangle(-60,30,140,36,y?2245666:3355443).setStrokeStyle(1,y?4508740:5592405).setInteractive({useHandCursor:y});a.push(E);const T=this.scene.add.text(-60,30,w,{fontSize:"13px",color:y?"#88ff88":"#666666",fontFamily:"monospace"}).setOrigin(.5);a.push(T),y&&(E.on("pointerover",()=>E.setFillStyle(3368499)),E.on("pointerout",()=>E.setFillStyle(2245666)),E.on("pointerup",()=>{this.towerSystem.upgradeSelected();const P=this.towerSystem.getSelected();P?this.show({placed:P,level:P.tower.getLevel(),path:P.tower.getPath()??void 0,stats:{damage:P.tower.getDamage(),fireRate:P.tower.getFireRate(),range:P.tower.getRange()}}):this.hide()}));const x=t.placed.tower.getSellValue(),S=this.scene.add.rectangle(80,30,130,36,4465186).setStrokeStyle(1,13386820).setInteractive({useHandCursor:!0});a.push(S);const R=this.scene.add.text(80,30,`SELL 💰${x}`,{fontSize:"13px",color:"#ff8888",fontFamily:"monospace"}).setOrigin(.5);a.push(R),S.on("pointerover",()=>S.setFillStyle(6697779)),S.on("pointerout",()=>S.setFillStyle(4465186)),S.on("pointerup",()=>{this.towerSystem.sellSelected(),this.hide()});const D=this.scene.add.text(ne/2-14,-se/2+8,"✕",{fontSize:"16px",color:"#888888",fontFamily:"monospace"}).setOrigin(.5).setInteractive({useHandCursor:!0});a.push(D),D.on("pointerup",()=>{this.towerSystem.deselectAll(),this.hide()}),D.on("pointerover",()=>D.setColor("#ffffff")),D.on("pointerout",()=>D.setColor("#888888")),this.container=this.scene.add.container(i,o,a),this.container.setDepth(jt).setScrollFactor(0)}hide(){this.container&&(this.container.destroy(!0),this.container=null)}destroy(){l.off(p.TOWER_SELECTED,this.onSelected,this),l.off(p.TOWER_DESELECTED,this.onDeselected,this),this.hide()}}class Ht{constructor(t){r(this,"scene");r(this,"banner",null);this.scene=t}showNextWaveReady(t,e){this.showBanner(`⚔ Wave ${t} — Ready!`,`+💰${e} on completion`,2241314,2e3)}showWaveActive(t){this.showBanner(`⚔ Wave ${t} — INCOMING!`,"",3351057,1500)}showVictoryPending(){this.showBanner("🏆 All waves cleared!","Victory incoming…",2245666,2500)}destroy(){this.clearBanner()}showBanner(t,e,s,i){this.clearBanner();const{width:o}=this.scene.cameras.main,a=[],n=400,c=e?70:50,m=this.scene.add.rectangle(0,0,n,c,s,.88).setStrokeStyle(2,8947967);a.push(m),a.push(this.scene.add.text(0,e?-14:0,t,{fontSize:"20px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5)),e&&a.push(this.scene.add.text(0,14,e,{fontSize:"14px",color:"#aaffaa",fontFamily:"monospace"}).setOrigin(.5)),this.banner=this.scene.add.container(o/2,90,a),this.banner.setDepth(150).setScrollFactor(0).setAlpha(0),this.scene.tweens.add({targets:this.banner,alpha:1,duration:250,ease:"Power2",onComplete:()=>{this.scene.time.delayedCall(i,()=>{this.banner&&this.scene.tweens.add({targets:this.banner,alpha:0,duration:300,ease:"Power2",onComplete:()=>this.clearBanner()})})}})}clearBanner(){this.banner&&(this.banner.destroy(!0),this.banner=null)}}const A=class A{constructor(t){r(this,"container");r(this,"bg");r(this,"fill");r(this,"phaseMarker");r(this,"nameLabel");r(this,"hpLabel");r(this,"scene");r(this,"maxHp",100);r(this,"currentHp",100);this.scene=t,this.bg=t.add.rectangle(0,0,A.BAR_W+4,A.BAR_H+4,0,.85),this.fill=t.add.rectangle(-300,0,A.BAR_W,A.BAR_H,13378082,1).setOrigin(0,.5),this.phaseMarker=t.add.rectangle(0,0,3,A.BAR_H+6,16776960,1),this.nameLabel=t.add.text(0,-28,"BOSS",{fontSize:"14px",color:"#ffffff",fontStyle:"bold",fontFamily:"monospace"}).setOrigin(.5,1),this.hpLabel=t.add.text(A.BAR_W/2+8,0,"",{fontSize:"12px",color:"#ffcccc",fontFamily:"monospace"}).setOrigin(0,.5),this.container=t.add.container(A.BAR_X,A.BAR_Y,[this.bg,this.fill,this.phaseMarker,this.nameLabel,this.hpLabel]),this.container.setScrollFactor(0),this.container.setDepth(100),this.container.setVisible(!1),l.on(p.BOSS_PHASE_CHANGED,this.onPhaseChanged,this),l.on(p.BOSS_DIED,this.deactivate,this)}activate(t,e){this.maxHp=e,this.currentHp=e,this.nameLabel.setText(t.toUpperCase()),this.hpLabel.setText(`${e}`),this.fill.setFillStyle(13378082),this.fill.displayWidth=A.BAR_W,this.container.setVisible(!0)}setHP(t){this.currentHp=Math.max(0,t);const e=this.currentHp/this.maxHp;this.hpLabel.setText(`${Math.ceil(this.currentHp)}`),this.scene.tweens.add({targets:this.fill,displayWidth:A.BAR_W*e,duration:200,ease:"Linear"}),e<.3?this.fill.setFillStyle(16737792):e<.6?this.fill.setFillStyle(13386752):this.fill.setFillStyle(13378082),this.currentHp<=0&&this.deactivate()}deactivate(){this.container.setVisible(!1)}destroy(){l.off(p.BOSS_PHASE_CHANGED,this.onPhaseChanged,this),l.off(p.BOSS_DIED,this.deactivate,this),this.container.destroy()}onPhaseChanged(t){this.scene.tweens.add({targets:this.fill,alpha:{from:1,to:.3},yoyo:!0,repeat:3,duration:100})}};r(A,"BAR_W",600),r(A,"BAR_H",28),r(A,"BAR_X",640),r(A,"BAR_Y",24);let Oe=A;const _e=new Map(Be.towers.map(d=>[d.id,d])),B="monospace",N=2254370,qe=3377203,ie=4465186,Qe=5583667,Je=6702080,Gt=8939008,b=class b{constructor(t){r(this,"panel");r(this,"scene");r(this,"selectedTower",null);r(this,"titleText");r(this,"levelText");r(this,"upgradeBtnA");r(this,"upgradeBtnAText");r(this,"upgradeBtnB");r(this,"upgradeBtnBText");r(this,"sellBtn");r(this,"sellBtnText");r(this,"statDamage");r(this,"statRange");r(this,"statRate");r(this,"closeBtn");this.scene=t;const e=b.W,s=b.H,i=b.BTN_W,o=b.BTN_H,a=b.BTN_HLF,n=t.add.rectangle(0,0,e,s,1710638,.95).setStrokeStyle(2,4491519);this.titleText=t.add.text(0,-106,"TOWER",{fontSize:"14px",color:"#88aaff",fontStyle:"bold",fontFamily:B}).setOrigin(.5,0),this.levelText=t.add.text(0,-88,"Level 1",{fontSize:"11px",color:"#aaddff",fontFamily:B}).setOrigin(.5,0);const c=t.add.graphics();c.lineStyle(1,3359846,.8),c.lineBetween(-100,-68,e/2-10,-68);const m=-60;this.statDamage=t.add.text(-98,m,"DMG: —",{fontSize:"11px",color:"#ccddff",fontFamily:B}),this.statRange=t.add.text(-98,m+16,"RNG: —",{fontSize:"11px",color:"#ccddff",fontFamily:B}),this.statRate=t.add.text(-98,m+32,"SPD: —",{fontSize:"11px",color:"#ccddff",fontFamily:B});const f=-2;this.upgradeBtnA=t.add.rectangle(0,f,i,o,N).setStrokeStyle(1,4482730).setInteractive({useHandCursor:!0}),this.upgradeBtnAText=t.add.text(0,f,"UPGRADE 0g",{fontSize:"11px",color:"#aaffaa",fontFamily:B}).setOrigin(.5,.5),this.upgradeBtnA.on("pointerover",()=>{this.upgradeBtnA.setFillStyle(this.canAffordA()?qe:Qe)}),this.upgradeBtnA.on("pointerout",()=>{this.upgradeBtnA.setFillStyle(this.canAffordA()?N:ie)}),this.upgradeBtnA.on("pointerup",()=>this.onUpgradeA());const g=f+o+6;this.upgradeBtnB=t.add.rectangle(0,g,a*2,o,N).setStrokeStyle(1,4482730).setInteractive({useHandCursor:!0}).setVisible(!1),this.upgradeBtnBText=t.add.text(0,g,"",{fontSize:"11px",color:"#aaffaa",fontFamily:B}).setOrigin(.5,.5).setVisible(!1),this.upgradeBtnB.on("pointerover",()=>{this.upgradeBtnB.setFillStyle(this.canAffordB()?qe:Qe)}),this.upgradeBtnB.on("pointerout",()=>{this.upgradeBtnB.setFillStyle(this.canAffordB()?N:ie)}),this.upgradeBtnB.on("pointerup",()=>this.onUpgradeB());const y=82;this.sellBtn=t.add.rectangle(0,y,i,o,Je).setStrokeStyle(1,8939042).setInteractive({useHandCursor:!0}),this.sellBtnText=t.add.text(0,y,"SELL 0g",{fontSize:"11px",color:"#ffdd88",fontFamily:B}).setOrigin(.5,.5),this.sellBtn.on("pointerover",()=>this.sellBtn.setFillStyle(Gt)),this.sellBtn.on("pointerout",()=>this.sellBtn.setFillStyle(Je)),this.sellBtn.on("pointerup",()=>this.onSell()),this.closeBtn=t.add.text(e/2-6,-114,"✕",{fontSize:"12px",color:"#888888",fontFamily:B}).setOrigin(1,0).setInteractive({useHandCursor:!0}),this.closeBtn.on("pointerover",()=>this.closeBtn.setColor("#ffffff")),this.closeBtn.on("pointerout",()=>this.closeBtn.setColor("#888888")),this.closeBtn.on("pointerup",()=>this.close()),this.panel=t.add.container(0,0,[n,c,this.titleText,this.levelText,this.statDamage,this.statRange,this.statRate,this.upgradeBtnA,this.upgradeBtnAText,this.upgradeBtnB,this.upgradeBtnBText,this.sellBtn,this.sellBtnText,this.closeBtn]),this.panel.setScrollFactor(0),this.panel.setDepth(200),this.panel.setVisible(!1),l.on(p.GOLD_CHANGED,this.refreshAffordability,this)}open(t){this.selectedTower=t;const e=this.scene.cameras.main,s=t.x-e.scrollX,i=t.y-e.scrollY-130,o=u.Math.Clamp(s,b.W/2+10,1280-b.W/2-10),a=u.Math.Clamp(i,b.H/2+10,720-b.H/2-10);this.panel.setPosition(o,a),this.panel.setVisible(!0),this.refresh()}close(){this.selectedTower&&l.emit(p.TOWER_DESELECTED,{tower:this.selectedTower}),this.selectedTower=null,this.panel.setVisible(!1)}destroy(){l.off(p.GOLD_CHANGED,this.refreshAffordability,this),this.panel.destroy()}refresh(){const t=this.selectedTower;if(!t)return;const e=t.getLevel(),s=_e.get(t.getTowerId()),i=(s==null?void 0:s.name)??t.getTowerId().toUpperCase();this.titleText.setText(i.toUpperCase());const o=t.getPath()?` (${this.pathDisplayName(t.getTowerId(),t.getPath())})`:"";this.levelText.setText(`Level ${e}${o}`);const a=t.getDamage(),n=t.getRange(),c=t.getFireRate();this.statDamage.setText(`DMG: ${a>0?a:"—"}`),this.statRange.setText(`RNG: ${n}`),this.statRate.setText(`SPD: ${c>0?c.toFixed(1)+"/s":"—"}`);const m=t.getSellValue();if(this.sellBtnText.setText(`SELL ${m}g`),e>=3)this.setUpgradeAVisible(!1),this.upgradeBtnB.setVisible(!1),this.upgradeBtnBText.setVisible(!1);else if(e===2){const f=this.getL3Paths(t.getTowerId()),g=t.getUpgradeCost();f.length>=2?this.layoutTwoPathButtons(f[0],f[1],g):f.length===1?(this.layoutSingleUpgrade(`${f[0].name} ${g}g`,g),this.upgradeBtnB.setVisible(!1),this.upgradeBtnBText.setVisible(!1)):(this.layoutSingleUpgrade(`UPGRADE ${g}g`,g),this.upgradeBtnB.setVisible(!1),this.upgradeBtnBText.setVisible(!1))}else{const f=t.getUpgradeCost();this.layoutSingleUpgrade(`UPGRADE L2 ${f}g`,f),this.upgradeBtnB.setVisible(!1),this.upgradeBtnBText.setVisible(!1)}}refreshAffordability(t){this.selectedTower&&this.panel.visible&&this.refresh()}layoutSingleUpgrade(t,e){const s=_.gold>=e;this.upgradeBtnA.setSize(b.BTN_W,b.BTN_H),this.upgradeBtnA.setPosition(0,-2),this.upgradeBtnA.setFillStyle(s?N:ie),this.upgradeBtnAText.setPosition(0,-2),this.upgradeBtnAText.setText(t),this.upgradeBtnAText.setColor(s?"#aaffaa":"#ffaaaa"),this.setUpgradeAVisible(!0)}layoutTwoPathButtons(t,e,s){const i=_.gold>=s,o=b.BTN_HLF,a=b.BTN_H,n=-2;this.upgradeBtnA.setSize(o,a),this.upgradeBtnA.setPosition(-50,n),this.upgradeBtnA.setFillStyle(i?N:ie),this.upgradeBtnAText.setPosition(-50,n),this.upgradeBtnAText.setText(`${t.name}
+${s}g`),this.upgradeBtnAText.setColor(i?"#aaffaa":"#ffaaaa"),this.setUpgradeAVisible(!0),this.upgradeBtnA.setData("pathKey",t.key),this.upgradeBtnB.setSize(o,a),this.upgradeBtnB.setPosition(o/2+3,n),this.upgradeBtnB.setFillStyle(i?N:ie),this.upgradeBtnBText.setPosition(o/2+3,n),this.upgradeBtnBText.setText(`${e.name}
+${s}g`),this.upgradeBtnBText.setColor(i?"#aaffaa":"#ffaaaa"),this.upgradeBtnB.setData("pathKey",e.key),this.upgradeBtnB.setVisible(!0),this.upgradeBtnBText.setVisible(!0)}setUpgradeAVisible(t){this.upgradeBtnA.setVisible(t),this.upgradeBtnAText.setVisible(t)}onUpgradeA(){const t=this.selectedTower;if(!t)return;const e=t.getLevel();if(e>=3)return;const s=e===2?this.upgradeBtnA.getData("pathKey"):void 0;this.doUpgrade(t,s)}onUpgradeB(){const t=this.selectedTower;if(!t||t.getLevel()!==2)return;const e=this.upgradeBtnB.getData("pathKey");this.doUpgrade(t,e)}doUpgrade(t,e){const s=t.getUpgradeCost(e);_.gold({key:i,name:o.name})):[]}canAffordA(){const t=this.selectedTower;if(!t)return!1;const e=this.upgradeBtnA.getData("pathKey");return _.gold>=t.getUpgradeCost(e)}canAffordB(){const t=this.selectedTower;if(!t)return!1;const e=this.upgradeBtnB.getData("pathKey");return _.gold>=t.getUpgradeCost(e)}};r(b,"W",220),r(b,"H",240),r(b,"BTN_W",196),r(b,"BTN_H",28),r(b,"BTN_HLF",94);let Ie=b;class Ut{constructor(t){r(this,"circle");this.circle=t.add.graphics(),this.circle.setDepth(10),this.hide()}show(t,e,s,i=4491519){this.circle.clear(),this.circle.lineStyle(2,i,.8),this.circle.fillStyle(i,.15),this.circle.strokeCircle(t,e,s),this.circle.fillCircle(t,e,s),this.circle.setVisible(!0)}hide(){this.circle.clear(),this.circle.setVisible(!1)}destroy(){this.circle.destroy()}}const $t="modulepreload",Kt=function(d){return"/games/daveai-td2/"+d},Ze={},h=function(t,e,s){let i=Promise.resolve();if(e&&e.length>0){document.getElementsByTagName("link");const a=document.querySelector("meta[property=csp-nonce]"),n=(a==null?void 0:a.nonce)||(a==null?void 0:a.getAttribute("nonce"));i=Promise.allSettled(e.map(c=>{if(c=Kt(c),c in Ze)return;Ze[c]=!0;const m=c.endsWith(".css"),f=m?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${c}"]${f}`))return;const g=document.createElement("link");if(g.rel=m?"stylesheet":$t,m||(g.as="script"),g.crossOrigin="",g.href=c,n&&g.setAttribute("nonce",n),document.head.appendChild(g),m)return new Promise((y,w)=>{g.addEventListener("load",y),g.addEventListener("error",()=>w(new Error(`Unable to preload CSS for ${c}`)))})}))}function o(a){const n=new Event("vite:preloadError",{cancelable:!0});if(n.payload=a,window.dispatchEvent(n),!n.defaultPrevented)throw a}return i.then(a=>{for(const n of a||[])n.status==="rejected"&&o(n.reason);return t().catch(o)})},Yt=Object.assign({"../data/levels/world-01/level-01.json":()=>h(()=>import("./level-01-C_tB77dN.js"),[]),"../data/levels/world-01/level-02.json":()=>h(()=>import("./level-02-UoZTcMHJ.js"),[]),"../data/levels/world-01/level-03.json":()=>h(()=>import("./level-03-BdPUuNv5.js"),[]),"../data/levels/world-01/level-04.json":()=>h(()=>import("./level-04-BiZwQXaQ.js"),[]),"../data/levels/world-01/level-05.json":()=>h(()=>import("./level-05-CXZoxkyz.js"),[]),"../data/levels/world-01/level-06.json":()=>h(()=>import("./level-06-DAWpXHVq.js"),[]),"../data/levels/world-01/level-07.json":()=>h(()=>import("./level-07-CHD1Xsit.js"),[]),"../data/levels/world-02/level-01.json":()=>h(()=>import("./level-01-BNR7SWIG.js"),[]),"../data/levels/world-02/level-02.json":()=>h(()=>import("./level-02-CFzKI2Bx.js"),[]),"../data/levels/world-02/level-03.json":()=>h(()=>import("./level-03-Cw1TA8c_.js"),[]),"../data/levels/world-02/level-04.json":()=>h(()=>import("./level-04-DaaRq2NI.js"),[]),"../data/levels/world-02/level-05.json":()=>h(()=>import("./level-05-B2cAbMs8.js"),[]),"../data/levels/world-02/level-06.json":()=>h(()=>import("./level-06-YTa0-aYV.js"),[]),"../data/levels/world-02/level-07.json":()=>h(()=>import("./level-07-BZ53SnmN.js"),[]),"../data/levels/world-03/level-01.json":()=>h(()=>import("./level-01-Cz_67A_L.js"),[]),"../data/levels/world-03/level-02.json":()=>h(()=>import("./level-02-BJ08Y0-C.js"),[]),"../data/levels/world-03/level-03.json":()=>h(()=>import("./level-03-CGOkzcdq.js"),[]),"../data/levels/world-03/level-04.json":()=>h(()=>import("./level-04-C2N4BMop.js"),[]),"../data/levels/world-03/level-05.json":()=>h(()=>import("./level-05-C5duy23o.js"),[]),"../data/levels/world-03/level-06.json":()=>h(()=>import("./level-06-CqfzP3Te.js"),[]),"../data/levels/world-03/level-07.json":()=>h(()=>import("./level-07-BDbSIpdr.js"),[]),"../data/levels/world-04/level-01.json":()=>h(()=>import("./level-01-KVMcvgTB.js"),[]),"../data/levels/world-04/level-02.json":()=>h(()=>import("./level-02-DS4drlZU.js"),[]),"../data/levels/world-04/level-03.json":()=>h(()=>import("./level-03-BkdV1vQf.js"),[]),"../data/levels/world-04/level-04.json":()=>h(()=>import("./level-04-ZR_BL7MG.js"),[]),"../data/levels/world-04/level-05.json":()=>h(()=>import("./level-05-l19sR8Li.js"),[]),"../data/levels/world-04/level-06.json":()=>h(()=>import("./level-06-HQNRzw41.js"),[]),"../data/levels/world-04/level-07.json":()=>h(()=>import("./level-07-DpvV5IZY.js"),[]),"../data/levels/world-05/level-01.json":()=>h(()=>import("./level-01-xDmMuLen.js"),[]),"../data/levels/world-05/level-02.json":()=>h(()=>import("./level-02-BeJR54Bw.js"),[]),"../data/levels/world-05/level-03.json":()=>h(()=>import("./level-03-CVHVRBEC.js"),[]),"../data/levels/world-05/level-04.json":()=>h(()=>import("./level-04-CkDJrV7j.js"),[]),"../data/levels/world-05/level-05.json":()=>h(()=>import("./level-05-CsTw6-2u.js"),[]),"../data/levels/world-05/level-06.json":()=>h(()=>import("./level-06-Bbg5vx6K.js"),[]),"../data/levels/world-05/level-07.json":()=>h(()=>import("./level-07-b7nO1vnm.js"),[]),"../data/levels/world-06/level-01.json":()=>h(()=>import("./level-01-DkwEvOWw.js"),[]),"../data/levels/world-06/level-02.json":()=>h(()=>import("./level-02-BiQu3Buu.js"),[]),"../data/levels/world-06/level-03.json":()=>h(()=>import("./level-03-e4XqrG8p.js"),[]),"../data/levels/world-06/level-04.json":()=>h(()=>import("./level-04-BEZWIobi.js"),[]),"../data/levels/world-06/level-05.json":()=>h(()=>import("./level-05-CTSRgiRY.js"),[]),"../data/levels/world-06/level-06.json":()=>h(()=>import("./level-06-B72nC4xc.js"),[]),"../data/levels/world-06/level-07.json":()=>h(()=>import("./level-07-DrzsViWh.js"),[]),"../data/levels/world-08/level-01.json":()=>h(()=>import("./level-01-D89GdeZM.js"),[]),"../data/levels/world-08/level-02.json":()=>h(()=>import("./level-02-Dqh6s7wo.js"),[]),"../data/levels/world-08/level-03.json":()=>h(()=>import("./level-03-M-6qFCIU.js"),[]),"../data/levels/world-08/level-04.json":()=>h(()=>import("./level-04-CXLEOT8W.js"),[]),"../data/levels/world-08/level-05.json":()=>h(()=>import("./level-05-BkK7yQ74.js"),[]),"../data/levels/world-08/level-06.json":()=>h(()=>import("./level-06-ChSMcjFL.js"),[]),"../data/levels/world-08/level-07.json":()=>h(()=>import("./level-07-DC9t827L.js"),[]),"../data/levels/world-09/level-01.json":()=>h(()=>import("./level-01-Doz7SIZC.js"),[]),"../data/levels/world-09/level-02.json":()=>h(()=>import("./level-02-Cc_6Cmot.js"),[]),"../data/levels/world-09/level-03.json":()=>h(()=>import("./level-03-BARWTjBO.js"),[]),"../data/levels/world-09/level-04.json":()=>h(()=>import("./level-04-GvC9OC2-.js"),[]),"../data/levels/world-09/level-05.json":()=>h(()=>import("./level-05-BLm31U4F.js"),[]),"../data/levels/world-09/level-06.json":()=>h(()=>import("./level-06-BVt2zaY2.js"),[]),"../data/levels/world-09/level-07.json":()=>h(()=>import("./level-07-sS2jK4zz.js"),[]),"../data/levels/world-10/level-01.json":()=>h(()=>import("./level-01-Ds-bJBNu.js"),[]),"../data/levels/world-10/level-02.json":()=>h(()=>import("./level-02-B2M3hLuV.js"),[]),"../data/levels/world-10/level-03.json":()=>h(()=>import("./level-03-COCR62QH.js"),[]),"../data/levels/world-10/level-04.json":()=>h(()=>import("./level-04-DA5Ziyfr.js"),[]),"../data/levels/world-10/level-05.json":()=>h(()=>import("./level-05-z3OB-Eeu.js"),[]),"../data/levels/world-10/level-06.json":()=>h(()=>import("./level-06-BBoPuErM.js"),[]),"../data/levels/world-10/level-07.json":()=>h(()=>import("./level-07-DGoxVMN9.js"),[]),"../data/levels/world-11/level-01.json":()=>h(()=>import("./level-01-Cuww_JdD.js"),[]),"../data/levels/world-11/level-02.json":()=>h(()=>import("./level-02-kZfOdsyi.js"),[]),"../data/levels/world-11/level-03.json":()=>h(()=>import("./level-03-Dh-sXTp4.js"),[]),"../data/levels/world-11/level-04.json":()=>h(()=>import("./level-04-C4pRuHxe.js"),[]),"../data/levels/world-11/level-05.json":()=>h(()=>import("./level-05-BPvlwP0B.js"),[]),"../data/levels/world-11/level-06.json":()=>h(()=>import("./level-06-YLPOBwkX.js"),[]),"../data/levels/world-11/level-07.json":()=>h(()=>import("./level-07-DUX_DajD.js"),[]),"../data/levels/world-12/level-01.json":()=>h(()=>import("./level-01-Dj588ZtM.js"),[]),"../data/levels/world-12/level-02.json":()=>h(()=>import("./level-02-mb3jO4Gw.js"),[]),"../data/levels/world-12/level-03.json":()=>h(()=>import("./level-03-1UlIKD0Q.js"),[]),"../data/levels/world-12/level-04.json":()=>h(()=>import("./level-04-CFRxU1U-.js"),[]),"../data/levels/world-12/level-05.json":()=>h(()=>import("./level-05-B_Hy421Q.js"),[]),"../data/levels/world-12/level-06.json":()=>h(()=>import("./level-06-C_QV9Sef.js"),[]),"../data/levels/world-12/level-07.json":()=>h(()=>import("./level-07-CIN8Pdoi.js"),[]),"../data/levels/world-13/level-01.json":()=>h(()=>import("./level-01-yTjqiIVo.js"),[]),"../data/levels/world-13/level-02.json":()=>h(()=>import("./level-02-BY0K_c1c.js"),[]),"../data/levels/world-13/level-03.json":()=>h(()=>import("./level-03-MtnWjl04.js"),[]),"../data/levels/world-13/level-04.json":()=>h(()=>import("./level-04-ChxeRfoM.js"),[]),"../data/levels/world-13/level-05.json":()=>h(()=>import("./level-05-vX-1EazO.js"),[]),"../data/levels/world-13/level-06.json":()=>h(()=>import("./level-06-pumYmmG_.js"),[]),"../data/levels/world-13/level-07.json":()=>h(()=>import("./level-07-D64cZdPs.js"),[]),"../data/levels/world-14/level-01.json":()=>h(()=>import("./level-01-C6H48JrC.js"),[]),"../data/levels/world-14/level-02.json":()=>h(()=>import("./level-02-CBH7vq4l.js"),[]),"../data/levels/world-14/level-03.json":()=>h(()=>import("./level-03-CWvcUtIp.js"),[]),"../data/levels/world-14/level-04.json":()=>h(()=>import("./level-04-Di0_85nG.js"),[]),"../data/levels/world-14/level-05.json":()=>h(()=>import("./level-05-BKLUPjxr.js"),[]),"../data/levels/world-14/level-06.json":()=>h(()=>import("./level-06-DoXscGRM.js"),[]),"../data/levels/world-14/level-07.json":()=>h(()=>import("./level-07-C9KSb-8I.js"),[]),"../data/levels/world-15/level-01.json":()=>h(()=>import("./level-01-BzlEZR1x.js"),[]),"../data/levels/world-15/level-02.json":()=>h(()=>import("./level-02-DGYWlJPa.js"),[]),"../data/levels/world-15/level-03.json":()=>h(()=>import("./level-03-BaOc1NLt.js"),[]),"../data/levels/world-15/level-04.json":()=>h(()=>import("./level-04-Du7oKhdK.js"),[]),"../data/levels/world-15/level-05.json":()=>h(()=>import("./level-05-BmZzmr9R.js"),[]),"../data/levels/world-15/level-06.json":()=>h(()=>import("./level-06-CsNwuVY9.js"),[]),"../data/levels/world-15/level-07.json":()=>h(()=>import("./level-07-B8UAagNp.js"),[]),"../data/levels/world-16/level-01.json":()=>h(()=>import("./level-01-DgWkLZJS.js"),[]),"../data/levels/world-16/level-02.json":()=>h(()=>import("./level-02-ChtOl_yp.js"),[]),"../data/levels/world-16/level-03.json":()=>h(()=>import("./level-03-h-qfybgt.js"),[]),"../data/levels/world-16/level-04.json":()=>h(()=>import("./level-04-KrfSSPv2.js"),[]),"../data/levels/world-16/level-05.json":()=>h(()=>import("./level-05-1ilFD6pt.js"),[]),"../data/levels/world-16/level-06.json":()=>h(()=>import("./level-06-D49mP320.js"),[]),"../data/levels/world-16/level-07.json":()=>h(()=>import("./level-07-CJ7K6SPY.js"),[]),"../data/levels/world-17/level-01.json":()=>h(()=>import("./level-01-DNjwXgf5.js"),[]),"../data/levels/world-17/level-02.json":()=>h(()=>import("./level-02-Co3Y3obv.js"),[]),"../data/levels/world-17/level-03.json":()=>h(()=>import("./level-03-C8cOJ7jp.js"),[]),"../data/levels/world-17/level-04.json":()=>h(()=>import("./level-04-DXBA7uhw.js"),[]),"../data/levels/world-17/level-05.json":()=>h(()=>import("./level-05-DfAHphhm.js"),[]),"../data/levels/world-17/level-06.json":()=>h(()=>import("./level-06-Bcby5gGS.js"),[]),"../data/levels/world-17/level-07.json":()=>h(()=>import("./level-07-Df086Wg-.js"),[]),"../data/levels/world-18/level-01.json":()=>h(()=>import("./level-01-D35EemEB.js"),[]),"../data/levels/world-18/level-02.json":()=>h(()=>import("./level-02-DHYZ-5nK.js"),[]),"../data/levels/world-18/level-03.json":()=>h(()=>import("./level-03-CNNP6_n1.js"),[]),"../data/levels/world-18/level-04.json":()=>h(()=>import("./level-04-CRWxEHw-.js"),[]),"../data/levels/world-18/level-05.json":()=>h(()=>import("./level-05-CHz8-Ys0.js"),[]),"../data/levels/world-18/level-06.json":()=>h(()=>import("./level-06-6AIqzjqo.js"),[]),"../data/levels/world-18/level-07.json":()=>h(()=>import("./level-07-CrbRIWQ_.js"),[]),"../data/levels/world-19/level-01.json":()=>h(()=>import("./level-01-Blgz7yZo.js"),[]),"../data/levels/world-19/level-02.json":()=>h(()=>import("./level-02-CbK78oZb.js"),[]),"../data/levels/world-19/level-03.json":()=>h(()=>import("./level-03-DKO6TCSI.js"),[]),"../data/levels/world-19/level-04.json":()=>h(()=>import("./level-04-B4iQh-VK.js"),[]),"../data/levels/world-19/level-05.json":()=>h(()=>import("./level-05-upD4TVxi.js"),[]),"../data/levels/world-19/level-06.json":()=>h(()=>import("./level-06-VrjIfOw0.js"),[]),"../data/levels/world-19/level-07.json":()=>h(()=>import("./level-07-Dh_al5rY.js"),[]),"../data/levels/world-20/level-01.json":()=>h(()=>import("./level-01-1lOKw1ZR.js"),[]),"../data/levels/world-20/level-02.json":()=>h(()=>import("./level-02-Dx0ATAOF.js"),[]),"../data/levels/world-20/level-03.json":()=>h(()=>import("./level-03-Dq0YUr1G.js"),[]),"../data/levels/world-20/level-04.json":()=>h(()=>import("./level-04-qLljK_ws.js"),[]),"../data/levels/world-20/level-05.json":()=>h(()=>import("./level-05-DAAq8pkk.js"),[]),"../data/levels/world-20/level-06.json":()=>h(()=>import("./level-06-CSv2DSUp.js"),[]),"../data/levels/world-20/level-07.json":()=>h(()=>import("./level-07-CUDlohcI.js"),[]),"../data/levels/world-21/level-01.json":()=>h(()=>import("./level-01-BpdXI8_S.js"),[]),"../data/levels/world-21/level-02.json":()=>h(()=>import("./level-02-B0da9sR0.js"),[]),"../data/levels/world-21/level-03.json":()=>h(()=>import("./level-03-7NOr76kT.js"),[]),"../data/levels/world-21/level-04.json":()=>h(()=>import("./level-04-DseYTBF4.js"),[]),"../data/levels/world-21/level-05.json":()=>h(()=>import("./level-05-BbGbDCH3.js"),[]),"../data/levels/world-21/level-06.json":()=>h(()=>import("./level-06-B5aq1m-r.js"),[]),"../data/levels/world-21/level-07.json":()=>h(()=>import("./level-07-B_9t2tU4.js"),[]),"../data/levels/world-22/level-01.json":()=>h(()=>import("./level-01-25uGNvf7.js"),[]),"../data/levels/world-22/level-02.json":()=>h(()=>import("./level-02-DxiLbFsB.js"),[]),"../data/levels/world-22/level-03.json":()=>h(()=>import("./level-03-ZZJwDfHi.js"),[]),"../data/levels/world-22/level-04.json":()=>h(()=>import("./level-04-saMFLIo9.js"),[]),"../data/levels/world-22/level-05.json":()=>h(()=>import("./level-05-lojKLp2y.js"),[]),"../data/levels/world-22/level-06.json":()=>h(()=>import("./level-06-93KWENNM.js"),[]),"../data/levels/world-22/level-07.json":()=>h(()=>import("./level-07-B02aV1z0.js"),[]),"../data/levels/world-23/level-01.json":()=>h(()=>import("./level-01-DnicaP6L.js"),[]),"../data/levels/world-23/level-02.json":()=>h(()=>import("./level-02-BhSKlQZB.js"),[]),"../data/levels/world-23/level-03.json":()=>h(()=>import("./level-03-RAWFEPbU.js"),[]),"../data/levels/world-23/level-04.json":()=>h(()=>import("./level-04-B9QGDH3i.js"),[]),"../data/levels/world-23/level-05.json":()=>h(()=>import("./level-05-SOFH2cNi.js"),[]),"../data/levels/world-23/level-06.json":()=>h(()=>import("./level-06-DbfB9Yji.js"),[]),"../data/levels/world-23/level-07.json":()=>h(()=>import("./level-07-B7SMzz5c.js"),[]),"../data/levels/world-24/level-01.json":()=>h(()=>import("./level-01-DgiocTRl.js"),[]),"../data/levels/world-24/level-02.json":()=>h(()=>import("./level-02-Dk_HIC0h.js"),[]),"../data/levels/world-24/level-03.json":()=>h(()=>import("./level-03-Cm33UoKc.js"),[]),"../data/levels/world-24/level-04.json":()=>h(()=>import("./level-04-DdQPSGtz.js"),[]),"../data/levels/world-24/level-05.json":()=>h(()=>import("./level-05-DAzBe-oL.js"),[]),"../data/levels/world-24/level-06.json":()=>h(()=>import("./level-06-D8T7Wfez.js"),[]),"../data/levels/world-24/level-07.json":()=>h(()=>import("./level-07-COxPLkBk.js"),[]),"../data/levels/world-25/level-01.json":()=>h(()=>import("./level-01-GeRc2jVf.js"),[]),"../data/levels/world-25/level-02.json":()=>h(()=>import("./level-02-jc4T91zx.js"),[]),"../data/levels/world-25/level-03.json":()=>h(()=>import("./level-03-CZe_lqht.js"),[]),"../data/levels/world-25/level-04.json":()=>h(()=>import("./level-04-aiCPwTSZ.js"),[]),"../data/levels/world-25/level-05.json":()=>h(()=>import("./level-05-F-HvBjdP.js"),[]),"../data/levels/world-25/level-06.json":()=>h(()=>import("./level-06-G51qjiW9.js"),[]),"../data/levels/world-25/level-07.json":()=>h(()=>import("./level-07-CcQr2J3j.js"),[]),"../data/levels/world-26/level-01.json":()=>h(()=>import("./level-01-2U-kKtcy.js"),[]),"../data/levels/world-26/level-02.json":()=>h(()=>import("./level-02-CA3JCsMC.js"),[]),"../data/levels/world-26/level-03.json":()=>h(()=>import("./level-03-1gqmi-c2.js"),[]),"../data/levels/world-26/level-04.json":()=>h(()=>import("./level-04-BS54ZD5l.js"),[]),"../data/levels/world-26/level-05.json":()=>h(()=>import("./level-05-CYxA6iTA.js"),[]),"../data/levels/world-26/level-06.json":()=>h(()=>import("./level-06-DUDtfRgh.js"),[]),"../data/levels/world-26/level-07.json":()=>h(()=>import("./level-07-BLoL4zrN.js"),[]),"../data/levels/world-27/level-01.json":()=>h(()=>import("./level-01-D7hzpPMW.js"),[]),"../data/levels/world-27/level-02.json":()=>h(()=>import("./level-02-BZs-ATyq.js"),[]),"../data/levels/world-27/level-03.json":()=>h(()=>import("./level-03-D2pXEkxA.js"),[]),"../data/levels/world-27/level-04.json":()=>h(()=>import("./level-04-CAX1ABug.js"),[]),"../data/levels/world-27/level-05.json":()=>h(()=>import("./level-05-DPAz1rqa.js"),[]),"../data/levels/world-27/level-06.json":()=>h(()=>import("./level-06-DZJ7vlb_.js"),[]),"../data/levels/world-27/level-07.json":()=>h(()=>import("./level-07-BXf_yGmd.js"),[]),"../data/levels/world-28/level-01.json":()=>h(()=>import("./level-01-NEcNW_tg.js"),[]),"../data/levels/world-28/level-02.json":()=>h(()=>import("./level-02-CX06_L5l.js"),[]),"../data/levels/world-28/level-03.json":()=>h(()=>import("./level-03-BZy_FmET.js"),[]),"../data/levels/world-28/level-04.json":()=>h(()=>import("./level-04-BX037YHv.js"),[]),"../data/levels/world-28/level-05.json":()=>h(()=>import("./level-05-nIhHYWqb.js"),[]),"../data/levels/world-28/level-06.json":()=>h(()=>import("./level-06-CeZbb6CD.js"),[]),"../data/levels/world-28/level-07.json":()=>h(()=>import("./level-07-m9ebWp0x.js"),[]),"../data/levels/world-29/level-01.json":()=>h(()=>import("./level-01-DlXETsa4.js"),[]),"../data/levels/world-29/level-02.json":()=>h(()=>import("./level-02-B0A9jMbv.js"),[]),"../data/levels/world-29/level-03.json":()=>h(()=>import("./level-03-CKTmgbQy.js"),[]),"../data/levels/world-29/level-04.json":()=>h(()=>import("./level-04-Ep5jyKM3.js"),[]),"../data/levels/world-29/level-05.json":()=>h(()=>import("./level-05-qEr3zVtb.js"),[]),"../data/levels/world-29/level-06.json":()=>h(()=>import("./level-06-DmZuma_T.js"),[]),"../data/levels/world-29/level-07.json":()=>h(()=>import("./level-07-DP66zCb0.js"),[]),"../data/levels/world-30/level-01.json":()=>h(()=>import("./level-01-BnXa06pH.js"),[]),"../data/levels/world-30/level-02.json":()=>h(()=>import("./level-02-CfUtjzJY.js"),[]),"../data/levels/world-30/level-03.json":()=>h(()=>import("./level-03-CRfjVDON.js"),[]),"../data/levels/world-30/level-04.json":()=>h(()=>import("./level-04-d7E5I5zj.js"),[]),"../data/levels/world-30/level-05.json":()=>h(()=>import("./level-05-DCC6jkWF.js"),[]),"../data/levels/world-30/level-06.json":()=>h(()=>import("./level-06-DG68dGRh.js"),[]),"../data/levels/world-30/level-07.json":()=>h(()=>import("./level-07-BQYdF1vi.js"),[])}),k=class k{static async loadLevel(t){if(k.cache.has(t))return k.cache.get(t);const e=k.levelIdToPath(t);if(!e)return console.warn(`[LevelManager] Cannot resolve path for levelId: ${t}`),null;try{const s=`../data/levels/${k.levelIdToRelative(t)}`,i=Yt[s];if(!i)return console.warn(`[LevelManager] No bundled module for: ${s}`),null;const o=await i(),a=o.default??o;return k.cache.set(t,a),a}catch(s){return console.error(`[LevelManager] Failed to load level "${t}" from "${e}":`,s),null}}static levelIdToPath(t){const e=k.levelIdToRelative(t);return e?`src/data/levels/${e}`:""}static levelIdToRelative(t){const e=t.match(/^(world-\d{2})-level-(\d{2})$/);if(e)return`${e[1]}/level-${e[2]}.json`;const s=t.match(/^(world-\d{2})-boss$/);return s?`${s[1]}/level-06.json`:""}static getLevelIdsForWorld(t){if(t===7)return[];const e=`world-${String(t).padStart(2,"0")}`,s=[];for(let i=1;i<=6;i++)s.push(`${e}-level-${String(i).padStart(2,"0")}`);return s}static getLevelProgress(t){try{const e=localStorage.getItem(`level_progress_${t}`);return e?JSON.parse(e):null}catch{return null}}static saveLevelProgress(t,e){try{const s=k.getLevelProgress(t),i={levelId:t,stars:Math.max(e.stars,(s==null?void 0:s.stars)??0),bestLives:Math.max(e.bestLives,(s==null?void 0:s.bestLives)??0),bestGold:Math.max(e.bestGold,(s==null?void 0:s.bestGold)??0),completed:e.completed||((s==null?void 0:s.completed)??!1)};localStorage.setItem(`level_progress_${t}`,JSON.stringify(i))}catch(s){console.error("[LevelManager] Could not save level progress:",s)}}};r(k,"cache",new Map);let ke=k;const Xt={"freeze-all":(d,t=2500)=>{l.emit("ability:freeze-all",{duration:t})},"gold-multiplier":(d=3,t=12e3)=>{l.emit("ability:gold-multiplier",{value:d,duration:t})},"tower-speed-multiplier":(d=2,t=6e3)=>{l.emit("ability:tower-surge",{value:d,duration:t})},"aoe-damage":(d=300)=>{l.emit("ability:aoe-damage",{damage:d})},"restore-lives":(d=3)=>{l.emit(p.LIVES_CHANGED,d)},"stun-bosses":(d,t=4e3)=>{l.emit("ability:stun-bosses",{duration:t})}};class qt{constructor(t,e){r(this,"scene");r(this,"worldId");r(this,"abilities",[]);r(this,"waveCount",0);r(this,"onWaveCompleted",()=>{this.waveCount++;let t=!1;this.abilities.forEach(e=>{e.earnedEveryNWaves&&this.waveCount%e.earnedEveryNWaves===0&&(e.charges++,_.abilityCharges[e.id]=e.charges,t=!0)}),t&&l.emit("ability:charges-changed",this.getChargesSnapshot())});r(this,"onBossKilled",()=>{let t=!1;this.abilities.forEach(e=>{e.earnedOn==="boss-kill"&&(e.charges++,_.abilityCharges[e.id]=e.charges,t=!0)}),t&&l.emit("ability:charges-changed",this.getChargesSnapshot())});r(this,"onUseAbility",t=>{this.useAbility(t)});this.scene=t,this.worldId=e,!(e<21)&&(this.loadAbilitiesForWorld(),this.registerListeners())}getAbilities(){return this.abilities}useAbility(t){const e=this.abilities.find(s=>s.id===t);return!e||e.charges<=0?!1:(e.charges--,_.abilityCharges[t]=e.charges,h(()=>import("./items-BixPSnJ-.js"),[]).then(s=>{if(!this.scene.sys.isActive())return;const o=(s.default??s).find(a=>a.id===t);if(o){const a=Xt[o.effectType];a&&a(o.value,o.duration)}}).catch(()=>{}),l.emit("ability:charges-changed",this.getChargesSnapshot()),!0)}destroy(){l.off(p.WAVE_COMPLETED,this.onWaveCompleted,this),l.off(p.BOSS_DIED,this.onBossKilled,this),this.scene.events.off("ui:use-ability",this.onUseAbility,this)}loadAbilitiesForWorld(){const t=[{id:"ability-freeze-pulse",name:"Freeze Pulse",icon:"❄️",charges:0,earnedEveryNWaves:2},{id:"ability-gold-rush",name:"Gold Rush",icon:"💰",charges:0,earnedEveryNWaves:3},{id:"ability-tower-surge",name:"Tower Surge",icon:"⚡",charges:0,earnedEveryNWaves:3},{id:"ability-landslide",name:"Landslide",icon:"🪨",charges:0,earnedEveryNWaves:2},{id:"ability-healing-spring",name:"Healing Spring",icon:"💧",charges:0,earnedEveryNWaves:4},{id:"ability-boss-stun",name:"Boss Stun",icon:"💥",charges:0,earnedOn:"boss-kill"}],s={21:["ability-landslide","ability-freeze-pulse"],22:["ability-freeze-pulse","ability-tower-surge"],23:["ability-boss-stun","ability-gold-rush"],24:["ability-freeze-pulse","ability-landslide"],25:["ability-tower-surge","ability-healing-spring"],26:["ability-landslide","ability-gold-rush"],27:["ability-boss-stun","ability-freeze-pulse"],28:["ability-tower-surge","ability-landslide"],29:["ability-boss-stun","ability-healing-spring"],30:["ability-freeze-pulse","ability-gold-rush","ability-tower-surge","ability-landslide","ability-healing-spring","ability-boss-stun"]}[this.worldId]??[];this.abilities=t.filter(i=>s.includes(i.id)),this.abilities.forEach(i=>{_.abilityCharges[i.id]!==void 0&&(i.charges=_.abilityCharges[i.id])})}registerListeners(){l.on(p.WAVE_COMPLETED,this.onWaveCompleted,this),l.on(p.BOSS_DIED,this.onBossKilled,this),this.scene.events.on("ui:use-ability",this.onUseAbility,this)}getChargesSnapshot(){return Object.fromEntries(this.abilities.map(t=>[t.id,t.charges]))}}const Qt=2e3,Jt=100,Zt=500,es=1.25,ts=[{minCombo:50,multiplier:3},{minCombo:25,multiplier:2.5},{minCombo:10,multiplier:2},{minCombo:5,multiplier:1.5},{minCombo:1,multiplier:1}],ss=new Set([10,25,50]);class is{constructor(t){r(this,"scene");r(this,"state");r(this,"decayTimer");this.scene=t,this.state={count:0,multiplier:1,lastKillTime:0,expTotal:0,expForNextCharge:Zt},l.on(p.ENEMY_DIED,this.onEnemyKilled,this),this.decayTimer=this.scene.time.addEvent({delay:Jt,callback:this.tickDecay,callbackScope:this,loop:!0})}getState(){return{...this.state}}destroy(){l.off(p.ENEMY_DIED,this.onEnemyKilled,this),this.decayTimer&&this.decayTimer.remove(!1)}onEnemyKilled(t){const{x:e,y:s,reward:i,hp:o}=t,a=this.scene.time.now;this.state.count+=1,this.state.lastKillTime=a,this.state.multiplier=this.resolveMultiplier(this.state.count),l.emit("combo:updated",{count:this.state.count,multiplier:this.state.multiplier,x:e,y:s}),ss.has(this.state.count)&&l.emit("combo:fanfare",{milestone:this.state.count,x:e,y:s});const n=Math.floor(i*(this.state.multiplier-1));n>0&&l.emit("combo:gold-bonus",{bonus:n,x:e,y:s});const c=o>0&&i>0?o/i:1,m=i*.1*Math.min(c,2);this.awardExp(m)}awardExp(t){for(this.state.expTotal+=t;this.state.expTotal>=this.state.expForNextCharge;)this.state.expTotal-=this.state.expForNextCharge,this.state.expForNextCharge=Math.ceil(this.state.expForNextCharge*es),l.emit("exp:charge-earned",{});l.emit("exp:updated",{current:this.state.expTotal,max:this.state.expForNextCharge,percent:this.state.expTotal/this.state.expForNextCharge})}tickDecay(){if(this.state.count===0)return;this.scene.time.now>this.state.lastKillTime+Qt&&this.resetCombo()}resetCombo(){this.state.count=0,this.state.multiplier=1,l.emit("combo:reset",{})}resolveMultiplier(t){for(const e of ts)if(t>=e.minCombo)return e.multiplier;return 1}}const le=14,os=20,as=4,rs=1200,et=8e3,ns="Back.easeOut",ls=1.6,ds={"gold-coin":{color:16768256,emoji:"💰",label:"GOLD"},"speed-boost":{color:56831,emoji:"⚡",label:"SPEED"},shield:{color:4491519,emoji:"🛡️",label:"SHIELD"},bomb:{color:16729088,emoji:"💣",label:"BOMB"},heal:{color:4521796,emoji:"❤️",label:"HEAL"}},Ce=[{type:"gold-coin",chance:.25},{type:"speed-boost",chance:.08},{type:"shield",chance:.05},{type:"bomb",chance:.06},{type:"heal",chance:.04}];function hs(){const d=Math.random();let t=0;for(const e of Ce)if(t+=e.chance,d{const s=hs();s!==null&&this.spawnDrop(e.x??0,e.y??0,s)},this.onBossDied=e=>{const s=["heal","gold-coin",cs()],i=e.x??0,o=e.y??0;s.forEach((a,n)=>{this.scene.time.delayedCall(n*120,()=>{const c=ps(i,o,n,s.length);this.spawnDrop(c.x,c.y,a)})})},l.on(p.ENEMY_DIED,this.onEnemyKilled,this),l.on(p.BOSS_DIED,this.onBossDied,this)}spawnDrop(t,e,s){const i=ds[s],o=this.scene.add.circle(t,e,os,i.color,.25);o.setDepth(10);const a=this.scene.add.circle(t,e,le,i.color,1);a.setDepth(11),a.setStrokeStyle(2,16777215,.6);const n=this.scene.add.text(t,e-1,i.emoji,{fontSize:"14px",align:"center"});n.setOrigin(.5,.5),n.setDepth(12),a.setInteractive(new u.Geom.Circle(le,le,le+4),u.Geom.Circle.Contains),a.on(u.Input.Events.POINTER_OVER,()=>{g.collected||(this.scene.tweens.add({targets:[o],alpha:.55,scale:1.25,duration:120,ease:"Sine.easeOut"}),this.scene.input.setDefaultCursor("pointer"))}),a.on(u.Input.Events.POINTER_OUT,()=>{g.collected||(this.scene.tweens.add({targets:[o],alpha:.25,scale:1,duration:120,ease:"Sine.easeOut"}),this.scene.input.setDefaultCursor("default"))}),a.on(u.Input.Events.POINTER_DOWN,()=>{g.collected||this.collectDrop(g)}),a.setScale(0),o.setScale(0),n.setScale(0),this.scene.tweens.add({targets:[a,o,n],scale:1,duration:380,ease:ns}),this.scene.time.delayedCall(380,()=>{this.scene.tweens.add({targets:o,scale:1.4,alpha:0,duration:300,ease:"Sine.easeOut",onComplete:()=>{o.setAlpha(.25),o.setScale(1)}})});const c=this.scene.tweens.add({targets:[a,o,n],y:`+=${as}`,duration:rs,ease:"Sine.easeInOut",yoyo:!0,repeat:-1}),m=et-2e3,f=this.scene.tweens.add({targets:[a,o,n],alpha:0,duration:600,ease:"Sine.easeIn",delay:et-600,onStart:()=>{this.scene.tweens.add({targets:[a,o,n],alpha:.2,duration:160,ease:"Linear",yoyo:!0,repeat:5,delay:m})},onComplete:()=>{this.destroyDrop(g)}}),g={circle:a,label:n,glow:o,floatTween:c,expireTween:f,type:s,collected:!1};this.activeDrops.push(g)}destroy(){l.off(p.ENEMY_DIED,this.onEnemyKilled,this),l.off(p.BOSS_DIED,this.onBossDied,this),[...this.activeDrops].forEach(t=>this.destroyDrop(t)),this.activeDrops=[]}collectDrop(t){t.collected=!0,t.floatTween.stop(),t.expireTween.stop(),this.scene.input.setDefaultCursor("default"),this.applyEffect(t.type),this.scene.tweens.add({targets:[t.circle,t.glow,t.label],scale:ls,alpha:0,duration:260,ease:"Cubic.easeOut",onComplete:()=>{this.destroyDrop(t)}}),l.emit("drop:collected",{type:t.type})}applyEffect(t){switch(t){case"gold-coin":{const e=u.Math.Between(15,50);l.emit("drop:gold",{amount:e});break}case"speed-boost":l.emit("drop:speed-boost",{duration:8e3});break;case"shield":l.emit("drop:shield",{});break;case"bomb":l.emit("drop:bomb",{});break;case"heal":l.emit("drop:heal",{amount:2});break}}destroyDrop(t){var s,i,o,a,n;(s=t.floatTween)!=null&&s.isPlaying()&&t.floatTween.stop(),(i=t.expireTween)!=null&&i.isPlaying()&&t.expireTween.stop(),(o=t.circle)!=null&&o.active&&(t.circle.removeAllListeners(),t.circle.destroy()),(a=t.label)!=null&&a.active&&t.label.destroy(),(n=t.glow)!=null&&n.active&&t.glow.destroy();const e=this.activeDrops.indexOf(t);e!==-1&&this.activeDrops.splice(e,1)}}const us={damage:{color:"#ffffff",fontSize:18,bold:!1},crit:{color:"#ff4444",fontSize:26,bold:!0},exp:{color:"#44ffff",fontSize:16,bold:!1},gold:{color:"#ffdd00",fontSize:16,bold:!1},combo:{color:"#ff8800",fontSize:22,bold:!0},fanfare:{color:"#dd44ff",fontSize:36,bold:!0,stroke:"#ffffff",strokeThickness:4},powerup:{color:"#44ff44",fontSize:18,bold:!0},heal:{color:"#88ff88",fontSize:18,bold:!1}},fs={damage:1100,crit:1100,exp:1100,gold:1100,combo:800,fanfare:1800,powerup:1100,heal:1100},gs={damage:70,crit:80,exp:70,gold:70,combo:60,fanfare:0,powerup:70,heal:70},_s=20;class ys{constructor(t){r(this,"scene");r(this,"pool",[]);this.scene=t,this._preallocate(_s)}show(t,e,s,i){const o=this._acquire(),a=us[i],n=fs[i],c=gs[i];o.setText(s),o.setStyle({color:a.color,fontSize:`${a.fontSize}px`,fontStyle:a.bold?"bold":"normal",fontFamily:"Arial, sans-serif",stroke:a.stroke??"",strokeThickness:a.strokeThickness??0}),o.setDepth(1e3),i==="fanfare"?this._animateFanfare(o,n):i==="crit"?this._animateCrit(o,t,e,c,n):i==="combo"?this._animateCombo(o,t,e,c,n):this._animateDefault(o,t,e,c,n)}destroy(){for(const t of this.pool)this.scene.tweens.killTweensOf(t),t.destroy();this.pool=[]}_preallocate(t){for(let e=0;ethis._release(t,a,n)})}_animateCrit(t,e,s,i,o){const a=e,n=s,c=12;t.setPosition(a,n),t.setScale(1.3),t.setAlpha(1),this.scene.tweens.add({targets:t,y:n+c,scaleX:1,scaleY:1,duration:80,ease:"Quad.easeIn",onComplete:()=>{this.scene.tweens.add({targets:t,y:n-i,alpha:0,duration:o-80,ease:"Quad.easeOut",onComplete:()=>this._release(t,a,n)})}})}_animateCombo(t,e,s,i,o){const a=e,n=s-30,c=.4;t.setPosition(a,n),t.setScale(c),t.setAlpha(1),this.scene.tweens.add({targets:t,scaleX:1.2,scaleY:1.2,duration:120,ease:"Back.easeOut",onComplete:()=>{this.scene.tweens.add({targets:t,scaleX:1,scaleY:1,duration:80,ease:"Quad.easeOut",onComplete:()=>{this.scene.tweens.add({targets:t,y:n-i,alpha:0,duration:o-200,ease:"Quad.easeOut",onComplete:()=>this._release(t,a,n)})}})}})}_animateFanfare(t,e){const s=this.scene.cameras.main,i=s.scrollX+s.width/2,o=s.scrollY+s.height/2,a=i,n=o;t.setPosition(a,n),t.setScale(.5),t.setAlpha(0);const c=e-400,m=400;this.scene.tweens.add({targets:t,scaleX:1,scaleY:1,alpha:1,duration:300,ease:"Back.easeOut",onComplete:()=>{this.scene.tweens.add({targets:t,scaleX:1.05,scaleY:1.05,duration:Math.min(c/2,200),ease:"Sine.easeInOut",yoyo:!0,repeat:Math.floor(c/400),onComplete:()=>{this.scene.tweens.add({targets:t,alpha:0,scaleX:1.3,scaleY:1.3,duration:m,ease:"Quad.easeIn",onComplete:()=>this._release(t,a,n)})}})}})}}const ws=[{id:"first_blood",label:"🩸 FIRST BLOOD!",once:!0},{id:"boss_slayer",label:"💀 BOSS SLAYER!",once:!1},{id:"wave_5",label:"🌊 WAVE 5 CLEARED!",once:!0},{id:"wave_10",label:"🔟 WAVE 10 CLEARED!",once:!0},{id:"wave_master",label:"🏆 WAVE MASTER!",once:!0},{id:"gold_hoard",label:"💎 GOLD HOARD!",once:!0}];class vs{constructor(t,e){r(this,"scene");r(this,"floatingText");r(this,"earned",new Set);r(this,"killCount",0);r(this,"waveReached",0);r(this,"onEnemyKilled");r(this,"onBossDied");r(this,"onWaveCompleted");r(this,"onGoldChanged");this.scene=t,this.floatingText=e,this.onEnemyKilled=()=>{this.killCount+=1,this.killCount===1&&this.unlock("first_blood")},this.onBossDied=()=>{this.unlock("boss_slayer")},this.onWaveCompleted=s=>{this.waveReached+=1,this.waveReached===5&&this.unlock("wave_5"),this.waveReached===10&&this.unlock("wave_10")},this.onGoldChanged=s=>{s>=5e3&&this.unlock("gold_hoard")},l.on(p.ENEMY_DIED,this.onEnemyKilled,this),l.on(p.BOSS_DIED,this.onBossDied,this),l.on(p.WAVE_COMPLETED,this.onWaveCompleted,this),l.on(p.GOLD_CHANGED,this.onGoldChanged,this)}unlock(t){const e=ws.find(s=>s.id===t);e&&(e.once&&this.earned.has(t)||(e.once&&this.earned.add(t),this.showToast(e.label)))}destroy(){l.off(p.ENEMY_DIED,this.onEnemyKilled,this),l.off(p.BOSS_DIED,this.onBossDied,this),l.off(p.WAVE_COMPLETED,this.onWaveCompleted,this),l.off(p.GOLD_CHANGED,this.onGoldChanged,this)}showToast(t){const e=this.scene.cameras.main,s=e.scrollX+e.width*.5,i=e.scrollY+e.height*.72;this.floatingText.show(s,i,`🏅 ACHIEVEMENT
+${t}`,"powerup")}}let K=null;function fe(){return(!K||K.state==="closed")&&(K=new AudioContext),K.state==="suspended"&&K.resume(),K}function v(d,t,e,s,i,o,a=0,n){const c=d.currentTime+a/1e3,m=c+o/1e3,f=d.createGain();f.gain.setValueAtTime(0,c),f.gain.linearRampToValueAtTime(i,c+.004),f.gain.setTargetAtTime(0,m-.025,.012),f.connect(t);const g=d.createOscillator();g.type=e,g.frequency.setValueAtTime(s,c),n!==void 0&&g.frequency.linearRampToValueAtTime(n,m),g.connect(f),g.start(c),g.stop(m+.06)}function L(d,t,e,s,i,o,a=0){const n=d.currentTime+a/1e3,c=Math.ceil(d.sampleRate*(o/1e3)),m=d.createBuffer(1,c,d.sampleRate),f=m.getChannelData(0);for(let E=0;E{v(d,t,"square",380,e*.18,55)},crit:(d,t,e)=>{v(d,t,"square",780,e*.38,90,0,280),L(d,t,3500,.8,e*.22,60)},enemy_die:(d,t,e)=>{v(d,t,"sawtooth",280,e*.22,160,0,70)},boss_die:(d,t,e)=>{v(d,t,"sine",55,e*.55,700,0,28),v(d,t,"sawtooth",140,e*.3,450,40,38),L(d,t,2200,.5,e*.28,220,0),L(d,t,600,1.2,e*.35,500,60)},wave_start:(d,t,e)=>{v(d,t,"sine",75,e*.28,520,0,130),v(d,t,"sine",150,e*.18,320,180)},wave_complete:(d,t,e)=>{[523,659,784].forEach((i,o)=>v(d,t,"sine",i,e*.28,220,o*110))},victory:(d,t,e)=>{[523,587,659,698,784,880,988,1047].forEach((i,o)=>v(d,t,"sine",i,e*.3,280,o*90)),[523,659,784].forEach(i=>v(d,t,"sine",i/2,e*.15,900,200))},defeat:(d,t,e)=>{[440,415,392,370,349,330,311,293].forEach((i,o)=>v(d,t,"sine",i,e*.25,340,o*130)),v(d,t,"sawtooth",110,e*.2,1200,0,55)},tower_place:(d,t,e)=>{v(d,t,"sine",200,e*.32,130,0,90),L(d,t,800,1.5,e*.12,80)},tower_upgrade:(d,t,e)=>{v(d,t,"sine",659,e*.28,160,0),v(d,t,"sine",880,e*.34,200,150),v(d,t,"sine",1318,e*.22,180,310)},tower_sell:(d,t,e)=>{v(d,t,"sine",440,e*.24,220,0,300)}};function Fe(d,t=.28){var e;try{const s=fe(),i=s.createGain();i.gain.value=t,i.connect(s.destination),(e=ye[d])==null||e.call(ye,s,i,1),setTimeout(()=>{try{i.disconnect()}catch{}},3e3)}catch{}}const Es={hit:80,crit:120,enemy_die:100};class Ss{constructor(t){r(this,"muted",!1);r(this,"masterVol",.28);r(this,"lastPlayed",new Map);r(this,"_onDamage");r(this,"_onEnemyDie");r(this,"_onBossDie");r(this,"_onWaveStart");r(this,"_onWaveEnd");r(this,"_onPlace");r(this,"_onUpgrade");r(this,"_onSell");r(this,"_onSetVolume");fe(),this.masterVol=_.settings.sfxVolume,this._onDamage=e=>this._play(e.isCrit?"crit":"hit"),this._onEnemyDie=()=>this._play("enemy_die"),this._onBossDie=()=>this._play("boss_die"),this._onWaveStart=()=>this._play("wave_start"),this._onWaveEnd=()=>this._play("wave_complete"),this._onPlace=()=>this._play("tower_place"),this._onUpgrade=()=>this._play("tower_upgrade"),this._onSell=()=>this._play("tower_sell"),this._onSetVolume=e=>this.setVolume(e.volume),l.on(p.DAMAGE_DEALT,this._onDamage,this),l.on(p.ENEMY_DIED,this._onEnemyDie,this),l.on(p.BOSS_DIED,this._onBossDie,this),l.on(p.WAVE_STARTED,this._onWaveStart,this),l.on(p.WAVE_COMPLETED,this._onWaveEnd,this),l.on(p.TOWER_PLACED,this._onPlace,this),l.on(p.TOWER_UPGRADED,this._onUpgrade,this),l.on(p.TOWER_SOLD,this._onSell,this),l.on(p.SET_SFX_VOLUME,this._onSetVolume,this)}setMuted(t){this.muted=t}getMuted(){return this.muted}setVolume(t){this.masterVol=Math.max(0,Math.min(1,t))}getVolume(){return this.masterVol}destroy(){l.off(p.DAMAGE_DEALT,this._onDamage,this),l.off(p.ENEMY_DIED,this._onEnemyDie,this),l.off(p.BOSS_DIED,this._onBossDie,this),l.off(p.WAVE_STARTED,this._onWaveStart,this),l.off(p.WAVE_COMPLETED,this._onWaveEnd,this),l.off(p.TOWER_PLACED,this._onPlace,this),l.off(p.TOWER_UPGRADED,this._onUpgrade,this),l.off(p.TOWER_SOLD,this._onSell,this),l.off(p.SET_SFX_VOLUME,this._onSetVolume,this)}_play(t){if(this.muted)return;const e=Es[t];if(e!==void 0){const s=Date.now(),i=this.lastPlayed.get(t)??0;if(s-i{v(d,t,"sine",2400,e*.08,25)},ui_click:(d,t,e)=>{v(d,t,"square",1200,e*.14,35)},ui_menu_open:(d,t,e)=>{v(d,t,"sine",880,e*.18,100,0),v(d,t,"sine",1320,e*.16,120,80)},ui_menu_close:(d,t,e)=>{v(d,t,"sine",1320,e*.16,100,0),v(d,t,"sine",880,e*.14,120,80)}};class Ts{constructor(){r(this,"volume");r(this,"_onSetVolume");this.volume=_.settings.sfxVolume,this._onSetVolume=t=>{this.volume=Math.max(0,Math.min(1,t.volume))},l.on(p.SET_SFX_VOLUME,this._onSetVolume,this)}play(t){try{const e=fe(),s=e.createGain();s.gain.value=this.volume,s.connect(e.destination),bs[t](e,s,1),setTimeout(()=>{try{s.disconnect()}catch{}},1e3)}catch{}}setVolume(t){this.volume=Math.max(0,Math.min(1,t))}destroy(){l.off(p.SET_SFX_VOLUME,this._onSetVolume,this)}}const As={fire:(d,t,e)=>{L(d,t,800,1.2,e*.22,120),v(d,t,"sine",180,e*.12,100)},ice:(d,t,e)=>{L(d,t,4e3,2,e*.18,40),v(d,t,"sine",3200,e*.06,20)},lightning:(d,t,e)=>{v(d,t,"square",800,e*.2,60,0,200)},stone:(d,t,e)=>{v(d,t,"sine",120,e*.28,100),L(d,t,300,1.5,e*.15,150)},poison:(d,t,e)=>{v(d,t,"sine",300,e*.12,30,0),v(d,t,"sine",450,e*.12,30,35),v(d,t,"sine",600,e*.12,30,70)},magic:(d,t,e)=>{v(d,t,"triangle",1e3,e*.14,80,0,800),L(d,t,2e3,.8,e*.06,60)},physical:(d,t,e)=>{v(d,t,"sine",160,e*.06,50)}},Ds={burn:(d,t,e)=>{L(d,t,900,1,e*.1,60)},freeze:(d,t,e)=>{v(d,t,"sine",3600,e*.08,25),L(d,t,5e3,2,e*.06,30)},slow:(d,t,e)=>{v(d,t,"triangle",600,e*.08,60,0,400)},stun:(d,t,e)=>{v(d,t,"square",1400,e*.1,30)},poison:(d,t,e)=>{v(d,t,"sine",350,e*.08,25,0),v(d,t,"sine",500,e*.08,25,30)}};function xs(d,t,e){v(d,t,"sawtooth",220,e*.2,300,0),v(d,t,"sine",277,e*.18,280,20),v(d,t,"sine",330,e*.16,260,40),v(d,t,"triangle",415,e*.12,240,60),L(d,t,1800,.6,e*.24,180,0),L(d,t,600,1.5,e*.16,250,50)}const we=125;class Ps{constructor(){r(this,"volume");r(this,"lastImpact",0);r(this,"lastStatus",0);r(this,"lastSynergy",0);r(this,"_onImpact");r(this,"_onStatus");r(this,"_onSynergy");r(this,"_onSetVolume");this.volume=_.settings.sfxVolume,this._onImpact=t=>this.playImpactSound(t.element),this._onStatus=t=>this.playStatusSound(t.type),this._onSynergy=()=>this.playSynergySound(),this._onSetVolume=t=>{this.volume=Math.max(0,Math.min(1,t.volume))},l.on("vfx:impact-played",this._onImpact,this),l.on("vfx:status-applied",this._onStatus,this),l.on("vfx:synergy-played",this._onSynergy,this),l.on(p.SET_SFX_VOLUME,this._onSetVolume,this)}playImpactSound(t){if(t==="physical")return;const e=Date.now();e-this.lastImpact{try{s.disconnect()}catch{}},2e3)}catch{}}destroy(){l.off("vfx:impact-played",this._onImpact,this),l.off("vfx:status-applied",this._onStatus,this),l.off("vfx:synergy-played",this._onSynergy,this),l.off(p.SET_SFX_VOLUME,this._onSetVolume,this)}}function $(d){return Math.min(1,Math.log10(Math.max(1,d))/3)}let rt=1;function Ls(d){rt=Math.max(.1,Math.min(1,d))}function Rs(d,t,e){const s=1+$(d)*4,i=.5+Math.min(3.5,t/40),a={physical:1,fire:1.3,ice:1,lightning:.8,magic:1.1,poison:1.2,stone:1.1}[e];return Math.round(30*s*i*a*rt)}class Os{constructor(t){r(this,"scene");r(this,"camera");this.scene=t,this.camera=t.cameras.main}shake(t){const e=$(t);if(e<.4)return;const s=.002+e*.013,i=50+Math.round(e*250);this.camera.shake(i,s)}flash(t=16777215,e=.3,s=120){const i=t>>16&255,o=t>>8&255,a=t&255;this.camera.flash(s,i,o,a),this.camera.flashEffect.alpha=e}slowMotion(t=.3,e=1e3){this.scene.time.timeScale=t,this.scene.time.delayedCall(e*t,()=>{this.scene.time.timeScale=1})}zoomPulse(t=1.05,e=300){this.scene.tweens.add({targets:this.camera,zoom:t,duration:e/2,ease:"Quad.easeOut",yoyo:!0})}destroy(){}}const Is={physical:{fill:2236962,stroke:3355443,alpha:.5},fire:{fill:4465152,stroke:16737792,alpha:.6},ice:{fill:8965375,stroke:11197951,alpha:.4},lightning:{fill:3351040,stroke:16777028,alpha:.3},magic:{fill:4456550,stroke:10044671,alpha:.5},poison:{fill:2245632,stroke:4521728,alpha:.5},stone:{fill:3355443,stroke:5592405,alpha:.6}},ks=30;class Cs{constructor(t,e){r(this,"scene");r(this,"layer");r(this,"active",[]);this.scene=t,this.layer=e}isOnScreen(t,e,s=100){const i=this.scene.cameras.main;return t>=i.scrollX-s&&t<=i.scrollX+i.width+s&&e>=i.scrollY-s&&e<=i.scrollY+i.height+s}spawn(t,e,s,i){if(!this.isOnScreen(t,e))return;const o=$(s),a=8+Math.round(o*92),n=3e3+Math.round(o*4e3);this.active.length>=ks&&this.active.shift().gfx.destroy();const c=Is[i],m=this.scene.add.graphics();if(m.setDepth(1),m.fillStyle(c.fill,c.alpha),m.fillCircle(t,e,a),m.lineStyle(1.5,c.stroke,c.alpha*.8),m.strokeCircle(t,e,a),i==="physical"||i==="stone"){const f=3+Math.round(o*5);for(let g=0;g=0;e--){const s=this.active[e];s.ttl-=t;const i=s.ttl/s.maxTtl;i<.4&&s.gfx.setAlpha(i/.4),s.ttl<=0&&(s.gfx.destroy(),this.active.splice(e,1))}}destroy(){for(const t of this.active)t.gfx.destroy();this.active.length=0}}const tt={physical:"vfx-damage",fire:"vfx-fire",ice:"vfx-freeze",lightning:"vfx-lightning",magic:"vfx-damage",poison:"vfx-poison",stone:"vfx-stone"},Ms={"vfx-damage":10,"vfx-fire":19,"vfx-freeze":16,"vfx-lightning":14,"vfx-poison":40,"vfx-stone":18,"vfx-time":14,"vfx-defense":10},Bs=50;class Vs{constructor(t,e){r(this,"scene");r(this,"layer");r(this,"pool",[]);r(this,"animsRegistered",!1);this.scene=t,this.layer=e,this.registerAnimations(),this.initPool()}isOnScreen(t,e,s=100){const i=this.scene.cameras.main;return t>=i.scrollX-s&&t<=i.scrollX+i.width+s&&e>=i.scrollY-s&&e<=i.scrollY+i.height+s}playImpact(t,e,s,i){if(!this.isOnScreen(t,e))return;const o=this.acquire();if(!o)return;const n=.3+$(s)*2.2,c=tt[i];o.setPosition(t,e),o.setScale(n),o.setAlpha(1),o.setDepth(10),i==="magic"?o.setTint(10044671):o.clearTint(),o.setActive(!0).setVisible(!0),this.scene.anims.exists(c)?(o.play(c),o.once(u.Animations.Events.ANIMATION_COMPLETE,()=>{this.release(o)})):this.scene.tweens.add({targets:o,alpha:0,scaleX:n*1.5,scaleY:n*1.5,duration:200,onComplete:()=>this.release(o)})}playStatusLoop(t,e,s=-20){const i=this.acquire();if(!i)return null;const o=tt[e];return i.setScale(.5),i.setAlpha(.8),i.setDepth(15),i.setPosition(t.x,t.y+s),e==="magic"?i.setTint(10044671):i.clearTint(),i.setActive(!0).setVisible(!0),this.scene.anims.exists(o)&&i.play({key:o,repeat:-1}),i}release(t){t.stop(),t.setActive(!1).setVisible(!1),t.clearTint()}acquire(){for(const t of this.pool)if(!t.active)return t;if(this.pool.length>0){const t=this.pool[0];return this.release(t),t}return null}initPool(){for(let t=0;t0&&!this.scene.anims.exists(t)&&this.scene.anims.create({key:t,frames:i,frameRate:24,repeat:0})}this.animsRegistered=!0}}destroy(){for(const t of this.pool)t.destroy();this.pool.length=0}}const zs={physical:[8947848,6710886,11184810,4473924],fire:[16737792,16729088,16755200,16720384,13382400],ice:[8965375,11197951,6728430,13426175],lightning:[16777215,16777096,15658751,16777164],magic:[10044671,7807709,12281599,6689228],poison:[4521728,2280448,8978244,43520],stone:[8939076,6702114,11176038,4469538]},ve="__vfx_spark",Y="__vfx_soft",de="__vfx_pixel",me=class me{constructor(t,e){r(this,"scene");r(this,"layer");r(this,"activeEmitters",0);this.scene=t,this.layer=e,this.generateTextures()}isOnScreen(t,e,s=100){const i=this.scene.cameras.main;return t>=i.scrollX-s&&t<=i.scrollX+i.width+s&&e>=i.scrollY-s&&e<=i.scrollY+i.height+s}emitExplosion(t,e,s,i,o=0){if(!this.isOnScreen(t,e))return;const a=Rs(s,o,i),n=$(s),c=zs[i],m=20+n*80;this.emitBurst(t,e,Y,{quantity:Math.max(3,Math.round(a*.05)),speed:{min:200,max:400+n*300},lifespan:150,scale:{start:.3,end:1.5},alpha:{start:.8,end:0},tint:16777215,gravityY:0}),this.emitBurst(t,e,de,{quantity:Math.round(a*.3),speed:{min:100,max:300+n*200},lifespan:{min:300,max:600},scale:{start:.5+n*.5,end:.1},alpha:{start:1,end:.3},tint:c,gravityY:300,angle:{min:0,max:360},rotate:{min:-180,max:180}}),["fire","magic","poison","lightning"].includes(i)?this.emitBurst(t,e,Y,{quantity:Math.round(a*.2),speed:{min:20,max:60+n*40},lifespan:{min:400,max:800},scale:{start:.8+n,end:2+n*2},alpha:{start:.6,end:0},tint:c,gravityY:-50}):this.emitBurst(t,e,de,{quantity:Math.round(a*.2),speed:{min:80,max:200+n*150},lifespan:{min:400,max:700},scale:{start:.3+n*.4,end:.1},alpha:{start:1,end:.2},tint:c,gravityY:400,angle:{min:220,max:320}}),this.emitBurst(t,e,Y,{quantity:Math.round(a*.25),speed:{min:10,max:30+n*20},lifespan:{min:800,max:1500},scale:{start:.5,end:2+n*2},alpha:{start:.35,end:0},tint:i==="fire"?[3355443,2236962]:c,gravityY:-30,emitZone:{source:new u.Geom.Circle(0,0,m*.3),type:"random"}}),this.emitBurst(t,e,ve,{quantity:Math.round(a*.2),speed:{min:5,max:40+n*30},lifespan:{min:1e3,max:2e3},scale:{start:.3,end:.1},alpha:{start:.9,end:0},tint:c,gravityY:20,emitZone:{source:new u.Geom.Circle(0,0,m),type:"random"}})}emitPuff(t,e,s,i,o=400){this.isOnScreen(t,e)&&this.emitBurst(t,e,Y,{quantity:s,speed:{min:20,max:80},lifespan:o,scale:{start:.4,end:1.2},alpha:{start:.6,end:0},tint:i,gravityY:-20})}emitBurst(t,e,s,i){if(this.activeEmitters>=me.MAX_EMITTERS)return;const{quantity:o,...a}=i,n=this.scene.add.particles(t,e,s,{...a,frequency:-1,emitting:!1});n.setDepth(12),this.layer.add(n),n.explode(o,0,0),this.activeEmitters++;const c=i.lifespan;let m;typeof c=="object"&&c!==null&&"max"in c?m=c.max:typeof c=="number"?m=c:m=2e3,this.scene.time.delayedCall(m+200,()=>{n.destroy(),this.activeEmitters--})}generateTextures(){const t=this.scene.textures;if(!t.exists(ve)){const e=this.scene.make.graphics({x:0,y:0});e.fillStyle(16777215,1),e.fillCircle(4,4,4),e.generateTexture(ve,8,8),e.destroy()}if(!t.exists(Y)){const e=this.scene.make.graphics({x:0,y:0});e.fillStyle(16777215,.8),e.fillCircle(8,8,8),e.fillStyle(16777215,.4),e.fillCircle(8,8,6),e.generateTexture(Y,16,16),e.destroy()}if(!t.exists(de)){const e=this.scene.make.graphics({x:0,y:0});e.fillStyle(16777215,1),e.fillRect(0,0,3,3),e.generateTexture(de,3,3),e.destroy()}}destroy(){}};r(me,"MAX_EMITTERS",60);let Me=me;const st={physical:16777215,fire:16737792,ice:8965375,lightning:16777130,magic:10044671,poison:4521728,stone:11176038},Fs=.4,Ws=.6,it={burn:"fire",freeze:"ice",slow:"ice",stun:"lightning",poison:"poison"},js=[{statusA:"freeze",statusB:"stun",reaction:{name:"Shatter Blast",colors:[11197951,13426175,16777215,8961006],flashColor:13426175,damage:200,element:"ice"}},{statusA:"freeze",statusB:"burn",reaction:{name:"Thermal Shock",colors:[16777215,14540287,16746564,11193599],flashColor:16777215,damage:250,element:"fire"}},{statusA:"poison",statusB:"burn",reaction:{name:"Acid Melt",colors:[4521728,8978244,16737792,16755200],flashColor:8978244,damage:220,element:"poison"}},{statusA:"slow",statusB:"stun",reaction:{name:"Amplified Shock",colors:[16777130,16777096,16777215,16772676],flashColor:16777130,damage:180,element:"lightning"}},{statusA:"poison",statusB:"stun",reaction:{name:"Overcharge",colors:[4521728,11206468,16777130,8978312],flashColor:11206536,damage:200,element:"lightning"}}];function Ns(d){for(const t of js)if(d.includes(t.statusA)&&d.includes(t.statusB))return t.reaction;return null}const Hs={burn:[16737792,16729088,16755200],freeze:[8965375,11197951,13426175],slow:[10044671,7807709,12281599],stun:[16777130,16777096,16777215],poison:[4521728,2280448,8978244]};class Gs{constructor(t,e){r(this,"scene");r(this,"spritePool");r(this,"particles");r(this,"decals");r(this,"cameraFX");r(this,"statusOverlays",new Map);r(this,"onDamageDealt",t=>{const e=$(t.damage);e<.2?this.particles.emitPuff(t.x,t.y,5+Math.round(e*20),16777215,200):(this.spritePool.playImpact(t.x,t.y,t.damage,"physical"),this.particles.emitPuff(t.x,t.y,10+Math.round(e*30),16777215,300)),t.isCrit&&this.cameraFX.flash(16777215,.2,80)});r(this,"onSplashDamage",t=>{this.playImpact({x:t.x,y:t.y,element:"stone",damage:t.damage,isCrit:!1,isAoE:!0,isMiss:!1,splashRadius:t.radius})});r(this,"onEnemyDied",t=>{t.enemyId,t.reward,t.hp,this.playDeathEffect(t.x,t.y)});r(this,"onBossDied",t=>{t.bossId,t.reward,this.playBossDeath(t.x,t.y)});r(this,"onBossPhaseChanged",t=>{t.bossId,t.description,t.abilities;const e=this.scene.cameras.main;this.playBossPhaseTransition(e.centerX,e.centerY)});r(this,"onStatusApplied",t=>{t.value,t.duration,this.attachStatusOverlay(t.type,t.enemy,t.x,t.y);const e=this.statusOverlays.get(t.enemy);if(e&&e.size>=2){const s=Array.from(e.keys()),i=Ns(s);i&&this.playSynergyReaction(i,t.x,t.y)}});r(this,"onStatusExpired",t=>{this.removeStatusOverlay(t.type,t.enemy)});r(this,"onEnemyCleanup",t=>{this.cleanupAllOverlays(t.enemy)});this.scene=t,this.spritePool=new Vs(t,e),this.particles=new Me(t,e),this.decals=new Cs(t,e),this.cameraFX=new Os(t),this.setupListeners()}playImpact(t){const e=$(t.damage);if(this.spritePool.playImpact(t.x,t.y,t.damage,t.element),this.particles.emitExplosion(t.x,t.y,t.damage,t.element,t.splashRadius),(e>=.3||t.isAoE||t.isMiss)&&this.decals.spawn(t.x,t.y,t.damage,t.element),e>=Fs&&this.cameraFX.shake(t.damage),e>=Ws||t.isCrit){const s=st[t.element];this.cameraFX.flash(s,.15+e*.2,100)}l.emit("vfx:impact-played",{element:t.element,damage:t.damage,x:t.x,y:t.y})}playDeathEffect(t,e,s="physical"){this.particles.emitPuff(t,e,15,st[s]??16777215,500),this.spritePool.playImpact(t,e,50,s)}playBossPhaseTransition(t,e){this.cameraFX.flash(16777215,.4,200),this.cameraFX.shake(400),this.particles.emitExplosion(t,e,500,"magic",100)}playBossDeath(t,e){this.cameraFX.flash(16777215,.6,300),this.cameraFX.shake(800),this.particles.emitExplosion(t,e,1e3,"fire",120),this.spritePool.playImpact(t,e,1e3,"fire"),this.scene.time.delayedCall(300,()=>{this.cameraFX.slowMotion(.3,1500)}),this.scene.time.delayedCall(500,()=>{this.particles.emitExplosion(t,e,800,"magic",100),this.decals.spawn(t,e,1e3,"fire")})}playSynergyReaction(t,e,s){this.particles.emitExplosion(e,s,t.damage,t.element,60),this.spritePool.playImpact(e,s,t.damage,t.element),this.cameraFX.flash(t.flashColor,.35,150),this.cameraFX.shake(t.damage),this.decals.spawn(e,s,t.damage,t.element);const i=t.colors[0]??16777215;this.particles.emitPuff(e,s,20,i,700),l.emit("vfx:synergy-played",{name:t.name,element:t.element})}update(t){this.decals.update(t),this.updateStatusOverlays()}updateStatusOverlays(){const t=[];for(const[e,s]of this.statusOverlays){const i=e;if(!i.active){t.push(e);continue}for(const o of s.values())o.sprite&&o.sprite.active&&o.sprite.setPosition(i.x,i.y-20)}for(const e of t)this.cleanupAllOverlays(e)}attachStatusOverlay(t,e,s,i){let o=this.statusOverlays.get(e);if(o||(o=new Map,this.statusOverlays.set(e,o)),o.has(t))return;const a=Hs[t]??16777215;let n=null;switch(t){case"burn":{const c=it[t]??"fire";n=this.spritePool.playStatusLoop(e,c,-20),this.particles.emitPuff(s,i,8,a,500);break}case"freeze":{const c=e.bodySprite;c&&c.setTint(8965375),this.spritePool.playImpact(s,i,30,"ice"),this.particles.emitPuff(s,i,10,a,600);break}case"slow":{n=this.spritePool.playStatusLoop(e,"ice",-20),n&&(n.setTint(10044671),n.setScale(.3),n.setAlpha(.6));const c=e.bodySprite;c&&c.setTint(12290303),this.particles.emitPuff(s,i,6,a,800);break}case"stun":{this.particles.emitPuff(s,i-15,12,a,600);break}case"poison":{const c=it[t]??"poison";n=this.spritePool.playStatusLoop(e,c,-15),n&&n.setScale(.3),this.particles.emitPuff(s,i,8,a,500);break}default:this.particles.emitPuff(s,i,6,16777215,400);break}o.set(t,{sprite:n,target:e,statusType:t})}removeStatusOverlay(t,e){const s=this.statusOverlays.get(e);if(!s)return;const i=s.get(t);if(i){if(i.sprite&&this.spritePool.release(i.sprite),t==="freeze"||t==="slow"){const o=e.bodySprite;o&&o.clearTint()}s.delete(t),s.size===0&&this.statusOverlays.delete(e)}}cleanupAllOverlays(t){const e=this.statusOverlays.get(t);if(!e)return;for(const i of e.values())i.sprite&&this.spritePool.release(i.sprite);const s=t.bodySprite;s&&s.clearTint(),e.clear(),this.statusOverlays.delete(t)}setupListeners(){l.on(p.DAMAGE_DEALT,this.onDamageDealt,this),l.on(p.PROJECTILE_SPLASH,this.onSplashDamage,this),l.on(p.ENEMY_DIED,this.onEnemyDied,this),l.on(p.BOSS_DIED,this.onBossDied,this),l.on(p.BOSS_PHASE_CHANGED,this.onBossPhaseChanged,this),l.on("vfx:status-applied",this.onStatusApplied,this),l.on("vfx:status-expired",this.onStatusExpired,this),l.on("vfx:enemy-cleanup",this.onEnemyCleanup,this)}destroy(){l.off(p.DAMAGE_DEALT,this.onDamageDealt,this),l.off(p.PROJECTILE_SPLASH,this.onSplashDamage,this),l.off(p.ENEMY_DIED,this.onEnemyDied,this),l.off(p.BOSS_DIED,this.onBossDied,this),l.off(p.BOSS_PHASE_CHANGED,this.onBossPhaseChanged,this),l.off("vfx:status-applied",this.onStatusApplied,this),l.off("vfx:status-expired",this.onStatusExpired,this),l.off("vfx:enemy-cleanup",this.onEnemyCleanup,this);for(const t of this.statusOverlays.values())for(const e of t.values())e.sprite&&this.spritePool.release(e.sprite);this.statusOverlays.clear(),this.spritePool.destroy(),this.particles.destroy(),this.decals.destroy(),this.cameraFX.destroy()}}class nt extends u.Scene{constructor(e="GameScene"){super({key:e});r(this,"sceneManager");r(this,"waveManager");r(this,"towerSystem");r(this,"enemySystem");r(this,"bossSystem");r(this,"abilitySystem");r(this,"comboSystem");r(this,"dropSystem");r(this,"floatingText");r(this,"achievementSystem");r(this,"audioSystem");r(this,"uiAudio");r(this,"vfxAudioBridge");r(this,"musicController");r(this,"vfxManager");r(this,"maxCombo",0);r(this,"totalKills",0);r(this,"hud");r(this,"towerPanel");r(this,"wavePanel");r(this,"bossHealthBar");r(this,"upgradeMenu");r(this,"radiusOverlay");r(this,"gold",150);r(this,"lives",20);r(this,"currentWave",0);r(this,"totalWaves",0);r(this,"isPaused",!1);r(this,"waves",[]);r(this,"levelStarConditions",[]);r(this,"map");r(this,"pathPoints",[]);r(this,"towerLayer");r(this,"enemyLayer");r(this,"projectileLayer");r(this,"effectLayer")}create(e){this.sceneManager=new ee(this),_.currentWorldId=e.worldId,_.currentLevelId=e.levelId,this.gold=_.gold=150,this.lives=_.lives=20,this.towerLayer=this.add.layer(),this.enemyLayer=this.add.layer(),this.projectileLayer=this.add.layer(),this.effectLayer=this.add.layer(),this.setupMap(e),this.pathPoints=this.map?X.extractPathFromMap(this.map):this.buildFallbackPath(),this.drawPathVisual(),H.registerAnimations(this),he.registerAnimations(this),this.enemySystem=new Ve(this,this.pathPoints),this.enemySystem.setupListeners(),this.bossSystem=new ze(this,this.enemySystem),this.bossSystem.setupListeners(),this.towerSystem=new q(this),this.hud=new Wt(this,0,this.gold,this.lives),this.towerPanel=new Nt(this,this.towerSystem),this.wavePanel=new Ht(this),this.upgradeMenu=new Ie(this),this.radiusOverlay=new Ut(this),this.bossHealthBar=new Oe(this),this.applyWorldTint(e.worldId),this.setupInput(),this.registerEvents(),this.cameras.main.fadeIn(400,0,0,0),this.loadLevelAndInit(e)}async loadLevelAndInit(e){const s=await ke.loadLevel(e.levelId);if(!this.sys.isActive())return;if((s==null?void 0:s.startGold)!=null&&(this.gold=_.gold=s.startGold,this.hud.updateGold(this.gold)),(s==null?void 0:s.startLives)!=null&&(this.lives=_.lives=s.startLives,this.hud.updateLives(this.lives)),this.levelStarConditions=(s==null?void 0:s.stars)??[],this.waves=(s==null?void 0:s.waves)??this.buildPlaceholderWaves(e.worldId,e.isBoss??!1),this.totalWaves=this.waves.length,this.waveManager=new bt(this,this.waves,this.pathPoints),this.hud.updateWave(0,this.totalWaves),e.worldId>=21){this.abilitySystem=new qt(this,e.worldId);const o=this.abilitySystem.getAbilities();this.hud.buildAbilityBar(o),l.on("ability:charges-changed",a=>{this.hud.updateAbilityCharges(a,this.abilitySystem.getAbilities())},this)}this.floatingText=new ys(this),this.comboSystem=new is(this),this.dropSystem=new ms(this),this.hud.buildExpBar(e.worldId>=21),this.achievementSystem=new vs(this,this.floatingText),this.audioSystem=new Ss(this),this.uiAudio=new Ts,this.vfxAudioBridge=new Ps,this.musicController=new _t(this),this.musicController.play(gt(e.worldId)),Ls({low:.3,medium:.6,high:1}[_.settings.quality]??1),this.vfxManager=new Gs(this,this.effectLayer),this.registerCombatEvents(),this.time.delayedCall(2e3,()=>{var o;return(o=this.waveManager)==null?void 0:o.startNextWave()})}setupMap(e){const s=`map-${e.worldKey}-${e.levelId}`;if(this.cache.tilemap.has(s)){this.map=this.make.tilemap({key:s});const i=this.map.addTilesetImage("tileset",`ts-${e.worldKey}`);i&&(this.map.createLayer("Ground",i,0,0),this.map.createLayer("Decoration",i,0,0))}else{const{width:i,height:o}=this.cameras.main;if(this.textures.exists("world-01-bg")){const a=this.add.image(0,0,"world-01-bg").setOrigin(0,0);a.setDisplaySize(i,o),a.setDepth(-10)}else{const a=this.add.graphics();a.fillStyle(1714714,1),a.fillRect(0,0,i,o),a.lineStyle(1,2767402,.4);for(let n=0;nthis.togglePause()),(s=this.input.keyboard)==null||s.addKey(u.Input.Keyboard.KeyCodes.P).on("down",()=>this.togglePause())}registerEvents(){this.events.on("ui:send-wave",()=>{var e;this.isPaused||(e=this.waveManager)==null||e.startNextWave()}),this.events.on("ui:toggle-pause",()=>this.togglePause()),this.events.on("ui:speed-toggle",e=>{this.time.timeScale=e}),l.on(p.GOLD_CHANGED,e=>{this.gold=Math.max(0,this.gold+e),_.gold=this.gold,this.hud.updateGold(this.gold)},this),l.on(p.LIVES_CHANGED,e=>{this.lives+=e,_.lives=this.lives,this.hud.updateLives(this.lives),this.lives<=0&&this.time.delayedCall(800,()=>this.sceneManager.transitionTo("DefeatScene",{worldId:_.currentWorldId,levelId:_.currentLevelId,waveReached:this.currentWave,totalWaves:this.totalWaves}))},this),l.on(p.WAVE_STARTED,e=>{var s;this.currentWave=e.wave,this.hud.updateWave(this.currentWave,this.totalWaves),this.hud.setWaveButtonVisible(!1),this.wavePanel.showWaveActive(this.currentWave),this.currentWave===this.totalWaves&&this.totalWaves>1&&((s=this.musicController)==null||s.hardCut("bossA"))},this),l.on(p.WAVE_COMPLETED,e=>{var s;if(l.emit(p.GOLD_CHANGED,e.reward),this.currentWave{this.hud.setWaveButtonVisible(!1),this.wavePanel.showVictoryPending(),this.time.delayedCall(1500,()=>this.sceneManager.transitionTo("VictoryScene",{worldId:_.currentWorldId,levelId:_.currentLevelId,gold:this.gold,lives:this.lives,maxCombo:this.maxCombo,totalKills:this.totalKills,stars:this.evaluateStars(this.levelStarConditions,this.lives)}))},this),l.on(p.TOWER_SELECTED,e=>{var s,i,o;(s=e==null?void 0:e.placed)!=null&&s.tower&&((i=this.upgradeMenu)==null||i.open(e.placed.tower),(o=this.radiusOverlay)==null||o.show(e.placed.tower.x,e.placed.tower.y,e.placed.tower.getRange()))},this),l.on(p.TOWER_DESELECTED,()=>{var e,s;(e=this.upgradeMenu)==null||e.close(),(s=this.radiusOverlay)==null||s.hide()},this),l.on("boss:spawned",e=>{var s;(s=this.bossHealthBar)==null||s.activate(e.bossName,e.maxHp)},this),l.on("boss:hp-update",e=>{var s;(s=this.bossHealthBar)==null||s.setHP(e.hp)},this),l.on(p.TOWER_SOLD,e=>{var s;if(e&&"tower"in e&&e.tower){const i=this.towerSystem;(s=i.removeTowerBySprite)==null||s.call(i,e.tower)}},this)}evaluateStars(e,s){for(let i=0;i=(\d+)$/);n&&(a=s>=parseInt(n[1],10))}if(a)return Math.min(3,e.length-i)}return 0}registerCombatEvents(){l.on("combo:updated",e=>{if(this.floatingText&&(e.count>this.maxCombo&&(this.maxCombo=e.count),e.count>=5)){const s=e.multiplier>1?`×${e.count} COMBO! ×${e.multiplier.toFixed(1)}`:`×${e.count} COMBO!`;this.floatingText.show(e.x,e.y-20,s,"combo")}},this),l.on("combo:fanfare",e=>{if(!this.floatingText)return;const s={10:"🔥 ON FIRE!",25:"⚡ UNSTOPPABLE!",50:"💥 LEGENDARY!!"};this.floatingText.show(e.x,e.y,s[e.milestone]??"INCREDIBLE!","fanfare")},this),l.on("combo:gold-bonus",e=>{var s;this.gold=Math.max(0,this.gold+e.bonus),_.gold=this.gold,this.hud.updateGold(this.gold),(s=this.floatingText)==null||s.show(e.x,e.y-40,`+${e.bonus}💰`,"gold")},this),l.on("exp:updated",e=>{this.hud.updateExpBar(e.percent)},this),l.on("exp:charge-earned",()=>{var e;this.events.emit("exp:charge-earned"),(e=this.floatingText)==null||e.show(this.cameras.main.centerX,this.cameras.main.centerY+60,"✨ ABILITY CHARGE!","powerup")},this),l.on("drop:gold",e=>{l.emit(p.GOLD_CHANGED,e.amount)},this),l.on("drop:heal",e=>{var s;l.emit(p.LIVES_CHANGED,e.amount),(s=this.floatingText)==null||s.show(this.cameras.main.centerX,this.cameras.main.centerY,`+${e.amount} ❤️`,"heal")},this),l.on("drop:speed-boost",e=>{var s;l.emit("ability:tower-surge",{value:1.5,duration:e.duration}),(s=this.floatingText)==null||s.show(this.cameras.main.centerX,this.cameras.main.centerY,"⚡ SPEED BOOST!","powerup")},this),l.on("drop:shield",()=>{var e;l.emit(p.LIVES_CHANGED,1),(e=this.floatingText)==null||e.show(this.cameras.main.centerX,this.cameras.main.centerY,"🛡️ SHIELD!","powerup")},this),l.on("drop:bomb",()=>{var e;l.emit("ability:aoe-damage",{damage:200}),(e=this.floatingText)==null||e.show(this.cameras.main.centerX,this.cameras.main.centerY,"💣 BOOM!","crit")},this),l.on(p.DAMAGE_DEALT,e=>{if(!this.floatingText)return;const s=e.isCrit?`${e.damage}!`:String(e.damage);this.floatingText.show(e.x,e.y,s,e.isCrit?"crit":"damage")},this),l.on(p.ENEMY_DIED,()=>{this.totalKills+=1},this)}togglePause(){var e,s;this.isPaused=!this.isPaused,this.isPaused?(this.physics.pause(),(e=this.waveManager)==null||e.pause()):(this.physics.resume(),(s=this.waveManager)==null||s.resume())}applyWorldTint(e){const i={1:8956552,2:14527095,3:8956637,4:14518323,5:6719590,6:10044620,7:16777215,8:8952217,9:3364266,10:11167283,11:4460868,12:7838122,13:11176004,14:2271948,15:7838156,16:13386786,17:7812010,18:13408563,19:4456550,20:6706602,21:4504388,22:13408597,23:10066380,24:10053375,25:2250154,26:1122884,27:3359846,28:13382417,29:1114146}[e];if(i!==void 0&&i!==16777215){const{width:o,height:a}=this.cameras.main,n=this.add.rectangle(o/2,a/2,o,a,i,.18);n.setScrollFactor(0),n.setDepth(0)}}drawPathVisual(){if(!this.textures.exists("road-tile-1"))return;const e=48;for(let s=0;si===0?e.moveTo(s.x,s.y):e.lineTo(s.x,s.y)),e.strokePath(),this.pathPoints.forEach((s,i)=>{e.fillStyle(16737792,.9),e.fillCircle(s.x,s.y,7),this.add.text(s.x+10,s.y-10,String(i),{fontSize:"10px",color:"#ff6600"})})}buildPlaceholderWaves(e,s){const i=e*1.3,o=s?1:5;return Array.from({length:o},(a,n)=>({waveNumber:n+1,spawnGroups:[{enemy:`enemy-${String(Math.min(e*2,40)).padStart(2,"0")}`,count:Math.floor(5+n*3*i),interval:Math.max(500,1200-n*100),delay:0}],reward:Math.floor(50+n*15*i),stars:[]}))}update(e,s){var i;this.isPaused||(this.enemySystem.update(e,s),this.bossSystem.update(e,s),this.towerSystem.update(e,s,this.enemySystem.getEnemiesByProgress()),(i=this.vfxManager)==null||i.update(s))}shutdown(){var e,s,i,o,a,n,c,m,f,g,y,w,E,T;this.hud.destroy(),this.towerPanel.destroy(),this.wavePanel.destroy(),(e=this.upgradeMenu)==null||e.destroy(),(s=this.radiusOverlay)==null||s.destroy(),(i=this.bossHealthBar)==null||i.destroy(),this.towerSystem.destroy(),this.enemySystem.destroy(),(o=this.waveManager)==null||o.destroy(),(a=this.abilitySystem)==null||a.destroy(),(n=this.comboSystem)==null||n.destroy(),(c=this.dropSystem)==null||c.destroy(),(m=this.floatingText)==null||m.destroy(),(f=this.achievementSystem)==null||f.destroy(),(g=this.audioSystem)==null||g.destroy(),(y=this.uiAudio)==null||y.destroy(),(w=this.vfxAudioBridge)==null||w.destroy(),(E=this.musicController)==null||E.destroy(),(T=this.vfxManager)==null||T.destroy(),this.events.off("ui:send-wave"),this.events.off("ui:toggle-pause"),this.events.off("ui:speed-toggle"),l.off(p.GOLD_CHANGED,void 0,this),l.off(p.LIVES_CHANGED,void 0,this),l.off(p.WAVE_STARTED,void 0,this),l.off(p.WAVE_COMPLETED,void 0,this),l.off(p.ALL_WAVES_COMPLETED,void 0,this),l.off("ability:charges-changed",void 0,this),l.off("combo:updated",void 0,this),l.off("combo:fanfare",void 0,this),l.off("combo:gold-bonus",void 0,this),l.off("exp:updated",void 0,this),l.off("exp:charge-earned",void 0,this),l.off("drop:gold",void 0,this),l.off("drop:heal",void 0,this),l.off("drop:speed-boost",void 0,this),l.off("drop:shield",void 0,this),l.off("drop:bomb",void 0,this),l.off(p.DAMAGE_DEALT,void 0,this),l.off(p.ENEMY_DIED,void 0,this),l.off(p.TOWER_SELECTED,void 0,this),l.off(p.TOWER_DESELECTED,void 0,this),l.off("boss:spawned",void 0,this),l.off("boss:hp-update",void 0,this),l.off(p.TOWER_SOLD,void 0,this)}}function Us(d){return d>=15?3:d>=8?2:d>=1?1:0}class $s extends u.Scene{constructor(){super({key:"VictoryScene"})}create(t){const{width:e,height:s}=this.cameras.main,i=new ee(this),o=`world-${String(t.worldId).padStart(2,"0")}`,a=t.stars??Us(t.lives);_.completeLevel(o,t.levelId,a,0),Fe("victory"),this.add.rectangle(e/2,s/2,e,s,662026);for(let w=0;w<60;w++){const E=u.Math.Between(0,e),T=u.Math.Between(0,s),x=u.Math.Between(1,3),S=this.add.circle(E,T,x,16768324,.6);this.tweens.add({targets:S,alpha:{from:.1,to:.9},duration:u.Math.Between(700,2200),yoyo:!0,repeat:-1,delay:u.Math.Between(0,2e3)})}this.time.delayedCall(200,()=>{for(let w=0;w<30;w++){const E=Math.PI*2/30*w,T=u.Math.Between(60,220),x=e/2+Math.cos(E)*5,S=s*.32+Math.sin(E)*5,R=this.add.circle(x,S,u.Math.Between(3,7),16768324,1);this.tweens.add({targets:R,x:x+Math.cos(E)*T,y:S+Math.sin(E)*T,alpha:0,scale:.2,duration:u.Math.Between(600,1100),ease:"Quad.easeOut",onComplete:()=>R.destroy()})}});const n=this.add.text(e/2,s*.18,"VICTORY!",{fontSize:"72px",color:"#ffdd44",fontFamily:"sans-serif",fontStyle:"bold",stroke:"#aa8800",strokeThickness:6}).setOrigin(.5).setScale(0).setAlpha(0);this.tweens.add({targets:n,scale:1,alpha:1,duration:500,ease:"Back.easeOut"});const c=s*.36,m=70;for(let w=0;w<3;w++){const E=w{this.tweens.add({targets:x,scaleX:1.3,scaleY:.8,duration:80,yoyo:!0,ease:"Quad.easeOut"})})}this.add.text(e/2,s*.5,`💰 Gold: ${t.gold} ❤️ Lives: ${t.lives}`,{fontSize:"22px",color:"#aaffaa",fontFamily:"monospace"}).setOrigin(.5).setAlpha(0),this.tweens.add({targets:this.children.getAt(this.children.length-1),alpha:1,duration:400,delay:1400});const f=(t.maxCombo??0)>=5,g=(t.totalKills??0)>0;if(f||g){const w=[];g&&w.push(`⚔️ Kills: ${t.totalKills}`),f&&w.push(`🔥 Best Combo: ×${t.maxCombo}`);const E=this.add.text(e/2,s*.58,w.join(" "),{fontSize:"18px",color:"#88ccff",fontFamily:"monospace"}).setOrigin(.5).setAlpha(0);this.tweens.add({targets:E,alpha:1,duration:400,delay:1700})}const y=s*.74;this.time.delayedCall(1800,()=>{this.makeBtn(e/2-140,y,"RETRY",3355443,"#aaaaaa",()=>i.transitionTo("GameScene",{worldId:t.worldId,worldKey:o,levelId:t.levelId})),this.makeBtn(e/2,y,"NEXT LEVEL",2245666,"#88ff88",()=>i.transitionTo("LevelSelectScene",{worldId:t.worldId,worldKey:o})),this.makeBtn(e/2+145,y,"MENU",2236996,"#8888ff",()=>i.transitionTo("MainMenuScene"))})}makeBtn(t,e,s,i,o,a){const n=this.add.rectangle(t,e,200,48,i).setStrokeStyle(1,8947848).setInteractive({useHandCursor:!0});this.add.text(t,e,s,{fontSize:"18px",color:o,fontFamily:"monospace"}).setOrigin(.5);const c=m=>u.Display.Color.ValueToColor(m).darken(15).color;n.on("pointerover",()=>n.setFillStyle(c(i))),n.on("pointerout",()=>n.setFillStyle(i)),n.on("pointerup",a)}}class Ks extends u.Scene{constructor(){super({key:"DefeatScene"})}create(t){const{width:e,height:s}=this.cameras.main,i=new ee(this),o=`world-${String(t.worldId).padStart(2,"0")}`;this.cameras.main.shake(600,.012),Fe("defeat"),this.add.rectangle(e/2,s/2,e,s,1639685);for(let c=0;c<25;c++){const m=u.Math.Between(0,e),f=u.Math.Between(-40,s*.4),g=this.add.circle(m,f,u.Math.Between(2,5),16729088,.85);u.Math.Between(60,180),this.tweens.add({targets:g,y:f+u.Math.Between(200,s),alpha:0,duration:u.Math.Between(1200,2800),ease:"Quad.easeIn",delay:u.Math.Between(0,1500),repeat:-1,onRepeat:()=>{g.setPosition(u.Math.Between(0,e),u.Math.Between(-40,0)),g.setAlpha(.85)},x:m+u.Math.Between(-30,30)})}const a=this.add.rectangle(e/2,s/2,e,s,16711680,.35);this.tweens.add({targets:a,alpha:0,duration:1e3,ease:"Quad.easeOut"});const n=this.add.text(e/2,s*.24,"DEFEATED",{fontSize:"72px",color:"#ff4444",fontFamily:"sans-serif",fontStyle:"bold",stroke:"#880000",strokeThickness:6}).setOrigin(.5).setAlpha(0).setScale(1.4);if(this.tweens.add({targets:n,alpha:1,scale:1,duration:400,ease:"Quad.easeOut",onComplete:()=>{this.tweens.add({targets:n,scaleX:1.04,scaleY:1.04,duration:700,yoyo:!0,repeat:-1,ease:"Sine.easeInOut"})}}),this.add.text(e/2,s*.4,"The enemies have broken through!",{fontSize:"20px",color:"#cc8888",fontFamily:"monospace"}).setOrigin(.5).setAlpha(0),this.tweens.add({targets:this.children.getAt(this.children.length-1),alpha:1,duration:400,delay:500}),t.waveReached!==void 0&&t.totalWaves!==void 0){const c=this.add.text(e/2,s*.5,`📊 Reached Wave ${t.waveReached} / ${t.totalWaves}`,{fontSize:"20px",color:"#ff9966",fontFamily:"monospace"}).setOrigin(.5).setAlpha(0);this.tweens.add({targets:c,alpha:1,duration:400,delay:750})}this.time.delayedCall(900,()=>{this.makeBtn(e/2-115,s*.66,"RETRY",8921634,"#ff8888",()=>i.transitionTo("GameScene",{worldId:t.worldId,worldKey:o,levelId:t.levelId})),this.makeBtn(e/2+115,s*.66,"MAIN MENU",2236996,"#8888ff",()=>i.transitionTo("MainMenuScene"))})}makeBtn(t,e,s,i,o,a){const n=this.add.rectangle(t,e,200,50,i).setStrokeStyle(1,8947848).setInteractive({useHandCursor:!0});this.add.text(t,e,s,{fontSize:"20px",color:o,fontFamily:"monospace"}).setOrigin(.5);const c=m=>u.Display.Color.ValueToColor(m).darken(15).color;n.on("pointerover",()=>n.setFillStyle(c(i))),n.on("pointerout",()=>n.setFillStyle(i)),n.on("pointerup",a)}}class Ys extends u.Scene{constructor(){super({key:"SettingsScene"})}create(){const{width:t,height:e}=this.cameras.main;this.add.rectangle(t/2,e/2,t,e,0,.85),this.add.rectangle(t/2,e/2,480,420,1710638).setStrokeStyle(2,8947967),this.add.text(t/2,e/2-170,"SETTINGS",{fontSize:"36px",color:"#ffdd44",fontFamily:"sans-serif",fontStyle:"bold"}).setOrigin(.5),this.add.text(t/2-180,e/2-80,"Music Volume",{fontSize:"20px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(0,.5);const s=Math.round(_.settings.musicVolume*100),i=this.add.text(t/2+100,e/2-80,`${s}%`,{fontSize:"20px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5);this.createSmallBtn(t/2+50,e/2-80,"-",()=>{_.settings.musicVolume=Math.max(0,_.settings.musicVolume-.1),i.setText(`${Math.round(_.settings.musicVolume*100)}%`),_.save(),l.emit(p.SET_MUSIC_VOLUME,{volume:_.settings.musicVolume})}),this.createSmallBtn(t/2+150,e/2-80,"+",()=>{_.settings.musicVolume=Math.min(1,_.settings.musicVolume+.1),i.setText(`${Math.round(_.settings.musicVolume*100)}%`),_.save(),l.emit(p.SET_MUSIC_VOLUME,{volume:_.settings.musicVolume})}),this.add.text(t/2-180,e/2-20,"SFX Volume",{fontSize:"20px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(0,.5);const o=Math.round(_.settings.sfxVolume*100),a=this.add.text(t/2+100,e/2-20,`${o}%`,{fontSize:"20px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5);this.createSmallBtn(t/2+50,e/2-20,"-",()=>{_.settings.sfxVolume=Math.max(0,_.settings.sfxVolume-.1),a.setText(`${Math.round(_.settings.sfxVolume*100)}%`),_.save(),l.emit(p.SET_SFX_VOLUME,{volume:_.settings.sfxVolume})}),this.createSmallBtn(t/2+150,e/2-20,"+",()=>{_.settings.sfxVolume=Math.min(1,_.settings.sfxVolume+.1),a.setText(`${Math.round(_.settings.sfxVolume*100)}%`),_.save(),l.emit(p.SET_SFX_VOLUME,{volume:_.settings.sfxVolume})}),this.add.text(t/2-180,e/2+40,"Quality",{fontSize:"20px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(0,.5);const n=["low","medium","high"],c=this.add.text(t/2+100,e/2+40,_.settings.quality.toUpperCase(),{fontSize:"18px",color:"#ffdd44",fontFamily:"monospace"}).setOrigin(.5);this.createSmallBtn(t/2+50,e/2+40,"<",()=>{const g=n.indexOf(_.settings.quality);_.settings.quality=n[Math.max(0,g-1)],c.setText(_.settings.quality.toUpperCase()),_.save()}),this.createSmallBtn(t/2+150,e/2+40,">",()=>{const g=n.indexOf(_.settings.quality);_.settings.quality=n[Math.min(n.length-1,g+1)],c.setText(_.settings.quality.toUpperCase()),_.save()});const m=this.add.rectangle(t/2,e/2+110,220,44,8921634).setInteractive({useHandCursor:!0}).setStrokeStyle(2,11158596);this.add.text(t/2,e/2+110,"RESET PROGRESS",{fontSize:"16px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(.5),m.on("pointerup",()=>{_.reset(),this.add.text(t/2,e/2+145,"Progress reset!",{fontSize:"14px",color:"#ff4444",fontFamily:"sans-serif"}).setOrigin(.5)});const f=this.add.rectangle(t/2,e/2+170,200,48,2250137).setInteractive({useHandCursor:!0}).setStrokeStyle(2,4487099);this.add.text(t/2,e/2+170,"CLOSE",{fontSize:"22px",color:"#ffffff",fontFamily:"sans-serif"}).setOrigin(.5),f.on("pointerover",()=>f.setFillStyle(3373004)),f.on("pointerout",()=>f.setFillStyle(2250137)),f.on("pointerup",()=>this.scene.stop()),this.input.on("pointerup",g=>{const y=Math.abs(g.x-t/2),w=Math.abs(g.y-e/2);(y>240||w>210)&&this.scene.stop()}),this.cameras.main.fadeIn(200,0,0,0)}createSmallBtn(t,e,s,i){const o=this.add.rectangle(t,e,36,36,3359846).setInteractive({useHandCursor:!0}).setStrokeStyle(1,8947967);return this.add.text(t,e,s,{fontSize:"20px",color:"#ffffff",fontFamily:"monospace"}).setOrigin(.5),o.on("pointerover",()=>o.setFillStyle(4478378)),o.on("pointerout",()=>o.setFillStyle(3359846)),o.on("pointerup",i),o}}class Xs extends nt{constructor(){super("EndlessScene")}create(t){super.create({worldId:(t==null?void 0:t.worldId)??7,worldKey:"world-07",levelId:"endless",isBoss:!1});const{width:e}=this.cameras.main;this.add.text(e/2,40,"ENDLESS MODE",{fontSize:"28px",color:"#ffaa44",fontFamily:"sans-serif",fontStyle:"bold"}).setOrigin(.5)}}const lt=64,qs=20,Qs=12,Js=lt*qs,Zs=lt*Qs,ei={type:u.AUTO,width:Js,height:Zs,parent:"game-container",backgroundColor:"#000000",scene:[ct,yt,wt,Et,St,nt,$s,Ks,Ys,Xs],physics:{default:"arcade",arcade:{debug:!1,gravity:{x:0,y:0}}},scale:{mode:u.Scale.FIT,autoCenter:u.Scale.CENTER_BOTH},audio:{disableWebAudio:!1}},ti=new u.Game(ei);window.__phaserGame=ti;window.__gameState=null;
diff --git a/vps/patches/daveai-production-runtime-20260731/windsurf.html b/vps/patches/daveai-production-runtime-20260731/windsurf.html
new file mode 100644
index 00000000000..d19784e47d0
--- /dev/null
+++ b/vps/patches/daveai-production-runtime-20260731/windsurf.html
@@ -0,0 +1,2375 @@
+
+
+
+
+
+
+ DaveAI — Ops Control Panel
+
+
+
+
+
+
+ ⚠ ALERT: — click to dismiss
+
+
+
+
+
CPU –
+
MEM –
+
UPTIME –
+
+
+
last poll –
+
↻ Refresh
+
🔔 Alerts
+
← DaveAI
+
+
+
+
+
+
+
+
+
+
+
+
🚨 Monitor
+
⌨ Command
+
🖥 Procs
+
📄 Logs
+
🧪 Tests
+
🚀 Deploy
+
💬 Discuss
+
+
+
+
+
+
+
+
+
+ Shell via /tools/shell/run
+ Common: pm2 list
+ · pm2 logs · docker ps · df -h · free -m · uptime
+
+
+
+ root@vps:~$
+
+ Run
+ ✕
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+ Status
+ PID
+ Restarts
+ CPU
+ Mem
+ Uptime
+ Actions
+
+
+
+
+
+
+
+
+
+
+ Name
+ Status
+ Ports
+ Actions
+
+
+
+
+
+
+
+
+
+
+
+ Service:
+ agent-brain
+ daveai-api
+ agentic-ui
+ edge-tts
+ litellm
+ zeroclaw
+
+ ↓ Follow
+ ✕ Clear
+
+
+
+
+
+
+
+
▶ Playwright
+ E2E
+
▶ PW Headless
+
▶ k6 Load
+
▶ k6
+ Smoke
+
▶ Lighthouse
+
▶ axe
+ Scan
+
Select a test to run
+
✕ Clear
+
+
+
+
+
+
+
+
+
+
+
🧪 Deploy → Staging
+
Push current codebase to staging environment. Tests run automatically before
+ promotion.
+
▶ Deploy to Staging
+
+
+
🚀 Deploy → Production
+
Promote to daveai.tech production. Requires staging to be green.
+
▶ Deploy to
+ Production
+
+
+
⏪ Rollback
+
Roll back to previous checkpoint. Lists recent deployments to choose from.
+
⏪ Rollback
+
+
+
📊 Checkpoint Status
+
Loading...
+
💰 Budget Status
+
Loading...
+
+
+
+
+
+
+
+
+
+ Agents:
+
+ supervisor
+ coder
+ asset
+ qa
+
+
+ coder
+ supervisor
+ qa
+ asset
+
+
+ Start Discussion
+ ↻ Refresh List
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vps/patches/deploy-accessibility-contract-20260725.sh b/vps/patches/deploy-accessibility-contract-20260725.sh
new file mode 100644
index 00000000000..7a924b52463
--- /dev/null
+++ b/vps/patches/deploy-accessibility-contract-20260725.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+candidate_dir="${DAVEAI_CANDIDATE_DIR:-/tmp/daveai-accessibility-20260725}"
+candidate_html="$candidate_dir/daveai-ui-v6.html"
+candidate_css="$candidate_dir/daveai-v6.css"
+expected_html="a9dc0ebfb1e6e87f62329e642892e674a573bdddd70f8a0d5fd3408b826e3cff"
+expected_css="bca841c9633c7ef3420865fbdf41872ef4cd4130f71004842aa084b4f2246392"
+expected_cache_key="20260725-accessibility-1"
+
+test "$(sha256sum "$candidate_html" | awk '{print $1}')" = "$expected_html"
+test "$(sha256sum "$candidate_css" | awk '{print $1}')" = "$expected_css"
+grep -q "/assets/daveai-v6.css?v=$expected_cache_key" "$candidate_html"
+grep -q 'id="fp-tool-count"' "$candidate_html"
+grep -q 'function activateOnKeyboard(event)' "$candidate_html"
+
+stamp="$(date -u +%Y%m%dT%H%M%SZ)"
+backup="/opt/daveai/backups/${stamp}-accessibility-contract"
+mkdir -p "$backup"
+cp -a /var/www/agentic-website/index.html "$backup/index.html"
+cp -a /var/www/agentic-website/daveai-ui-v6.html "$backup/daveai-ui-v6.html"
+cp -a /var/www/agentic-website/assets/daveai-v6.css "$backup/daveai-v6.css"
+
+installed=0
+rollback() {
+ if [ "$installed" -eq 1 ]; then
+ cp -a "$backup/index.html" /var/www/agentic-website/index.html
+ cp -a "$backup/daveai-ui-v6.html" /var/www/agentic-website/daveai-ui-v6.html
+ cp -a "$backup/daveai-v6.css" /var/www/agentic-website/assets/daveai-v6.css
+ fi
+}
+trap rollback ERR
+
+install -o root -g root -m 0644 "$candidate_html" /var/www/agentic-website/daveai-ui-v6.html
+install -o www-data -g www-data -m 0644 "$candidate_html" /var/www/agentic-website/index.html
+install -o www-data -g www-data -m 0644 "$candidate_css" /var/www/agentic-website/assets/daveai-v6.css
+installed=1
+
+test "$(sha256sum /var/www/agentic-website/index.html | awk '{print $1}')" = "$expected_html"
+test "$(sha256sum /var/www/agentic-website/daveai-ui-v6.html | awk '{print $1}')" = "$expected_html"
+test "$(sha256sum /var/www/agentic-website/assets/daveai-v6.css | awk '{print $1}')" = "$expected_css"
+public_html="$(curl --fail --silent "https://daveai.tech/?release=$stamp")"
+grep -q "/assets/daveai-v6.css?v=$expected_cache_key" <<<"$public_html"
+grep -q 'id="fp-tool-count"' <<<"$public_html"
+grep -q 'function activateOnKeyboard(event)' <<<"$public_html"
+test "$(curl --fail --silent "https://daveai.tech/assets/daveai-v6.css?v=$expected_cache_key&release=$stamp" | sha256sum | awk '{print $1}')" = "$expected_css"
+curl --fail --silent https://api.daveai.tech/health | grep -q '"tools":120'
+
+trap - ERR
+printf 'backup=%s\n' "$backup"
+printf 'html_sha256=%s\n' "$expected_html"
+printf 'css_sha256=%s\n' "$expected_css"
+printf 'cache_key=%s\n' "$expected_cache_key"
+printf 'homepage=ok stylesheet=ok brain=ok accessibility_contract=ok\n'
diff --git a/vps/production-e2e-runner.cjs b/vps/production-e2e-runner.cjs
new file mode 100644
index 00000000000..2c8545366d9
--- /dev/null
+++ b/vps/production-e2e-runner.cjs
@@ -0,0 +1,262 @@
+#!/usr/bin/env node
+/* eslint-disable no-console */
+
+const fs = require('node:fs');
+const path = require('node:path');
+
+let chromium;
+try {
+ ({ chromium } = require('playwright'));
+} catch (error) {
+ console.error('Missing dependency: playwright. Install it or run with the Codex bundled NODE_PATH.');
+ console.error(error.message);
+ process.exit(1);
+}
+
+const base = process.env.DAVEAI_BASE_URL || 'https://daveai.tech';
+const proofDir = path.resolve(process.env.DAVEAI_PROOF_DIR || path.join('proofs', `daveai-prod-e2e-${Date.now()}`));
+const authEmail = process.env.DAVEAI_E2E_EMAIL || '';
+const authPassword = process.env.DAVEAI_E2E_PASSWORD || '';
+
+fs.mkdirSync(proofDir, { recursive: true });
+
+const results = [];
+
+function record(name, status, details = {}) {
+ results.push({ name, status, ...details });
+ const label = status === 'passed' ? 'PASS' : status === 'skipped' ? 'SKIP' : 'FAIL';
+ console.log(`${label} ${name}`);
+}
+
+async function newTrackedPage(browser, name) {
+ const page = await browser.newPage({ viewport: { width: 1365, height: 768 }, ignoreHTTPSErrors: true });
+ const trace = { badResponses: [], requestFailures: [], pageErrors: [], consoleMessages: [] };
+ page.on('response', (response) => {
+ const status = response.status();
+ if (status >= 400) trace.badResponses.push({ status, url: response.url() });
+ });
+ page.on('requestfailed', (request) => {
+ trace.requestFailures.push({
+ url: request.url(),
+ method: request.method(),
+ resourceType: request.resourceType(),
+ failure: request.failure()?.errorText || '',
+ });
+ });
+ page.on('pageerror', (error) => trace.pageErrors.push(String(error.message || error)));
+ page.on('console', (message) => {
+ const text = message.text();
+ if (/Failed to load resource|error|exception|uncaught|not found|502|404|401|ERR_FAILED/i.test(text)) {
+ trace.consoleMessages.push({ type: message.type(), text, location: message.location() });
+ }
+ });
+ page._daveTrace = trace;
+ page._proofName = name;
+ return page;
+}
+
+function assertCleanTrace(trace, allow = () => false) {
+ const badResponses = trace.badResponses.filter((item) => !allow(item));
+ const requestFailures = trace.requestFailures.filter((item) => {
+ // Chromium reports these when a page is reloaded, closed, or an iframe/game switches targets.
+ // They are not the same as a server/client failure such as 404, 502, CORS, DNS, or TLS.
+ if (/net::ERR_ABORTED/i.test(item.failure || '')) return false;
+ return !allow(item);
+ });
+ const consoleMessages = trace.consoleMessages.filter((item) => !/favicon|sandbox/i.test(item.text) && !allow(item));
+ return {
+ clean: badResponses.length === 0 && requestFailures.length === 0 && trace.pageErrors.length === 0 && consoleMessages.length === 0,
+ badResponses,
+ requestFailures,
+ pageErrors: trace.pageErrors,
+ consoleMessages,
+ };
+}
+
+async function screenshot(page, name) {
+ const file = path.join(proofDir, `${name}.png`);
+ await page.screenshot({ path: file, fullPage: true });
+ return file;
+}
+
+async function saveJson(name, payload) {
+ const file = path.join(proofDir, `${name}.json`);
+ fs.writeFileSync(file, JSON.stringify(payload, null, 2));
+ return file;
+}
+
+async function testHealth() {
+ const urls = [
+ `${base}/api/health`,
+ 'https://brain.daveai.tech/health',
+ 'https://voice.daveai.tech/health',
+ 'https://api.daveai.tech/health',
+ 'https://iptv.daveai.tech/api/provider-vault/providers',
+ ];
+ const checks = [];
+ for (const url of urls) {
+ const response = await fetch(url, { redirect: 'manual' });
+ checks.push({ url, status: response.status, contentType: response.headers.get('content-type'), body: (await response.text()).slice(0, 300) });
+ }
+ await saveJson('health-endpoints', checks);
+ const failed = checks.filter((check) => check.status !== 200);
+ record('health endpoints', failed.length ? 'failed' : 'passed', { checks, failed });
+}
+
+async function testUnauthChatGate(browser) {
+ const page = await newTrackedPage(browser, 'chat-auth-gate');
+ await page.goto(`${base}/?intro=off&e2e=auth-gate-${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ await page.evaluate(() => {
+ localStorage.removeItem('daveai_token');
+ localStorage.removeItem('daveai_token_ts');
+ });
+ await page.reload({ waitUntil: 'domcontentloaded' });
+ await page.waitForTimeout(3000);
+ const state = await page.evaluate(() => ({
+ authVisible: Boolean(document.querySelector('#auth-modal, .auth-modal, [data-auth-modal]')) || /sign in|create one|forgot password/i.test(document.body.innerText),
+ typing: /thinking/i.test(document.body.innerText),
+ bodyText: document.body.innerText.slice(0, 1000),
+ }));
+ const image = await screenshot(page, 'chat-auth-gate');
+ const trace = assertCleanTrace(page._daveTrace, (item) => /\/api\/health/.test(item.url || ''));
+ await saveJson('chat-auth-gate', { state, trace, image });
+ await page.close();
+ record('chat auth gate', state.authVisible && trace.clean ? 'passed' : 'failed', { state, trace, image });
+}
+
+async function testAuthenticatedChat(browser) {
+ if (!authEmail || !authPassword) {
+ record('authenticated chat reply', 'skipped', { reason: 'Set DAVEAI_E2E_EMAIL and DAVEAI_E2E_PASSWORD to run this proof.' });
+ return;
+ }
+ const page = await newTrackedPage(browser, 'authenticated-chat');
+ await page.goto(`${base}/?intro=off&e2e=chat-${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ await page.fill('input[type="email"], input[name="email"]', authEmail);
+ await page.fill('input[type="password"], input[name="password"]', authPassword);
+ await page.getByRole('button', { name: /sign in|log in|login/i }).first().click();
+ await page.waitForTimeout(3000);
+ const input = page.locator('#mc-input, textarea[placeholder*="Describe"], input[placeholder*="Describe"]').first();
+ await input.fill('Say exactly: production chat e2e ok');
+ await page.keyboard.press('Enter');
+ await page.waitForFunction(() => /production chat e2e ok|Ready/i.test(document.body.innerText), null, { timeout: 90000 }).catch(() => {});
+ await page.waitForTimeout(2000);
+ const state = await page.evaluate(() => ({ bodyText: document.body.innerText.slice(-2500), ready: /Ready/i.test(document.body.innerText) }));
+ const image = await screenshot(page, 'authenticated-chat');
+ const trace = assertCleanTrace(page._daveTrace);
+ await saveJson('authenticated-chat', { state, trace, image });
+ await page.close();
+ record('authenticated chat reply', state.ready && trace.clean ? 'passed' : 'failed', { state, trace, image });
+}
+
+async function testCarousel(browser) {
+ const page = await newTrackedPage(browser, 'carousel-launch');
+ await page.goto(`${base}/?intro=off&e2e=carousel-${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ await page.waitForTimeout(4000);
+ const launches = [];
+ for (let index = 0; index < 3; index += 1) {
+ await page.evaluate((i) => {
+ if (typeof window.launchCarouselGame === 'function') window.launchCarouselGame(i);
+ else document.querySelectorAll('[data-game-action="launch"], .game-card')[i]?.click();
+ }, index);
+ await page.waitForTimeout(3500);
+ launches.push(await page.evaluate(() => {
+ const iframe = document.querySelector('iframe#game-frame, iframe[src*="/games/"]');
+ return { iframeSrc: iframe?.src || '', bodyText: document.body.innerText.slice(0, 800) };
+ }));
+ await screenshot(page, `carousel-launch-${index}`);
+ }
+ const trace = assertCleanTrace(page._daveTrace);
+ await saveJson('carousel-launch', { launches, trace });
+ await page.close();
+ record('carousel launch', launches.every((item) => /\/games\//.test(item.iframeSrc)) && trace.clean ? 'passed' : 'failed', { launches, trace });
+}
+
+async function testCoreGames(browser) {
+ const games = [
+ { id: 'td2', url: `${base}/games/daveai-td2/index.html`, expect: /DaveAI TD2/i },
+ { id: 'asteroids', url: `${base}/games/asteroids/index.html`, expect: /Asteroids/i },
+ { id: 'siege', url: `${base}/games/daves-siege-td/index.html`, expect: /Siege TD|Dave's Siege/i },
+ ];
+ const gameResults = [];
+ for (const game of games) {
+ const page = await newTrackedPage(browser, `game-${game.id}`);
+ await page.goto(`${game.url}?e2e=${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ await page.waitForTimeout(9000);
+ const state = await page.evaluate(() => ({
+ title: document.title,
+ h1: document.querySelector('h1')?.innerText || '',
+ canvasCount: document.querySelectorAll('canvas').length,
+ buttonText: [...document.querySelectorAll('button')].map((button) => button.innerText || button.getAttribute('aria-label') || '').filter(Boolean).slice(0, 20),
+ bodyText: document.body.innerText.slice(0, 1200),
+ }));
+ const image = await screenshot(page, `game-${game.id}`);
+ const trace = assertCleanTrace(page._daveTrace, (item) => /WebGL|AudioContext|Autoplay/i.test(item.text || ''));
+ const passed = trace.clean && (state.canvasCount > 0 || game.expect.test(`${state.title}\n${state.h1}\n${state.bodyText}`));
+ gameResults.push({ game, state, trace, image, passed });
+ await page.close();
+ }
+ await saveJson('core-games', gameResults);
+ record('TD2/Asteroids/Siege runtime checks', gameResults.every((result) => result.passed) ? 'passed' : 'failed', { gameResults });
+}
+
+async function testNonGamePages(browser) {
+ const pages = [
+ { id: 'studio', url: `${base}/studio/index.html` },
+ { id: 'web-pages', url: `${base}/web-pages.html` },
+ { id: 'windsurf', url: `${base}/windsurf.html` },
+ { id: 'voice', url: 'https://voice.daveai.tech/' },
+ { id: 'voice-landing', url: `${base}/voice-landing.html` },
+ { id: 'streamhub', url: 'https://iptv.daveai.tech/' },
+ ];
+ const pageResults = [];
+ for (const item of pages) {
+ const page = await newTrackedPage(browser, `page-${item.id}`);
+ await page.goto(`${item.url}${item.url.includes('?') ? '&' : '?'}e2e=${Date.now()}`, { waitUntil: 'domcontentloaded', timeout: 45000 });
+ await page.waitForTimeout(5000);
+ const state = await page.evaluate(() => ({
+ title: document.title,
+ h1: document.querySelector('h1')?.innerText || '',
+ authGate: Boolean(document.querySelector('#windsurf-auth-gate')),
+ videoCount: document.querySelectorAll('video').length,
+ bodyText: document.body.innerText.slice(0, 1000),
+ }));
+ const image = await screenshot(page, `page-${item.id}`);
+ const trace = assertCleanTrace(page._daveTrace);
+ pageResults.push({ item, state, trace, image, passed: trace.clean });
+ await page.close();
+ }
+ await saveJson('non-game-pages', pageResults);
+ record('non-game pages', pageResults.every((result) => result.passed) ? 'passed' : 'failed', { pageResults });
+ const streamhub = pageResults.find((result) => result.item.id === 'streamhub');
+ record('StreamHub no-error render', streamhub && streamhub.passed && streamhub.state.videoCount > 0 ? 'passed' : 'failed', { streamhub });
+}
+
+(async () => {
+ const browser = await chromium.launch({ headless: true });
+ try {
+ await testHealth();
+ await testUnauthChatGate(browser);
+ await testAuthenticatedChat(browser);
+ await testCarousel(browser);
+ await testCoreGames(browser);
+ await testNonGamePages(browser);
+ } finally {
+ await browser.close();
+ }
+
+ const summary = {
+ generatedAt: new Date().toISOString(),
+ base,
+ proofDir,
+ results,
+ passed: results.filter((result) => result.status === 'passed').length,
+ failed: results.filter((result) => result.status === 'failed').length,
+ skipped: results.filter((result) => result.status === 'skipped').length,
+ };
+ await saveJson('summary', summary);
+ console.log(JSON.stringify(summary, null, 2));
+ process.exit(summary.failed === 0 ? 0 : 2);
+})().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/vps/proofs/DAVEAI_E2E_COMPLETION_20260724.md b/vps/proofs/DAVEAI_E2E_COMPLETION_20260724.md
new file mode 100644
index 00000000000..87b2dd2c15e
--- /dev/null
+++ b/vps/proofs/DAVEAI_E2E_COMPLETION_20260724.md
@@ -0,0 +1,254 @@
+# DaveAI End-to-End Completion Report
+
+- Date: 2026-07-25
+- Production: https://daveai.tech/
+- Brain: healthy, version 4.0.0
+- Canonical UI source SHA-256: `a9dc0ebfb1e6e87f62329e642892e674a573bdddd70f8a0d5fd3408b826e3cff`
+- Canonical CSS source SHA-256: `bca841c9633c7ef3420865fbdf41872ef4cd4130f71004842aa084b4f2246392`
+- Last verified production UI SHA-256: `a9dc0ebfb1e6e87f62329e642892e674a573bdddd70f8a0d5fd3408b826e3cff`
+- Last verified production CSS SHA-256: `bca841c9633c7ef3420865fbdf41872ef4cd4130f71004842aa084b4f2246392`
+- Accessibility deployment backup: `/opt/daveai/backups/20260725T104457Z-accessibility-contract`
+
+## Outcome
+
+The DaveAI website shell, project-building workflow, voice workflow, files
+workspace, agent routing, approval gates, project isolation, and production
+preview path have completed their final end-to-end pass. No known website-level
+wiring defect remains in the audited surfaces.
+
+The root page and V6 page contain the same deployed HTML. The Brain reports 120
+live runtime tools and four agents: Supervisor, Coder, Asset, and QA.
+
+The production UI is byte-identical to both canonical repository copies. The
+accessibility follow-up below is deployed with matching production hashes and
+the `20260725-accessibility-1` stylesheet release key.
+
+A source-of-truth verifier blocks deployment when the UI copies, the responsive
+stylesheet contract, Sites truth, critical project statuses, mode/workspace
+state rules, or status-label contrast drift.
+
+## Responsive Workspace Completion
+
+The structural workspace-preset follow-up is deployed and verified in the live
+site. All 12 presets were exercised through the production layout picker in
+both Mobile and Tablet modes.
+
+- Tablet retained its 768-pixel action window across every preset.
+- Mobile retained its 375-pixel action window across every preset.
+- Compact, expanded, minimal, top-docked, bottom-docked, rail-free, wide-rail,
+ left-Activity, right-Activity, and Activity-free variants changed actual
+ workspace geometry rather than only colour.
+- Chronicle opened a 420-pixel Activity drawer on the left; Horizons opened a
+ 360-pixel Activity drawer on the right.
+- Every preset kept the preview and composer contained with no root, chat,
+ composer, or top-bar horizontal overflow.
+- The four agent controls retained transparent backgrounds and readable agent
+ colours without hover.
+- The final browser console contained no errors or warnings.
+
+## Repository Accessibility Follow-up
+
+The repository follow-up on 2026-07-25 completed the legacy click-target and
+dynamic-ID audit.
+
+- The Git checkout was safely reconnected to `Ghenghis/bolt.diy` as `origin`
+ and `stackblitz-labs/bolt.diy` as `upstream`, both at `main` commit
+ `2e254ac19a696394030601bc602f54945b12bfc4`.
+- All clickable `div` and `span` controls now expose an interaction role,
+ keyboard focus, and Enter/Space activation, except modal backdrops whose
+ click handler is only an optional outside-dismiss affordance.
+- The 22 static service destinations are native links with safe new-tab
+ attributes.
+- The stylesheet release key is `20260725-accessibility-1`, ensuring the
+ accessibility CSS is fetched instead of an older cached asset.
+- `fp-tool-count` is now a real live-status node in the Tools panel.
+- Seven referenced IDs are confirmed runtime-created:
+ `admin-vps-dash`, `cmd-palette-overlay`, `demo-restore-btn`, `profile-box`,
+ `profile-ov`, `shortcuts-ov`, and `typing-ind`.
+- The accessibility contract now fails for unresolved ID references or
+ non-keyboard click targets. Canonical and runtime-mirror scans both pass with
+ zero unresolved IDs, zero unnamed buttons, and zero non-keyboard targets.
+- Production keyboard verification passed for the Tools tab, prompt
+ suggestion, preview device switcher, game carousel, and Voice Studio Presets
+ tab. Desktop mode was restored, Voice Studio was closed, and the final
+ browser console contained no errors or warnings.
+
+## Final Browser Regression
+
+| Surface | Final production result |
+|---|---|
+| Left flyouts | Projects, Pages, Sites, Tools, Agents, Skills, and Database all opened the correct visible panel |
+| Fixed left actions | Deploy, Analytics, and Settings opened the correct full workspace |
+| Action modes | Chat, Browser, Build, Code, Files, Research, Solve, Game, Hermes, and Admin/Ops all changed the center action surface |
+| Right panel | Activity, Memory, and History selected correctly |
+| Mode/right-panel behavior | Memory remained selected through all ten action-mode changes |
+| Agent selectors | Supervisor, Coder, QA, and Asset each became the single pressed/selected agent |
+| Responsive modes | Desktop, Tablet, Android, and Mobile each changed the preview class; Desktop was restored |
+| Canvas controls | Code opened the live Files pane; Preview restored the action window |
+| Canvas/mode state | Code no longer overwrites the selected global mode; Preview restores the selected mode and composer action state |
+| Locked mode semantics | Admin/Ops remains locked for non-admin users and exposes `aria-disabled="true"` with an admin-role explanation |
+| Voice controls | Text, Mic-to-Text, Text-to-Voice, and Full Voice changed visible state correctly; the mode accessibility label now changes with the visible state |
+| Voice picker | 99 Azure English Neural voices, one selected voice, open/close passed |
+| Voice toggles | Mute, Auto-read, and Think Aloud changed and restored state |
+| Voice Studio | Open/close passed; Azure diagnostics reported 4/4 |
+| Workspace layouts | 12 cards, one selected layout, apply/close passed |
+| Pages action | New Page prepared an editable builder prompt |
+| Sites action | New Site prepared an editable builder prompt |
+| Database action | `SELECT 1 AS ok` returned one row with `ok: 1` |
+| Skills action | Web Builder prepared the matching editable prompt |
+| Projects action | DaveAI Website loaded into the preview and closed the flyout |
+
+## Runtime Tool Truth
+
+The previous UI catalog contained 113 hand-authored entries and did not match
+the actual runtime. The Tools panel now treats `/api/tools` as authoritative.
+
+- 120 visible tool rows
+- 120 unique tool names
+- 120 Run buttons
+- Missing runtime entries such as `accessibility_check` and
+ `zeroclaw_workspace_tree` now appear
+- Stale UI-only entries such as `a11y_check` are removed
+- The left label, search placeholder, Agents panel, Admin panel, and status bar
+ all report the same live count
+- Opening `accessibility_check` produced the correct metadata and Run prepared
+ the capability-broker request in the composer
+- Destructive names continue to prepare approval-gated requests and do not
+ execute without approval
+
+## Route and Status Truth
+
+The final route sweep matched every Sites label after the nine remaining
+services were completed:
+
+| Label | Count | HTTP proof |
+|---|---:|---|
+| live | 14 | 14 public routes returned HTTP 200 |
+| auth | 8 | 8 protected roots returned the Authelia redirect and their public health checks passed |
+| soon | 0 | No Sites entry remains labeled `soon` |
+
+Projects now uses the same hostname truth. DaveAI Dev, Staging, and DIY display
+`auth`; Hermes3D displays `live`; the protected game portal remains `auth`. The game carousel
+contains only three unique live projects and no protected or duplicate card.
+The Sites panel renders all 22 catalog rows with readable, distinct live and
+auth badges.
+
+## Nine-Service Completion
+
+| Service | Accepted production behavior | Evidence |
+|---|---|---|
+| DIY Studio | Validated multi-file project scaffolds behind Authelia | `ev_eb85b65e687198da` |
+| Developer Console | 4/4 real dependency probes healthy behind Authelia | `ev_8a8671df8e0232de` |
+| Staging Gate | Route checks and canonical deployed UI hash passed | `ev_6953ab2794c3efd6` |
+| Gitea | Persistent, loopback-only, install-locked repository service | `ev_601951b01a1772fc` |
+| Adminer | Private Bolt PostgreSQL network and TCP connectivity proven | `ev_7965946348120e57` |
+| Monitoring | Grafana, Prometheus, node-exporter, and six-panel dashboard | `ev_202f2e04efb4e960`, corrected by `ev_99014310b2a74ed0` |
+| Fleet | Live redacted PM2/Docker inventory with corrected uptime | `ev_207fb5483a36db91` |
+| WebSocket | External WSS TLS upgrade and matching echo | `ev_0bf6cfcd1478c0f3` |
+| Hermes3D | Brain 4.0.0, 120 tools, 4/4 agents, browser rotation proof | `ev_1b374693551fbcd1` |
+
+## Voice-to-Project Golden Path
+
+Azure transcription produced this request:
+
+> Create a project called Voice Multi-fileproof. Use index HTML styles, CSS and
+> app JS. Display Voice project OK add a styled card and a button that changes
+> the status text. Validated and commit it.
+
+The authenticated Coder stream then:
+
+1. Created the owner-scoped project workspace.
+2. Materialized `index.html`, `styles.css`, and `app.js`.
+3. Ran validation and deterministic repair.
+4. Loaded the owner-scoped production preview.
+5. Clicked the requested control and proved the text changed from
+ `Voice Project OK` to `Status Changed`.
+6. Committed only after the completion gate passed.
+7. Finished with a clean Git workspace.
+
+Project URL:
+https://daveai.tech/user-projects/user-16/18-voice-multi-file-proof/index.html
+
+Commit: `0f7e74b93c89d14812d74739d32bcd22e5bb5dd1`
+
+Hashes:
+
+- `index.html`: `1eb6803c8b9d3826318e27d4522dd3f36bac66ca0186db96d5cd459fa44781ac`
+- `styles.css`: `f52fe86d74ba7234fbaece27c033b1fc54ff22cad37ca6051accf1a3a07220f4`
+- `app.js`: `6ffce921859e058b062f0495bbb5a0db29101f1b49dd29e9b7465bbd35d61c27`
+
+## Files Workspace
+
+The production Files mode passed:
+
+- authenticated owner-scoped tree load
+- file open and editable contents
+- save
+- new file
+- rename
+- preview refresh
+- project selection persistence
+
+Path overrides remain server-authorized; browser callers cannot escape the
+authenticated user's project workspace.
+
+## Security Completion
+
+- Project and stream endpoints require DaveAI authentication.
+- Normal users receive owner-scoped projects and files.
+- Cross-owner project access is denied.
+- Admin-only runtime and infrastructure operations remain admin-gated.
+- Destructive tool requests retain explicit approval gates.
+- Azure speech synthesis now requires an authenticated DaveAI user.
+- Anonymous Azure synthesis returned HTTP 401.
+- The browser sends its DaveAI authorization header for Voice Studio diagnostics
+ and speech synthesis.
+
+## Proof Artifacts
+
+- `20260724-mode-memory-wiring.png`
+- `20260724-tools-runtime-panel.png`
+- `20260724-sites-truth-panel.png`
+- `20260724-canonical-catalog-truth.png`
+- `20260724-canonical-catalog-truth-bottom.png`
+- `20260725-hermes3d-live-agent-graph.png`
+- `20260724-files-editor.png`
+- `20260724-voice-project.png`
+- `20260724-voice-build-prompt.mp3`
+
+Hash-chained evidence ledger entries:
+
+- Production release: `ev_5f3fa08b389e06ca`
+- Final browser E2E: `ev_1a4c71a073d55555`
+- Voice project E2E: `ev_50d57a6956031f79`
+- Canonical source/catalog and final regression: `ev_fa00a46449d12a3c`
+
+## Production Rollback
+
+Latest release rollback:
+
+`/opt/daveai/backups/20260725T024344Z-canonical-state-contrast`
+
+Additional focused rollback points:
+
+- `/opt/daveai/backups/20260725T023935Z-canvas-mode-state`
+- `/opt/daveai/backups/20260724T220259Z-canonical-source-catalogs`
+- `/opt/daveai/backups/20260724T200634Z-carousel-project-truth`
+- `/opt/daveai/backups/20260724T200502Z-runtime-tools-project-truth`
+- `/opt/daveai/backups/20260724T195825Z-final-ui-truth-tts-auth`
+
+## Operational Boundaries
+
+The main DaveAI website and all nine formerly `soon` subdomain services are
+complete for the audited workflow. No Sites entry remains labeled `soon`.
+Protected service roots remain labeled `auth` rather than being falsely
+represented as public.
+
+Tool Run controls prepare reviewable agent/broker requests. A tool executes
+after the user sends that request, and destructive operations still require
+approval. The audit did not execute all 120 tools destructively because doing so
+would alter or remove live infrastructure.
+
+DaveAI can create and validate complete multi-file projects. Output quality and
+speed still depend on the configured model provider; the local fallback model
+remains available when cloud routing is unavailable.
diff --git a/vps/verify-daveai-ui-contracts.cjs b/vps/verify-daveai-ui-contracts.cjs
new file mode 100644
index 00000000000..80b1060fd12
--- /dev/null
+++ b/vps/verify-daveai-ui-contracts.cjs
@@ -0,0 +1,120 @@
+#!/usr/bin/env node
+'use strict';
+
+const fs = require('node:fs');
+
+const htmlPath = process.argv[2];
+if (!htmlPath) {
+ throw new Error('usage: node verify-daveai-ui-contracts.cjs ');
+}
+const html = fs.readFileSync(htmlPath, 'utf8');
+
+const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map(match => match[1]);
+const idCounts = new Map();
+for (const id of ids) idCounts.set(id, (idCounts.get(id) || 0) + 1);
+const duplicateIds = [...idCounts].filter(([, count]) => count > 1);
+
+const declaredFunctions = new Set([
+ ...[...html.matchAll(/\b(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g)].map(match => match[1]),
+ ...[...html.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/g)].map(match => match[1]),
+ ...[...html.matchAll(/\bwindow\.([A-Za-z_$][\w$]*)\s*=/g)].map(match => match[1]),
+]);
+
+const handlerAttributes = [...html.matchAll(/\bon(?:click|change|input|keydown|keyup|contextmenu|mousedown|mouseup)="([^"]*)"/gi)];
+const ignoredCalls = new Set([
+ 'alert', 'confirm', 'prompt', 'fetch', 'open', 'setTimeout', 'clearTimeout',
+ 'parseInt', 'parseFloat', 'String', 'Number', 'Boolean', 'Date', 'Array',
+ 'Object', 'JSON', 'Math', 'encodeURIComponent', 'decodeURIComponent',
+]);
+const handlerCalls = new Set();
+for (const match of handlerAttributes) {
+ for (const call of match[1].matchAll(/(? !declaredFunctions.has(name)).sort();
+
+const referencedIds = new Set([
+ ...[...html.matchAll(/getElementById\(\s*['"]([^'"]+)['"]\s*\)/g)].map(match => match[1]),
+ ...[...html.matchAll(/querySelector\(\s*['"]#([A-Za-z][\w:-]*)['"]\s*\)/g)].map(match => match[1]),
+]);
+const createdIds = new Set([
+ ...[...html.matchAll(/\.id\s*=\s*['"]([^'"]+)['"]/g)].map(match => match[1]),
+ ...[...html.matchAll(/setAttribute\(\s*['"]id['"]\s*,\s*['"]([^'"]+)['"]\s*\)/g)].map(match => match[1]),
+]);
+const referencedIdsWithoutStaticNodes = [...referencedIds].filter(id => !idCounts.has(id));
+const dynamicallyCreatedIds = referencedIdsWithoutStaticNodes.filter(id => createdIds.has(id)).sort();
+const missingStaticIds = referencedIdsWithoutStaticNodes.filter(id => !createdIds.has(id)).sort();
+
+const unnamedButtons = [];
+for (const match of html.matchAll(/]*)>([\s\S]*?)<\/button>/gi)) {
+ const attrs = match[1];
+ const body = match[2];
+ const text = body
+ .replace(/<[^>]+>/g, ' ')
+ .replace(/&[A-Za-z0-9#]+;/g, ' ')
+ .replace(/\$\{[^}]+\}/g, ' dynamic ')
+ .replace(/\s+/g, ' ')
+ .trim();
+ const named = /\b(?:aria-label|title)="[^"]+"/i.test(attrs) || text.length > 0;
+ if (!named) {
+ const line = html.slice(0, match.index).split('\n').length;
+ unnamedButtons.push({ line, attrs: attrs.trim().replace(/\s+/g, ' ').slice(0, 160) });
+ }
+}
+
+const nonKeyboardClickTargets = [];
+for (const match of html.matchAll(/<(div|span)\b([^>]*\bonclick="[^"]+"[^>]*)>/gi)) {
+ const attrs = match[2];
+ const role = attrs.match(/\brole="([^"]+)"/i)?.[1] || '';
+ const hasTabIndex = /\btabindex="-?\d+"/i.test(attrs);
+ const hasKeyboardHandler = /\bonkey(?:down|up)="[^"]+"/i.test(attrs);
+ const isBackdropDismissal = role === 'dialog'
+ && /event\.target(?:\.id)?\s*={2,3}\s*(?:this|['"][^'"]+['"])/i.test(attrs);
+ if (!isBackdropDismissal && (!role || !hasTabIndex || !hasKeyboardHandler)) {
+ const line = html.slice(0, match.index).split('\n').length;
+ nonKeyboardClickTargets.push({
+ line,
+ tag: match[1],
+ missing: [
+ !role ? 'role' : null,
+ !hasTabIndex ? 'tabindex' : null,
+ !hasKeyboardHandler ? 'keyboard-handler' : null,
+ ].filter(Boolean),
+ attrs: attrs.trim().replace(/\s+/g, ' ').slice(0, 160),
+ });
+ }
+}
+
+const scriptBlocks = [...html.matchAll(/