app · 59d2872

Dev index Recent changes / versions Changelog About TODO

feat(diagnostics): 應用診斷頁與 HTTP log bridge

59d2872 · 2026-11-10 22:09 · app
.env.exampleDiagnosticsRoutes.cjsDocs/ChangeHistory.mdDocs/RUNTIME.mdDocs/TEST_FILES_REFERENCE.mdScriptLib/diagnostics/store.cjsServer.jsTest/diagnostics.test.cjsTest/diagnostics_ui.cjsWebAssets.cjsassets/app.cssassets/app.css.build.jsonassets/diagnostics.jsdiagnostics.htmllang/en.jsonlang/zh-TW.jsonlog.htmlmonitor.htmlruntime.htmltodo.md
commit 59d287271f761d73cc99e28e22df6bda9bd14334Author: Nuclear0709 <agp05432@gmail.com>Date:   Tue Nov 10 22:09:48 2026 +0800     feat(diagnostics): 應用診斷頁與 HTTP log bridge        - 新增 /diagnostics 頁、logio 卡片、最近診斷與應用日誌    - 新增 LogServer 相容 POST /post 與 /api/diagnostics/report,支援可選 DIAGNOSTICS_TOKEN    - 診斷只存記憶體,不寫 log/SQLite/聊天/traffic    - 日誌舊到新、可選自動滾到最新,關閉時保留 scrollTop    - 補 store/route/UI 測試與導覽、文件 diff --git a/.env.example b/.env.exampleindex 7b15628..9fd96be 100644--- a/.env.example+++ b/.env.example@@ -116,6 +116,11 @@ SOCKET_API=http://192.168.0.101:9322 # MONITOR_HISTORY_SESSIONS=10 # MONITOR_HISTORY_MAX_MESSAGES=20000 +# ─── 應用診斷 ────────────────────────────────────────+# ReplyKit/外部工具的 HTTP log bridge 可 POST 到 http://<主服務>:3332/post。+# 選填 token;設定後必須用 ?token=... 或 x-diagnostics-token/Bearer。+# DIAGNOSTICS_TOKEN=+ # ─── 圖片快取 ──────────────────────────────────────── # TikTok/Twitch 頭像、禮物圖與贊助 overlay 圖片的本地快取 URL 前綴。 # 跨機 overlay 請改成 http://<主服務區網IP>:3332diff --git a/DiagnosticsRoutes.cjs b/DiagnosticsRoutes.cjsnew file mode 100644index 0000000..e31b088--- /dev/null+++ b/DiagnosticsRoutes.cjs@@ -0,0 +1,109 @@+const fs = require('node:fs');+const path = require('node:path');++const MAX_BODY = 4 * 1024 * 1024;++function json(res, status, value) {+    res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });+    res.end(JSON.stringify(value));+}++/**+ * 診斷入口:HTTP LogServer 相容 /post、帶外 diagnostic、以及唯讀診斷頁。+ * 資料只存在記憶體,不寫 log 檔或聊天歷史。+ * @param {import('./ScriptLib/diagnostics/store.cjs').DiagnosticsStore} store+ * @param {{token?: string}} [options]+ */+function createDiagnosticsRoutes(store, { token = '' } = {}) {+    function authorizedIntake(req) {+        if (!token) return true;+        const header = req.headers['x-diagnostics-token'];+        if (header === token) return true;+        const bearer = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '');+        if (bearer === token) return true;+        const url = new URL(req.url, 'http://localhost');+        return url.searchParams.get('token') === token;+    }++    function servePage(req, res) {+        if (req.url.split('?')[0] !== '/diagnostics') return false;+        if (req.method !== 'GET') {+            res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' });+            res.end('Method Not Allowed');+            return true;+        }+        fs.readFile(path.join(__dirname, 'diagnostics.html'), 'utf8', (error, html) => {+            if (error) {+                res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });+                res.end('Error loading diagnostics.html');+                return;+            }+            res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });+            res.end(html);+        });+        return true;+    }++    function serveApi(req, res, authorized = false) {+        const url = new URL(req.url, 'http://localhost');+        if (url.pathname !== '/api/diagnostics') return false;+        if (!authorized) {+            json(res, 401, { ok: false, code: 'AUTH_REQUIRED', error: '請先登入' });+            return true;+        }+        if (req.method !== 'GET') {+            json(res, 405, { ok: false, error: '只支援 GET' });+            return true;+        }+        json(res, 200, {+            ok: true,+            ...store.snapshot({+                limit: Number(url.searchParams.get('limit')) || 500,+                q: url.searchParams.get('q') || ''+            })+        });+        return true;+    }++    function serveIntake(req, res) {+        const url = new URL(req.url, 'http://localhost');+        if (!['/post', '/api/diagnostics/report'].includes(url.pathname)) return false;+        if (req.method !== 'POST') {+            json(res, 405, { status: 'error', message: '只支援 POST' });+            return true;+        }+        if (!authorizedIntake(req)) {+            json(res, 401, { status: 'error', message: 'Unauthorized' });+            return true;+        }+        let body = '';+        let tooLarge = false;+        req.on('data', chunk => {+            body += chunk;+            if (Buffer.byteLength(body) > MAX_BODY) tooLarge = true;+        });+        req.on('end', () => {+            if (tooLarge) {+                json(res, 413, { status: 'error', message: 'body too large' });+                return;+            }+            try {+                const data = JSON.parse(body);+                if (data && typeof data === 'object' && data.type === 'diagnostic') {+                    const accepted = store.ingestDiagnostic(data);+                    json(res, 200, { status: accepted ? 'ok' : 'ignored', received: { diagnostics: accepted ? 1 : 0 } });+                    return;+                }+                const result = store.ingestLogs(data);+                json(res, 200, { status: 'ok', received: { logs: result.accepted } });+            } catch {+                json(res, 400, { status: 'error', message: 'Invalid JSON' });+            }+        });+        return true;+    }++    return { servePage, serveApi, serveIntake };+}++module.exports = { createDiagnosticsRoutes };diff --git a/Docs/ChangeHistory.md b/Docs/ChangeHistory.mdindex 20b9a37..d03ea09 100644--- a/Docs/ChangeHistory.md+++ b/Docs/ChangeHistory.md@@ -1,5 +1,38 @@ # ChangeHistory +## 2026.11.10 20:42 應用診斷入口與 HTTP log bridge++**類型**: 功能 · **檔案**: `diagnostics.html`、`assets/diagnostics.js`、`DiagnosticsRoutes.cjs`、`ScriptLib/diagnostics/store.cjs`、`Server.js`、`WebAssets.cjs`、`monitor.html`、`runtime.html`、`log.html`、`lang/*.json`、`.env.example`、`Test/diagnostics.test.cjs`、`Test/diagnostics_ui.cjs`、`Docs/RUNTIME.md`、`Docs/TEST_FILES_REFERENCE.md`++### 修改++- 新增 `/diagnostics` 診斷頁與導覽入口。+- 新增 LogServer 相容 `POST /post`(也接受 `/api/diagnostics/report`):+  - `{logs:[{time,title,body}]}`、單一 log 或陣列。+  - 也接受 `type:"diagnostic"` 的 logio 摘要。+  - 可選 `DIAGNOSTICS_TOKEN`,支援 query/`x-diagnostics-token`/Bearer。+- 診斷只存在記憶體(logs 2000、diagnostic 100),不寫 log 檔、SQLite、聊天、traffic 或通知。+- `/diagnostics` 顯示 logio 卡片、最近帶外診斷與應用日誌;2 秒自動更新,需主服務登入才能讀 API。+- 應用日誌改為舊到新、最新在底部;新增「自動滾到最新」開關,關閉時保留目前 scrollTop。+- 補 memory store/route/UI 測試。++### 使用++ReplyKit 的 `logURL` 改指向 `http://<TTWChat主機>:3332/post`;若有設定 `DIAGNOSTICS_TOKEN`,可加 `?token=...`。socket diagnostic 可由既有橋接轉送到同一個 `/api/diagnostics/report`。++### 限制++- 資料不持久化,重啟即清空;這是刻意的診斷暫存。+- 目前不實作 TTWChat 自己連 ReplyKit socket server;採 HTTP bridge。++### 驗證++- `node --test Test/diagnostics.test.cjs`+- `node Test/diagnostics_ui.cjs`+- `npm run lint`、`npm run check:css`++---+ ## 2026.11.10 20:30 monitor 工具面板不再撐滿版面  **類型**: 修正 · **檔案**: `assets/monitor.css`、`Test/monitor_audience_ui.cjs`diff --git a/Docs/RUNTIME.md b/Docs/RUNTIME.mdindex a6f2fe6..ef64802 100644--- a/Docs/RUNTIME.md+++ b/Docs/RUNTIME.md@@ -33,6 +33,8 @@ Socket 使用既有 SOCKET_API 設定(可再用 SOCKET_APIS 加開多個端點  工具列會顯示子程序維護的本場加入/點讚/聊天累計。數字隨 socket payload 的 `session` metadata 更新;點讚以原始 `data.count` 累加,因此與每位使用者的 60 秒顯示計數不同。點讚的 Bark/ntfy 通知會像加入事件一樣合併 3 秒,monitor 仍保留逐筆點讚列。 +應用診斷(ReplyKit/外部 HTTP log bridge 與帶外 logio)顯示在 `/diagnostics`;只存在記憶體,不寫 log/SQLite/聊天歷史。+ 圖片(TikTok/Twitch 頭像、禮物圖與贊助 overlay 圖片)由 MediaCache 持久化到 `cache/media/` 與 `data/media.sqlite`,訊息改傳 `MEDIA_BASE_URL/media/<hash>`;詳見 `Docs/MEDIA_CACHE.md`。  歷史訊息保存在 `data/monitor.sqlite`(`sessions`/`messages`)。每次 TikTok 子程序啟動是一場;預設保留最近 10 場、14 天、每場最多 20,000 則,可用 `MONITOR_HISTORY_DAYS`/`MONITOR_HISTORY_SESSIONS`/`MONITOR_HISTORY_MAX_MESSAGES` 調整。測試訊息與 `isTest` 不寫入;歷史 API 需要主服務登入,即時 SSE 仍維持原本公開行為。diff --git a/Docs/TEST_FILES_REFERENCE.md b/Docs/TEST_FILES_REFERENCE.mdindex 2b2580b..bba817a 100644--- a/Docs/TEST_FILES_REFERENCE.md+++ b/Docs/TEST_FILES_REFERENCE.md@@ -65,6 +65,8 @@ node Test/xxx.mjs | `Test/media_cache.test.cjs` | 圖片快取本地 URL、下載、SVG/私有位址拒絕、路由與 SocketHub 改寫 | | `Test/inline_image.test.cjs` | data URI 圖片格式、大小/數量限制與通知剝離 | | `Test/socket_test_routes.test.cjs` | Raw Socket 測試入口的登入、子程序檢查與 payload 轉送 |+| `Test/diagnostics.test.cjs` | 診斷記憶體 store、LogServer 相容 `/post`、diagnostic 與 token |+| `Test/diagnostics_ui.cjs` | `/diagnostics` 日誌順序、自動滾到最新、logio 卡片與無 page error | | `Test/notify.test.mjs` | 通知格式化與 Bark/ntfy 逐通道結果 | | `Test/traffic_planner_ui.cjs` | 人流分類、批次補分類、預覽/套用/復原與手機寬度 | | `Test/traffic_planner_collecting_ui.cjs` | 資料累積建議、時段分組提醒與一週計畫 UI |diff --git a/ScriptLib/diagnostics/store.cjs b/ScriptLib/diagnostics/store.cjsnew file mode 100644index 0000000..610f5a0--- /dev/null+++ b/ScriptLib/diagnostics/store.cjs@@ -0,0 +1,107 @@+/**+ * 應用診斷的記憶體暫存:HTTP log bridge(LogServer 相容)與帶外 diagnostic。+ * 不寫 log 檔、不進聊天、不進 SQLite;所有資料都有筆數與字串上限。+ */+class DiagnosticsStore {+    constructor({ maxLogs = 2000, maxDiagnostics = 100, maxBody = 8000, now = Date.now } = {}) {+        this.maxLogs = maxLogs;+        this.maxDiagnostics = maxDiagnostics;+        this.maxBody = maxBody;+        this.now = now;+        this.logs = [];+        this.diagnostics = new Map();+        this.recentDiagnostics = [];+        this.stats = { logsReceived: 0, logsDropped: 0, diagnosticsReceived: 0 };+        this.lastReceived = null;+    }++    text(value) {+        return String(value == null ? '' : value).slice(0, this.maxBody);+    }++    normalizeLog(entry) {+        if (!entry || typeof entry !== 'object') return null;+        const title = this.text(entry.title).trim();+        const body = this.text(entry.body ?? entry.message);+        if (!title && !body) return null;+        return {+            time: this.text(entry.time),+            title: title || 'log',+            body,+            receivedAt: this.now()+        };+    }++    /** 接受 LogServer 的 {logs:[...]}、單一 log 物件或陣列。 */+    ingestLogs(payload) {+        let entries;+        if (Array.isArray(payload)) entries = payload;+        else if (Array.isArray(payload?.logs)) entries = payload.logs;+        else entries = [payload];+        let accepted = 0;+        for (const entry of entries.slice(0, this.maxLogs)) {+            const normalized = this.normalizeLog(entry);+            if (!normalized) {+                this.stats.logsDropped += 1;+                continue;+            }+            this.logs.push(normalized);+            accepted += 1;+            this.stats.logsReceived += 1;+        }+        if (this.logs.length > this.maxLogs) {+            const removed = this.logs.length - this.maxLogs;+            this.logs.splice(0, removed);+            this.stats.logsDropped += removed;+        }+        if (accepted) this.lastReceived = this.now();+        return { accepted, total: this.logs.length };+    }++    /** 帶外 logio/diagnostic;只保留每個 subsystem/source 的最新摘要。 */+    ingestDiagnostic(payload) {+        if (!payload || typeof payload !== 'object') return false;+        if (payload.type !== 'diagnostic') return false;+        const subsystem = this.text(payload.subsystem || 'unknown').slice(0, 80);+        const source = this.text(payload.source || 'unknown').slice(0, 40);+        const count = Number.isFinite(Number(payload.count)) ? Math.max(0, Number(payload.count)) : 0;+        const entry = {+            type: 'diagnostic',+            subsystem,+            source,+            count,+            lastError: this.text(payload.lastError),+            receivedAt: this.now()+        };+        this.diagnostics.set(subsystem + ':' + source, entry);+        this.recentDiagnostics.unshift(entry);+        if (this.recentDiagnostics.length > this.maxDiagnostics) this.recentDiagnostics.pop();+        this.stats.diagnosticsReceived += 1;+        this.lastReceived = entry.receivedAt;+        return true;+    }++    snapshot({ limit = 500, q = '' } = {}) {+        const query = String(q || '').trim().toLowerCase();+        const size = Math.min(Math.max(Number(limit) || 500, 1), this.maxLogs);+        const logs = this.logs+            .filter(entry => !query+                || entry.title.toLowerCase().includes(query)+                || entry.body.toLowerCase().includes(query))+            .slice(-size)+            .reverse();+        return {+            logs,+            diagnostics: [...this.diagnostics.values()].sort((a, b) => b.receivedAt - a.receivedAt),+            recentDiagnostics: this.recentDiagnostics,+            stats: {+                ...this.stats,+                bufferedLogs: this.logs.length,+                bufferedDiagnostics: this.recentDiagnostics.length+            },+            lastReceived: this.lastReceived+        };+    }+}++module.exports = { DiagnosticsStore };diff --git a/Server.js b/Server.jsindex 3aa0428..851904b 100644--- a/Server.js+++ b/Server.js@@ -61,6 +61,11 @@ const serveAudience = createAudienceRoutes(audience); const mediaRoutes = require('./MediaRoutes.cjs'); const serveMedia = mediaRoutes.createMediaRoutes(); const serveMediaAdmin = mediaRoutes.createMediaAdminRoutes();+const { DiagnosticsStore } = require('./ScriptLib/diagnostics/store.cjs');+const { createDiagnosticsRoutes } = require('./DiagnosticsRoutes.cjs');+const diagnosticsRoutes = createDiagnosticsRoutes(new DiagnosticsStore(), {+    token: process.env.DIAGNOSTICS_TOKEN || ''+}); const audiencePrune = setInterval(() => {     try { audience.prune(180); } catch (error) { console.error('[Audience] 清理失敗:', error.message); } }, 24 * 3600000);@@ -625,6 +630,9 @@ const server = http.createServer((req, res) => {     if (serveMonitorHistory(req, res, isValidToken(parseCookies(req).authToken))) return;     if (require('./RestreamRoutes.cjs').serveRestream(req,res,isValidToken(parseCookies(req).authToken))) return;     if (serveRuntime(req, res, runtimeState, startRuntime)) return;+    if (diagnosticsRoutes.serveIntake(req, res)) return;+    if (diagnosticsRoutes.servePage(req, res)) return;+    if (diagnosticsRoutes.serveApi(req, res, isValidToken(parseCookies(req).authToken))) return;     if (serveMediaAdmin(req, res, isValidToken(parseCookies(req).authToken))) return;     if (serveSocketTest(req, res, isValidToken(parseCookies(req).authToken))) return;     if (serveMedia(req, res)) return;diff --git a/Test/diagnostics.test.cjs b/Test/diagnostics.test.cjsnew file mode 100644index 0000000..4cc592c--- /dev/null+++ b/Test/diagnostics.test.cjs@@ -0,0 +1,92 @@+const test = require('node:test');+const assert = require('node:assert/strict');+const { EventEmitter } = require('node:events');+const { DiagnosticsStore } = require('../ScriptLib/diagnostics/store.cjs');+const { createDiagnosticsRoutes } = require('../DiagnosticsRoutes.cjs');++function request(route, { url = '/post', method = 'POST', body = '', headers = {} } = {}) {+    return new Promise(resolve => {+        const req = new EventEmitter();+        req.url = url;+        req.method = method;+        req.headers = headers;+        const res = {+            writeHead(status) { this.status = status; },+            end(value) { resolve({ status: this.status, body: value ? JSON.parse(value) : null }); }+        };+        route(req, res);+        setImmediate(() => {+            if (body) req.emit('data', Buffer.from(body));+            req.emit('end');+        });+    });+}++test('DiagnosticsStore 接收 LogServer logs 與帶外 diagnostic', () => {+    const store = new DiagnosticsStore({ maxLogs: 3 });+    store.ingestLogs({ logs: [+        { time: 'a', title: 'one', body: 'first' },+        { time: 'b', title: 'two', body: 'second' }+    ] });+    store.ingestLogs({ time: 'c', title: 'three', body: 'third' });+    store.ingestLogs({ time: 'd', title: 'four', body: 'fourth' });+    const snapshot = store.snapshot({ q: 'four' });+    assert.equal(snapshot.logs.length, 1);+    assert.equal(snapshot.logs[0].title, 'four');+    assert.equal(snapshot.stats.bufferedLogs, 3);++    assert.equal(store.ingestDiagnostic({+        type: 'diagnostic', subsystem: 'logio', source: 'extension', count: 4, lastError: 'disk full'+    }), true);+    assert.equal(store.ingestDiagnostic({ type: 'other' }), false);+    const diag = store.snapshot().diagnostics[0];+    assert.deepEqual({ subsystem: diag.subsystem, source: diag.source, count: diag.count, lastError: diag.lastError },+        { subsystem: 'logio', source: 'extension', count: 4, lastError: 'disk full' });+});++test('診斷入口接受 LogServer /post、diagnostic 與可選 token', async () => {+    const store = new DiagnosticsStore();+    const routes = createDiagnosticsRoutes(store, { token: 'secret' });+    assert.equal((await request(routes.serveIntake, {+        body: JSON.stringify({ logs: [{ title: 'x', body: 'y' }] })+    })).status, 401);+    const accepted = await request(routes.serveIntake, {+        headers: { 'x-diagnostics-token': 'secret' },+        body: JSON.stringify({ logs: [{ title: 'x', body: 'y' }] })+    });+    assert.equal(accepted.status, 200);+    assert.equal(accepted.body.received.logs, 1);+    const diagnostic = await request(routes.serveIntake, {+        url: '/api/diagnostics/report',+        headers: { authorization: 'Bearer secret' },+        body: JSON.stringify({ type: 'diagnostic', subsystem: 'logio', source: 'mainapp', count: 2, lastError: 'nope' })+    });+    assert.equal(diagnostic.status, 200);+    assert.equal(diagnostic.body.received.diagnostics, 1);+    assert.equal(store.snapshot().diagnostics[0].source, 'mainapp');+});++test('診斷 API 需要登入並回傳 snapshot', async () => {+    const store = new DiagnosticsStore();+    store.ingestDiagnostic({ type: 'diagnostic', subsystem: 'logio', source: 'extension', count: 1, lastError: '' });+    const routes = createDiagnosticsRoutes(store);+    const denied = await new Promise(resolve => {+        const req = new EventEmitter();+        req.url = '/api/diagnostics';+        req.method = 'GET';+        req.headers = {};+        const res = { writeHead(status) { this.status = status; }, end(body) { resolve({ status: this.status, body: JSON.parse(body) }); } };+        routes.serveApi(req, res, false);+    });+    assert.equal(denied.status, 401);+    const allowed = await new Promise(resolve => {+        const req = new EventEmitter();+        req.url = '/api/diagnostics';+        req.method = 'GET';+        req.headers = {};+        const res = { writeHead(status) { this.status = status; }, end(body) { resolve({ status: this.status, body: JSON.parse(body) }); } };+        routes.serveApi(req, res, true);+    });+    assert.equal(allowed.status, 200);+    assert.equal(allowed.body.diagnostics.length, 1);+});diff --git a/Test/diagnostics_ui.cjs b/Test/diagnostics_ui.cjsnew file mode 100644index 0000000..22e662d--- /dev/null+++ b/Test/diagnostics_ui.cjs@@ -0,0 +1,71 @@+/** /diagnostics 頁面冒煙測試:LogServer 日誌、logio 卡片與無 page error。 */+/* global document -- 僅在瀏覽器回呼中使用。 */+const assert = require('node:assert/strict');+const http = require('node:http');+const fs = require('node:fs');+const path = require('node:path');+const { serveWebAsset } = require('../WebAssets.cjs');+const { browserPath } = require('./browser_path.cjs');++async function main() {+    const pageHtml = fs.readFileSync(path.join(__dirname, '../diagnostics.html'));+    const server = http.createServer((req, res) => {+        const url = new URL(req.url, 'http://127.0.0.1');+        if (url.pathname === '/diagnostics') {+            res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });+            res.end(pageHtml);+            return;+        }+        if (url.pathname === '/api/diagnostics') {+            res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });+            res.end(JSON.stringify({+                ok: true,+                logs: [+                    { time: '2026-11-10 20:00:01', title: 'ReplyKit', body: 'capture started', receivedAt: Date.now() },+                    { time: '2026-11-10 20:00:00', title: 'ReplyKit', body: 'older line', receivedAt: Date.now() - 1000 }+                ],+                diagnostics: [{ subsystem: 'logio', source: 'extension', count: 3, lastError: 'disk full', receivedAt: Date.now() }],+                recentDiagnostics: [{ subsystem: 'logio', source: 'extension', count: 3, lastError: 'disk full', receivedAt: Date.now() }],+                stats: { bufferedLogs: 1, bufferedDiagnostics: 1 },+                lastReceived: Date.now()+            }));+            return;+        }+        if (serveWebAsset(req, res)) return;+        res.writeHead(404);+        res.end();+    });+    await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));+    let browser;+    try {+        const { default: puppeteer } = await import('puppeteer-core');+        browser = await puppeteer.launch({ executablePath: browserPath(), headless: true });+        const page = await browser.newPage();+        const errors = [];+        page.on('pageerror', error => errors.push(error.message));+        await page.goto(`http://127.0.0.1:${server.address().port}/diagnostics`, { waitUntil: 'networkidle0' });+        await page.waitForFunction(() => document.querySelectorAll('#diagLogs .diag-log').length === 2);+        const result = await page.evaluate(() => ({+            title: document.querySelector('h1').textContent,+            logs: [...document.querySelectorAll('#diagLogs .diag-log')].map(node => node.textContent),+            diagnostic: document.querySelector('#diagCards').textContent,+            loginHidden: document.querySelector('#diagCallout').hidden,+            autoScroll: document.querySelector('#diagAutoScroll').checked,+            atBottom: Math.abs(document.querySelector('#diagLogs').scrollHeight - document.querySelector('#diagLogs').clientHeight - document.querySelector('#diagLogs').scrollTop) < 4+        }));+        assert.match(result.title, /應用診斷/);+        assert.match(result.logs[0], /older line/);+        assert.match(result.logs[1], /capture started/);+        assert.equal(result.autoScroll, true);+        assert.equal(result.atBottom, true);+        assert.match(result.diagnostic, /logio/);+        assert.equal(result.loginHidden, true);+        assert.deepEqual(errors, []);+        console.log('Diagnostics UI passed: log stream, logio card, and no page errors.');+    } finally {+        await browser?.close();+        await new Promise(resolve => server.close(resolve));+    }+}++main().catch(error => { console.error(error); process.exitCode = 1; });diff --git a/WebAssets.cjs b/WebAssets.cjsindex b16a8d0..7291afa 100644--- a/WebAssets.cjs+++ b/WebAssets.cjs@@ -24,7 +24,8 @@ function createWebAssetHandler(root = __dirname) {             '/assets/keyword.js': ['assets/keyword.js', 'text/javascript; charset=utf-8'],             '/assets/console.js': ['assets/console.js', 'text/javascript; charset=utf-8'],             '/assets/monitor.js': ['assets/monitor.js', 'text/javascript; charset=utf-8'],-            '/assets/monitor.css': ['assets/monitor.css', 'text/css; charset=utf-8']+            '/assets/monitor.css': ['assets/monitor.css', 'text/css; charset=utf-8'],+            '/assets/diagnostics.js': ['assets/diagnostics.js', 'text/javascript; charset=utf-8']         };         const pathname = req.url.split('?')[0];         const locale = /^\/lang\/([a-zA-Z0-9-]+)\.json$/.exec(pathname)?.[1];diff --git a/assets/app.css b/assets/app.cssindex ed568a4..4d46870 100644--- a/assets/app.css+++ b/assets/app.css@@ -1,2 +1,2 @@ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */-@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-900:oklch(40.8% .123 38.172);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-900:oklch(41.4% .112 45.904);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-900:oklch(42.1% .095 57.708);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-900:oklch(40.5% .101 131.063);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-900:oklch(39.3% .095 152.535);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-teal-300:oklch(85.5% .138 181.071);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-teal-900:oklch(38.6% .063 188.416);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-900:oklch(39.8% .07 227.392);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-900:oklch(35.9% .144 278.697);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-900:oklch(38.1% .176 304.987);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-900:oklch(40.1% .17 325.612);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-900:oklch(40.8% .153 2.432);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-700:oklch(37.2% .044 257.287);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-3xl:48rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-widest:.1em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing) * 2)}.top-6{top:calc(var(--spacing) * 6)}.right-2{right:calc(var(--spacing) * 2)}.right-6{right:calc(var(--spacing) * 6)}.left-2{left:calc(var(--spacing) * 2)}.z-50{z-index:50}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-0\.5{margin-inline:calc(var(--spacing) * .5)}.mx-auto{margin-inline:auto}.my-1{margin-block:var(--spacing)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mr-1{margin-right:var(--spacing)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-12{height:calc(var(--spacing) * 12)}.h-40{height:calc(var(--spacing) * 40)}.h-52{height:calc(var(--spacing) * 52)}.h-full{height:100%}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-80{max-height:calc(var(--spacing) * 80)}.max-h-\[80dvh\]{max-height:80dvh}.max-h-\[85vh\]{max-height:85vh}.min-h-0{min-height:0}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-12{width:calc(var(--spacing) * 12)}.w-24{width:calc(var(--spacing) * 24)}.w-96{width:calc(var(--spacing) * 96)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[1600px\]{max-width:1600px}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:0}.min-w-40{min-width:calc(var(--spacing) * 40)}.min-w-48{min-width:calc(var(--spacing) * 48)}.min-w-\[200px\]{min-width:200px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-1{row-gap:var(--spacing)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-blue-500\/40{border-color:#3080ff66}@supports (color:color-mix(in lab, red, red)){.border-blue-500\/40{border-color:color-mix(in oklab, var(--color-blue-500) 40%, transparent)}}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-700\/60{border-color:#36415399}@supports (color:color-mix(in lab, red, red)){.border-gray-700\/60{border-color:color-mix(in oklab, var(--color-gray-700) 60%, transparent)}}.border-gray-800{border-color:var(--color-gray-800)}.border-gray-800\/60{border-color:#1e293999}@supports (color:color-mix(in lab, red, red)){.border-gray-800\/60{border-color:color-mix(in oklab, var(--color-gray-800) 60%, transparent)}}.border-green-500\/40{border-color:#00c75866}@supports (color:color-mix(in lab, red, red)){.border-green-500\/40{border-color:color-mix(in oklab, var(--color-green-500) 40%, transparent)}}.border-green-600{border-color:var(--color-green-600)}.border-purple-500\/40{border-color:#ac4bff66}@supports (color:color-mix(in lab, red, red)){.border-purple-500\/40{border-color:color-mix(in oklab, var(--color-purple-500) 40%, transparent)}}.border-red-500\/40{border-color:#fb2c3666}@supports (color:color-mix(in lab, red, red)){.border-red-500\/40{border-color:color-mix(in oklab, var(--color-red-500) 40%, transparent)}}.border-red-600{border-color:var(--color-red-600)}.border-transparent{border-color:#0000}.border-yellow-500\/40{border-color:#edb20066}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/40{border-color:color-mix(in oklab, var(--color-yellow-500) 40%, transparent)}}.border-yellow-600{border-color:var(--color-yellow-600)}.border-l-blue-500{border-left-color:var(--color-blue-500)}.border-l-gray-600{border-left-color:var(--color-gray-600)}.border-l-green-600{border-left-color:var(--color-green-600)}.border-l-purple-500{border-left-color:var(--color-purple-500)}.border-l-red-500{border-left-color:var(--color-red-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-900\/60{background-color:#7b330699}@supports (color:color-mix(in lab, red, red)){.bg-amber-900\/60{background-color:color-mix(in oklab, var(--color-amber-900) 60%, transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-black\/70{background-color:#000000b3}@supports (color:color-mix(in lab, red, red)){.bg-black\/70{background-color:color-mix(in oklab, var(--color-black) 70%, transparent)}}.bg-black\/80{background-color:#000c}@supports (color:color-mix(in lab, red, red)){.bg-black\/80{background-color:color-mix(in oklab, var(--color-black) 80%, transparent)}}.bg-blue-500\/20{background-color:#3080ff33}@supports (color:color-mix(in lab, red, red)){.bg-blue-500\/20{background-color:color-mix(in oklab, var(--color-blue-500) 20%, transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-900\/60{background-color:#1c398e99}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/60{background-color:color-mix(in oklab, var(--color-blue-900) 60%, transparent)}}.bg-blue-900\/70{background-color:#1c398eb3}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/70{background-color:color-mix(in oklab, var(--color-blue-900) 70%, transparent)}}.bg-cyan-600\/80{background-color:#0092b5cc}@supports (color:color-mix(in lab, red, red)){.bg-cyan-600\/80{background-color:color-mix(in oklab, var(--color-cyan-600) 80%, transparent)}}.bg-cyan-900\/60{background-color:#104e6499}@supports (color:color-mix(in lab, red, red)){.bg-cyan-900\/60{background-color:color-mix(in oklab, var(--color-cyan-900) 60%, transparent)}}.bg-emerald-600\/80{background-color:#009767cc}@supports (color:color-mix(in lab, red, red)){.bg-emerald-600\/80{background-color:color-mix(in oklab, var(--color-emerald-600) 80%, transparent)}}.bg-fuchsia-900\/60{background-color:#72137899}@supports (color:color-mix(in lab, red, red)){.bg-fuchsia-900\/60{background-color:color-mix(in oklab, var(--color-fuchsia-900) 60%, transparent)}}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-700\/50{background-color:#36415380}@supports (color:color-mix(in lab, red, red)){.bg-gray-700\/50{background-color:color-mix(in oklab, var(--color-gray-700) 50%, transparent)}}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-500\/20{background-color:#00c75833}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/20{background-color:color-mix(in oklab, var(--color-green-500) 20%, transparent)}}.bg-green-600{background-color:var(--color-green-600)}.bg-green-900{background-color:var(--color-green-900)}.bg-green-900\/60{background-color:#0d542b99}@supports (color:color-mix(in lab, red, red)){.bg-green-900\/60{background-color:color-mix(in oklab, var(--color-green-900) 60%, transparent)}}.bg-green-900\/80{background-color:#0d542bcc}@supports (color:color-mix(in lab, red, red)){.bg-green-900\/80{background-color:color-mix(in oklab, var(--color-green-900) 80%, transparent)}}.bg-indigo-900\/60{background-color:#312c8599}@supports (color:color-mix(in lab, red, red)){.bg-indigo-900\/60{background-color:color-mix(in oklab, var(--color-indigo-900) 60%, transparent)}}.bg-lime-900\/60{background-color:#35530e99}@supports (color:color-mix(in lab, red, red)){.bg-lime-900\/60{background-color:color-mix(in oklab, var(--color-lime-900) 60%, transparent)}}.bg-orange-900\/60{background-color:#7e2a0c99}@supports (color:color-mix(in lab, red, red)){.bg-orange-900\/60{background-color:color-mix(in oklab, var(--color-orange-900) 60%, transparent)}}.bg-pink-900\/60{background-color:#86104399}@supports (color:color-mix(in lab, red, red)){.bg-pink-900\/60{background-color:color-mix(in oklab, var(--color-pink-900) 60%, transparent)}}.bg-purple-500\/20{background-color:#ac4bff33}@supports (color:color-mix(in lab, red, red)){.bg-purple-500\/20{background-color:color-mix(in oklab, var(--color-purple-500) 20%, transparent)}}.bg-purple-700{background-color:var(--color-purple-700)}.bg-purple-900\/60{background-color:#59168b99}@supports (color:color-mix(in lab, red, red)){.bg-purple-900\/60{background-color:color-mix(in oklab, var(--color-purple-900) 60%, transparent)}}.bg-purple-900\/70{background-color:#59168bb3}@supports (color:color-mix(in lab, red, red)){.bg-purple-900\/70{background-color:color-mix(in oklab, var(--color-purple-900) 70%, transparent)}}.bg-red-500\/20{background-color:#fb2c3633}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/20{background-color:color-mix(in oklab, var(--color-red-500) 20%, transparent)}}.bg-red-500\/80{background-color:#fb2c36cc}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/80{background-color:color-mix(in oklab, var(--color-red-500) 80%, transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-red-600\/80{background-color:#e40014cc}@supports (color:color-mix(in lab, red, red)){.bg-red-600\/80{background-color:color-mix(in oklab, var(--color-red-600) 80%, transparent)}}.bg-red-800{background-color:var(--color-red-800)}.bg-red-900{background-color:var(--color-red-900)}.bg-red-900\/60{background-color:#82181a99}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/60{background-color:color-mix(in oklab, var(--color-red-900) 60%, transparent)}}.bg-red-900\/70{background-color:#82181ab3}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/70{background-color:color-mix(in oklab, var(--color-red-900) 70%, transparent)}}.bg-red-900\/80{background-color:#82181acc}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/80{background-color:color-mix(in oklab, var(--color-red-900) 80%, transparent)}}.bg-sky-600{background-color:var(--color-sky-600)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-teal-600\/80{background-color:#009588cc}@supports (color:color-mix(in lab, red, red)){.bg-teal-600\/80{background-color:color-mix(in oklab, var(--color-teal-600) 80%, transparent)}}.bg-teal-900\/60{background-color:#0b4f4a99}@supports (color:color-mix(in lab, red, red)){.bg-teal-900\/60{background-color:color-mix(in oklab, var(--color-teal-900) 60%, transparent)}}.bg-yellow-500\/20{background-color:#edb20033}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/20{background-color:color-mix(in oklab, var(--color-yellow-500) 20%, transparent)}}.bg-yellow-600{background-color:var(--color-yellow-600)}.bg-yellow-900\/60{background-color:#733e0a99}@supports (color:color-mix(in lab, red, red)){.bg-yellow-900\/60{background-color:color-mix(in oklab, var(--color-yellow-900) 60%, transparent)}}.bg-yellow-900\/80{background-color:#733e0acc}@supports (color:color-mix(in lab, red, red)){.bg-yellow-900\/80{background-color:color-mix(in oklab, var(--color-yellow-900) 80%, transparent)}}.object-contain{object-fit:contain}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-blue-300{color:var(--color-blue-300)}.text-blue-400{color:var(--color-blue-400)}.text-cyan-300{color:var(--color-cyan-300)}.text-cyan-400{color:var(--color-cyan-400)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-fuchsia-300{color:var(--color-fuchsia-300)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-green-200{color:var(--color-green-200)}.text-green-300{color:var(--color-green-300)}.text-green-400{color:var(--color-green-400)}.text-indigo-300{color:var(--color-indigo-300)}.text-lime-300{color:var(--color-lime-300)}.text-orange-300{color:var(--color-orange-300)}.text-orange-400{color:var(--color-orange-400)}.text-pink-300{color:var(--color-pink-300)}.text-purple-200{color:var(--color-purple-200)}.text-purple-300{color:var(--color-purple-300)}.text-purple-400{color:var(--color-purple-400)}.text-red-200{color:var(--color-red-200)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-sky-200{color:var(--color-sky-200)}.text-sky-300{color:var(--color-sky-300)}.text-sky-400{color:var(--color-sky-400)}.text-slate-300{color:var(--color-slate-300)}.text-teal-300{color:var(--color-teal-300)}.text-white{color:var(--color-white)}.text-yellow-200{color:var(--color-yellow-200)}.text-yellow-300{color:var(--color-yellow-300)}.text-yellow-400{color:var(--color-yellow-400)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.accent-blue-500{accent-color:var(--color-blue-500)}.accent-red-500{accent-color:var(--color-red-500)}.opacity-60{opacity:.6}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:text-gray-300:is(:where(.group):hover *){color:var(--color-gray-300)}.hover\:bg-amber-500:hover{background-color:var(--color-amber-500)}.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-cyan-500:hover{background-color:var(--color-cyan-500)}.hover\:bg-emerald-500:hover{background-color:var(--color-emerald-500)}.hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-gray-900\/50:hover{background-color:#10182880}@supports (color:color-mix(in lab, red, red)){.hover\:bg-gray-900\/50:hover{background-color:color-mix(in oklab, var(--color-gray-900) 50%, transparent)}}.hover\:bg-green-500:hover{background-color:var(--color-green-500)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-sky-500:hover{background-color:var(--color-sky-500)}.hover\:bg-teal-500:hover{background-color:var(--color-teal-500)}.hover\:bg-yellow-700:hover{background-color:var(--color-yellow-700)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-gray-300:hover{color:var(--color-gray-300)}.hover\:text-red-300:hover{color:var(--color-red-300)}.hover\:text-sky-300:hover{color:var(--color-sky-300)}.hover\:text-white:hover{color:var(--color-white)}.hover\:underline:hover{text-decoration-line:underline}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:ring-blue-500:hover{--tw-ring-color:var(--color-blue-500)}}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:h-52{height:calc(var(--spacing) * 52)}.sm\:h-64{height:calc(var(--spacing) * 64)}.sm\:flex-none{flex:none}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:justify-between{justify-content:space-between}.sm\:gap-6{gap:calc(var(--spacing) * 6)}:where(.sm\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}}@media (min-width:48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (min-width:80rem){.xl\:col-span-2{grid-column:span 2/span 2}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}.app-ui:not(.font-mono){font-family:system-ui,Microsoft JhengHei,sans-serif}.app-ui button,.app-ui input,.app-ui select,.app-ui textarea{max-width:100%}.app-ui button:disabled{cursor:default;opacity:.55}.app-ui :is(button,a,input,select,textarea,summary):focus-visible{outline-offset:3px;outline:2px solid #60a5fa}.app-ui nav{flex-wrap:wrap}.app-ui nav a{align-items:center;min-height:36px;display:inline-flex}.page-header{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;display:flex}.page-header h1{flex-shrink:0}.app-ui .flex>*,.app-ui .grid>*{min-width:0}.app-ui [role=dialog],.app-ui #safetyOverlay>div{max-height:calc(100dvh - 32px);overflow-y:auto}.config-page,.login-page{color-scheme:dark;--config-bg:#070b12;--config-surface:#0f172a;--config-surface-2:#111c31;--config-border:#243147;--config-text:#e5edf8;--config-muted:#94a3b8;--config-accent:#3b82f6;background:var(--config-bg);color:var(--config-text)}.config-page{padding:clamp(12px,3vw,32px);display:block}.config-page #configContent{width:100%;max-width:1180px;margin:auto}.config-shell{grid-template-columns:224px minmax(0,1fr);align-items:start;gap:24px;display:grid}.config-sidebar{gap:18px;display:grid;position:sticky;top:20px}.config-brand{align-items:center;gap:10px;padding:2px 4px;display:flex}.config-brand-mark{color:#eff6ff;letter-spacing:.06em;background:#1d4ed8;border-radius:12px;flex:none;justify-content:center;align-items:center;width:38px;height:38px;font-size:12px;font-weight:800;line-height:1;display:inline-flex}.config-brand-copy{gap:2px;min-width:0;display:grid}.config-brand-copy strong{color:#f8fafc;font-size:14px;line-height:1.15;display:block}.config-brand-copy span{color:var(--config-muted);font-size:12px;line-height:1.2;display:block}.config-nav{gap:4px;display:grid}.config-nav a{min-height:38px;color:var(--config-muted);border:1px solid #0000;border-radius:9px;align-items:center;padding:8px 10px;font-size:13px;text-decoration:none;display:flex}.config-nav a:hover{border-color:var(--config-border);background:var(--config-surface-2);color:#e2e8f0}.config-nav a.is-active{color:#dbeafe;background:#172554;border-color:#1d4ed8}.config-sidebar-note{border:1px solid var(--config-border);color:var(--config-muted);border-radius:10px;margin:0;padding:12px;font-size:12px;line-height:1.6}.config-main{min-width:0}.config-header{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:18px;display:flex}.config-header h1{color:#f8fafc;margin:2px 0 4px;font-size:clamp(24px,4vw,32px);line-height:1.2}.config-eyebrow{color:#60a5fa;letter-spacing:.14em;text-transform:uppercase;margin:0;font-size:10px;font-weight:700}.config-subtitle{max-width:62ch;color:var(--config-muted);margin:0;font-size:13px;line-height:1.6}.config-header-actions{flex-wrap:wrap;gap:8px;display:flex}.config-mobile-nav{gap:6px;margin-bottom:14px;padding-bottom:2px;display:none;overflow-x:auto}.config-mobile-nav a{border:1px solid var(--config-border);color:var(--config-muted);border-radius:999px;flex:none;padding:7px 10px;font-size:12px;text-decoration:none}.config-mobile-nav a.is-active{color:#dbeafe;background:#172554;border-color:#1d4ed8}.config-card{border:1px solid var(--config-border);background:var(--config-surface);border-radius:16px;margin-bottom:16px;padding:clamp(16px,2.4vw,24px);scroll-margin-top:18px}.config-card-header{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:16px;display:flex}.config-card h2{color:#f8fafc;margin:2px 0 0;font-size:18px}.config-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 16px;display:grid}.config-span-2{grid-column:span 2}.config-field{align-content:start;gap:6px;min-width:0;display:grid}.config-field label{color:#cbd5e1;letter-spacing:.01em;font-size:12px;font-weight:650}.config-field-end{align-content:end}.config-optional{color:#64748b;font-size:10px;font-weight:500}.config-help{color:var(--config-muted);margin:0;font-size:11px;line-height:1.55}.config-card-header>.config-help{text-align:right;max-width:34ch}.config-input{border:1px solid var(--config-border);width:100%;min-height:40px;color:var(--config-text);font:inherit;background:#0b1220;border-radius:8px;padding:9px 11px;font-size:13px}.config-input:hover{border-color:#334155}.config-input:focus,.config-input:focus-visible{border-color:var(--config-accent);outline:none;box-shadow:0 0 0 3px #3b82f62e}.config-input::placeholder{color:#64748b}.config-select{appearance:auto}.config-textarea{resize:vertical;min-height:84px}.config-input option{color:var(--config-text);background:#0f172a}.config-input code,.config-help code{border:1px solid var(--config-border);color:#bfdbfe;background:#0b1220;border-radius:4px;padding:1px 4px;font-size:10px}.config-advanced{border-top:1px solid var(--config-border);margin-top:16px;padding-top:14px}.config-advanced summary{color:#93c5fd;cursor:pointer;font-size:12px;font-weight:650}.config-advanced summary:hover{color:#bfdbfe}.config-advanced .config-grid{margin-top:14px}.config-auth-actions{flex-wrap:wrap;gap:8px;display:flex}.config-primary-button,.config-secondary-button{min-height:38px;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;padding:8px 14px;font-size:12px;font-weight:650;text-decoration:none;transition:background-color .15s,border-color .15s;display:inline-flex}.config-primary-button{color:#fff;background:#2563eb}.config-primary-button:hover{background:#1d4ed8}.config-secondary-button{border-color:var(--config-border);background:var(--config-surface-2);color:#dbe4f0}.config-secondary-button:hover{background:#1e293b;border-color:#475569}.config-save-bar{z-index:20;border:1px solid var(--config-border);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:#0f172af0;border-radius:14px;justify-content:space-between;align-items:center;gap:16px;margin-top:4px;padding:12px 14px;display:flex;position:sticky;bottom:10px;box-shadow:0 12px 32px #00000059}.config-save-status{gap:2px;display:grid}.config-save-state{color:#cbd5e1;font-size:12px;font-weight:650}.config-save-state.is-dirty{color:#fbbf24}.config-save-state.is-saving{color:#93c5fd}.config-save-actions{flex:none;gap:8px;display:flex}.config-safety-card{border:1px solid var(--config-border);background:var(--config-surface);border-radius:18px;width:min(100%,460px);padding:28px;box-shadow:0 24px 80px #00000080}.config-safety-card h1{color:#f8fafc;margin:4px 0 8px;font-size:22px;line-height:1.25}.config-safety-card .config-subtitle{font-size:13px}.login-page{background:radial-gradient(circle at 50% 0%, #13213b 0, var(--config-bg) 48%);justify-content:center;align-items:center;min-height:100vh;padding:24px;display:flex}.login-card{border:1px solid var(--config-border);background:var(--config-surface);border-radius:18px;width:100%;max-width:380px;padding:30px;box-shadow:0 24px 80px #00000073}.login-card h1{color:#f8fafc;margin:0 0 4px;font-size:22px}.login-hint{color:var(--config-muted);margin:0 0 20px;font-size:12px;line-height:1.6}.login-field{gap:6px;display:grid}.login-field label{color:#cbd5e1;font-size:12px;font-weight:650}.login-error{color:#f87171;min-height:18px;margin:12px 0 0;font-size:12px}@media (max-width:960px){.config-shell{grid-template-columns:1fr;gap:0}.config-sidebar{display:none}.config-mobile-nav{display:flex}.config-header{margin-bottom:14px}.config-card{scroll-margin-top:58px}}@media (max-width:640px){.config-page{padding:12px}.config-header{flex-direction:column}.config-header-actions{width:100%}.config-header-actions>*{flex:1}.config-grid{grid-template-columns:1fr}.config-span-2{grid-column:span 1}.config-card-header{flex-direction:column;gap:6px}.config-card-header>.config-help{text-align:left;max-width:none}.config-save-bar{border-radius:12px 12px 0 0;flex-direction:row;align-items:center;padding:10px 12px;bottom:0}.config-save-status .config-help{display:none}.config-save-actions{flex:none}.config-save-actions .config-primary-button{width:auto}.login-page{padding:16px}.login-card{padding:24px}}.console-panel{height:calc(100dvh - 32px);min-height:360px;padding:clamp(12px,2vw,24px)}.console-panel pre{overflow-wrap:anywhere;min-height:0}.app-ui pre{tab-size:4}@media (max-width:640px){.app-ui{padding:12px}.app-ui nav a,.app-ui button,.app-ui select{min-height:40px}.app-ui h1{font-size:1.4rem}.page-header{align-items:flex-start}.page-header nav{width:100%}.app-ui .mobile-wrap{flex-wrap:wrap;gap:8px}.app-ui .mobile-stack{flex-direction:column;align-items:stretch}.app-ui #safetyOverlay{padding:12px}.app-ui #safetyOverlay>div{margin:0;padding:24px 16px}}.runtime-page{color-scheme:dark;background:#0b1220}.runtime-option{background:#0b1220;border:1px solid #273449;border-radius:10px;gap:6px;padding:10px 12px;display:grid}.runtime-option-label{color:#e2e8f0;cursor:pointer;align-items:center;gap:8px;font-size:13px;display:flex}.runtime-option input[type=checkbox]{accent-color:#3b82f6;width:16px;height:16px}.runtime-account{color:#e2e8f0;width:100%;font:inherit;background:#0f172a;border:1px solid #334155;border-radius:7px;margin-top:2px;padding:7px 9px;font-size:12px}.runtime-account:focus{border-color:#3b82f6;outline:none;box-shadow:0 0 0 3px #3b82f62e}.runtime-page fieldset:disabled .runtime-option{opacity:.72}.runtime-card{background:#111c31;border:1px solid #273449;border-radius:16px;margin-bottom:16px;padding:clamp(16px,2.4vw,24px)}.runtime-card-head{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:14px;display:flex}.runtime-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.runtime-button{min-height:36px;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;padding:7px 12px;font-size:13px;font-weight:650;text-decoration:none;display:inline-flex}.runtime-button:disabled{cursor:default;opacity:.5}.runtime-button-primary{color:#fff;background:#2563eb}.runtime-button-primary:hover{background:#1d4ed8}.runtime-button-secondary{color:#dbe4f0;background:#0f172a;border-color:#334155}.runtime-button-secondary:hover{background:#1e293b;border-color:#475569}.runtime-button-danger{color:#fee2e2;background:#7f1d1d;border-color:#991b1b}.runtime-button-danger:hover{background:#991b1b}.runtime-state-dot{background:#64748b;border-radius:999px;flex:none;width:12px;height:12px;margin-top:5px;box-shadow:0 0 0 4px #64748b29}.runtime-state-dot[data-state=running]{background:#22c55e;box-shadow:0 0 0 4px #22c55e29}.runtime-state-dot[data-state=starting]{background:#38bdf8;box-shadow:0 0 0 4px #38bdf829}.runtime-state-dot[data-state=stopping]{background:#f59e0b;box-shadow:0 0 0 4px #f59e0b29}.runtime-state-dot[data-state=failed]{background:#ef4444;box-shadow:0 0 0 4px #ef444429}.runtime-metric-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:16px;display:grid}.runtime-metric{background:#0b1220;border:1px solid #273449;border-radius:10px;min-width:0;padding:10px 11px}.runtime-metric span{color:#94a3b8;margin-bottom:4px;font-size:11px;display:block}.runtime-metric strong{overflow-wrap:anywhere;color:#f1f5f9;font-size:13px;font-weight:650;display:block}.runtime-source-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:4px;display:grid}.runtime-source{background:#0b1220;border:1px solid #273449;border-radius:12px;min-width:0;padding:14px}.runtime-source-title{color:#f1f5f9;margin:10px 0 4px;font-size:14px;font-weight:650}.runtime-source-detail{color:#94a3b8;margin:0;font-size:12px;line-height:1.6}.runtime-mode-badge{color:#cbd5e1;border:1px solid #334155;border-radius:999px;flex:none;padding:3px 8px;font-size:11px}.runtime-mode-badge[data-mode=native]{color:#bfdbfe;background:#172554;border-color:#1d4ed8}.runtime-mode-badge[data-mode=browser]{color:#ddd6fe;background:#2e1065;border-color:#6d28d9}.runtime-mode-badge[data-mode=manual]{color:#fde68a;background:#451a03;border-color:#b45309}.runtime-toast-region{z-index:60;pointer-events:none;gap:8px;width:min(360px,100vw - 32px);display:grid;position:fixed;bottom:16px;right:16px}.runtime-toast{color:#e2e8f0;background:#111c31;border:1px solid #334155;border-left:3px solid #60a5fa;border-radius:9px;padding:10px 12px;font-size:12px;line-height:1.5;animation:.16s ease-out runtime-toast-in;box-shadow:0 12px 32px #00000059}.runtime-toast-success{border-left-color:#22c55e}.runtime-toast-error{border-left-color:#ef4444}.runtime-toast-info{border-left-color:#60a5fa}@keyframes runtime-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){.runtime-toast{animation:none}}@media (max-width:900px){.runtime-metric-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.runtime-source-grid{grid-template-columns:1fr}}@media (max-width:640px){.runtime-page{padding:12px}.runtime-card{padding:16px}.runtime-card-head{flex-direction:column}.runtime-actions{width:100%}.runtime-actions>*,.runtime-actions form{flex:1}.runtime-actions .runtime-button{width:100%}.runtime-toast-region{width:calc(100vw - 24px);bottom:12px;right:12px}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}\ No newline at end of file+@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-red-600:oklch(57.7% .245 27.325);--color-red-700:oklch(50.5% .213 27.518);--color-red-800:oklch(44.4% .177 26.899);--color-red-900:oklch(39.6% .141 25.723);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-orange-900:oklch(40.8% .123 38.172);--color-amber-300:oklch(87.9% .169 91.605);--color-amber-400:oklch(82.8% .189 84.429);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-600:oklch(66.6% .179 58.318);--color-amber-900:oklch(41.4% .112 45.904);--color-yellow-200:oklch(94.5% .129 101.54);--color-yellow-300:oklch(90.5% .182 98.111);--color-yellow-400:oklch(85.2% .199 91.936);--color-yellow-500:oklch(79.5% .184 86.047);--color-yellow-600:oklch(68.1% .162 75.834);--color-yellow-700:oklch(55.4% .135 66.442);--color-yellow-900:oklch(42.1% .095 57.708);--color-lime-300:oklch(89.7% .196 126.665);--color-lime-900:oklch(40.5% .101 131.063);--color-green-200:oklch(92.5% .084 155.995);--color-green-300:oklch(87.1% .15 154.449);--color-green-400:oklch(79.2% .209 151.711);--color-green-500:oklch(72.3% .219 149.579);--color-green-600:oklch(62.7% .194 149.214);--color-green-700:oklch(52.7% .154 150.069);--color-green-900:oklch(39.3% .095 152.535);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-400:oklch(76.5% .177 163.223);--color-emerald-500:oklch(69.6% .17 162.48);--color-emerald-600:oklch(59.6% .145 163.225);--color-teal-300:oklch(85.5% .138 181.071);--color-teal-500:oklch(70.4% .14 182.503);--color-teal-600:oklch(60% .118 184.704);--color-teal-900:oklch(38.6% .063 188.416);--color-cyan-300:oklch(86.5% .127 207.078);--color-cyan-400:oklch(78.9% .154 211.53);--color-cyan-500:oklch(71.5% .143 215.221);--color-cyan-600:oklch(60.9% .126 221.723);--color-cyan-900:oklch(39.8% .07 227.392);--color-sky-200:oklch(90.1% .058 230.902);--color-sky-300:oklch(82.8% .111 230.318);--color-sky-400:oklch(74.6% .16 232.661);--color-sky-500:oklch(68.5% .169 237.323);--color-sky-600:oklch(58.8% .158 241.966);--color-blue-300:oklch(80.9% .105 251.813);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-900:oklch(37.9% .146 265.522);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-900:oklch(35.9% .144 278.697);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-300:oklch(82.7% .119 306.383);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-purple-700:oklch(49.6% .265 301.924);--color-purple-900:oklch(38.1% .176 304.987);--color-fuchsia-300:oklch(83.3% .145 321.434);--color-fuchsia-900:oklch(40.1% .17 325.612);--color-pink-300:oklch(82.3% .12 346.018);--color-pink-900:oklch(40.8% .153 2.432);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-700:oklch(37.2% .044 257.287);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--container-3xl:48rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-widest:.1em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.top-0{top:0}.top-1\/2{top:50%}.top-2{top:calc(var(--spacing) * 2)}.top-6{top:calc(var(--spacing) * 6)}.right-2{right:calc(var(--spacing) * 2)}.right-6{right:calc(var(--spacing) * 6)}.left-2{left:calc(var(--spacing) * 2)}.z-50{z-index:50}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.mx-0\.5{margin-inline:calc(var(--spacing) * .5)}.mx-auto{margin-inline:auto}.my-1{margin-block:var(--spacing)}.my-2{margin-block:calc(var(--spacing) * 2)}.my-3{margin-block:calc(var(--spacing) * 3)}.my-4{margin-block:calc(var(--spacing) * 4)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mr-1{margin-right:var(--spacing)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.\!mb-0{margin-bottom:0!important}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-12{height:calc(var(--spacing) * 12)}.h-40{height:calc(var(--spacing) * 40)}.h-52{height:calc(var(--spacing) * 52)}.h-full{height:100%}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-80{max-height:calc(var(--spacing) * 80)}.max-h-\[80dvh\]{max-height:80dvh}.max-h-\[85vh\]{max-height:85vh}.min-h-0{min-height:0}.min-h-full{min-height:100%}.min-h-screen{min-height:100vh}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-8{width:calc(var(--spacing) * 8)}.w-12{width:calc(var(--spacing) * 12)}.w-24{width:calc(var(--spacing) * 24)}.w-96{width:calc(var(--spacing) * 96)}.w-full{width:100%}.max-w-3xl{max-width:var(--container-3xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[1600px\]{max-width:1600px}.max-w-sm{max-width:var(--container-sm)}.min-w-0{min-width:0}.min-w-40{min-width:calc(var(--spacing) * 40)}.min-w-48{min-width:calc(var(--spacing) * 48)}.min-w-\[200px\]{min-width:200px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-6{gap:calc(var(--spacing) * 6)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}:where(.space-x-2>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-3>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse)))}:where(.space-x-4>:not(:last-child)){--tw-space-x-reverse:0;margin-inline-start:calc(calc(var(--spacing) * 4) * var(--tw-space-x-reverse));margin-inline-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-x-reverse)))}.gap-y-1{row-gap:var(--spacing)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-blue-500\/40{border-color:#3080ff66}@supports (color:color-mix(in lab, red, red)){.border-blue-500\/40{border-color:color-mix(in oklab, var(--color-blue-500) 40%, transparent)}}.border-gray-600{border-color:var(--color-gray-600)}.border-gray-700{border-color:var(--color-gray-700)}.border-gray-700\/60{border-color:#36415399}@supports (color:color-mix(in lab, red, red)){.border-gray-700\/60{border-color:color-mix(in oklab, var(--color-gray-700) 60%, transparent)}}.border-gray-800{border-color:var(--color-gray-800)}.border-gray-800\/60{border-color:#1e293999}@supports (color:color-mix(in lab, red, red)){.border-gray-800\/60{border-color:color-mix(in oklab, var(--color-gray-800) 60%, transparent)}}.border-green-500\/40{border-color:#00c75866}@supports (color:color-mix(in lab, red, red)){.border-green-500\/40{border-color:color-mix(in oklab, var(--color-green-500) 40%, transparent)}}.border-green-600{border-color:var(--color-green-600)}.border-purple-500\/40{border-color:#ac4bff66}@supports (color:color-mix(in lab, red, red)){.border-purple-500\/40{border-color:color-mix(in oklab, var(--color-purple-500) 40%, transparent)}}.border-red-500\/40{border-color:#fb2c3666}@supports (color:color-mix(in lab, red, red)){.border-red-500\/40{border-color:color-mix(in oklab, var(--color-red-500) 40%, transparent)}}.border-red-600{border-color:var(--color-red-600)}.border-transparent{border-color:#0000}.border-yellow-500\/40{border-color:#edb20066}@supports (color:color-mix(in lab, red, red)){.border-yellow-500\/40{border-color:color-mix(in oklab, var(--color-yellow-500) 40%, transparent)}}.border-yellow-600{border-color:var(--color-yellow-600)}.border-l-blue-500{border-left-color:var(--color-blue-500)}.border-l-gray-600{border-left-color:var(--color-gray-600)}.border-l-green-600{border-left-color:var(--color-green-600)}.border-l-purple-500{border-left-color:var(--color-purple-500)}.border-l-red-500{border-left-color:var(--color-red-500)}.bg-amber-600{background-color:var(--color-amber-600)}.bg-amber-900\/60{background-color:#7b330699}@supports (color:color-mix(in lab, red, red)){.bg-amber-900\/60{background-color:color-mix(in oklab, var(--color-amber-900) 60%, transparent)}}.bg-black{background-color:var(--color-black)}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab, red, red)){.bg-black\/60{background-color:color-mix(in oklab, var(--color-black) 60%, transparent)}}.bg-black\/70{background-color:#000000b3}@supports (color:color-mix(in lab, red, red)){.bg-black\/70{background-color:color-mix(in oklab, var(--color-black) 70%, transparent)}}.bg-black\/80{background-color:#000c}@supports (color:color-mix(in lab, red, red)){.bg-black\/80{background-color:color-mix(in oklab, var(--color-black) 80%, transparent)}}.bg-blue-500\/20{background-color:#3080ff33}@supports (color:color-mix(in lab, red, red)){.bg-blue-500\/20{background-color:color-mix(in oklab, var(--color-blue-500) 20%, transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-blue-700{background-color:var(--color-blue-700)}.bg-blue-900\/60{background-color:#1c398e99}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/60{background-color:color-mix(in oklab, var(--color-blue-900) 60%, transparent)}}.bg-blue-900\/70{background-color:#1c398eb3}@supports (color:color-mix(in lab, red, red)){.bg-blue-900\/70{background-color:color-mix(in oklab, var(--color-blue-900) 70%, transparent)}}.bg-cyan-600\/80{background-color:#0092b5cc}@supports (color:color-mix(in lab, red, red)){.bg-cyan-600\/80{background-color:color-mix(in oklab, var(--color-cyan-600) 80%, transparent)}}.bg-cyan-900\/60{background-color:#104e6499}@supports (color:color-mix(in lab, red, red)){.bg-cyan-900\/60{background-color:color-mix(in oklab, var(--color-cyan-900) 60%, transparent)}}.bg-emerald-600\/80{background-color:#009767cc}@supports (color:color-mix(in lab, red, red)){.bg-emerald-600\/80{background-color:color-mix(in oklab, var(--color-emerald-600) 80%, transparent)}}.bg-fuchsia-900\/60{background-color:#72137899}@supports (color:color-mix(in lab, red, red)){.bg-fuchsia-900\/60{background-color:color-mix(in oklab, var(--color-fuchsia-900) 60%, transparent)}}.bg-gray-600{background-color:var(--color-gray-600)}.bg-gray-700{background-color:var(--color-gray-700)}.bg-gray-700\/50{background-color:#36415380}@supports (color:color-mix(in lab, red, red)){.bg-gray-700\/50{background-color:color-mix(in oklab, var(--color-gray-700) 50%, transparent)}}.bg-gray-800{background-color:var(--color-gray-800)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-green-500\/20{background-color:#00c75833}@supports (color:color-mix(in lab, red, red)){.bg-green-500\/20{background-color:color-mix(in oklab, var(--color-green-500) 20%, transparent)}}.bg-green-600{background-color:var(--color-green-600)}.bg-green-900{background-color:var(--color-green-900)}.bg-green-900\/60{background-color:#0d542b99}@supports (color:color-mix(in lab, red, red)){.bg-green-900\/60{background-color:color-mix(in oklab, var(--color-green-900) 60%, transparent)}}.bg-green-900\/80{background-color:#0d542bcc}@supports (color:color-mix(in lab, red, red)){.bg-green-900\/80{background-color:color-mix(in oklab, var(--color-green-900) 80%, transparent)}}.bg-indigo-900\/60{background-color:#312c8599}@supports (color:color-mix(in lab, red, red)){.bg-indigo-900\/60{background-color:color-mix(in oklab, var(--color-indigo-900) 60%, transparent)}}.bg-lime-900\/60{background-color:#35530e99}@supports (color:color-mix(in lab, red, red)){.bg-lime-900\/60{background-color:color-mix(in oklab, var(--color-lime-900) 60%, transparent)}}.bg-orange-900\/60{background-color:#7e2a0c99}@supports (color:color-mix(in lab, red, red)){.bg-orange-900\/60{background-color:color-mix(in oklab, var(--color-orange-900) 60%, transparent)}}.bg-pink-900\/60{background-color:#86104399}@supports (color:color-mix(in lab, red, red)){.bg-pink-900\/60{background-color:color-mix(in oklab, var(--color-pink-900) 60%, transparent)}}.bg-purple-500\/20{background-color:#ac4bff33}@supports (color:color-mix(in lab, red, red)){.bg-purple-500\/20{background-color:color-mix(in oklab, var(--color-purple-500) 20%, transparent)}}.bg-purple-700{background-color:var(--color-purple-700)}.bg-purple-900\/60{background-color:#59168b99}@supports (color:color-mix(in lab, red, red)){.bg-purple-900\/60{background-color:color-mix(in oklab, var(--color-purple-900) 60%, transparent)}}.bg-purple-900\/70{background-color:#59168bb3}@supports (color:color-mix(in lab, red, red)){.bg-purple-900\/70{background-color:color-mix(in oklab, var(--color-purple-900) 70%, transparent)}}.bg-red-500\/20{background-color:#fb2c3633}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/20{background-color:color-mix(in oklab, var(--color-red-500) 20%, transparent)}}.bg-red-500\/80{background-color:#fb2c36cc}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/80{background-color:color-mix(in oklab, var(--color-red-500) 80%, transparent)}}.bg-red-600{background-color:var(--color-red-600)}.bg-red-600\/80{background-color:#e40014cc}@supports (color:color-mix(in lab, red, red)){.bg-red-600\/80{background-color:color-mix(in oklab, var(--color-red-600) 80%, transparent)}}.bg-red-800{background-color:var(--color-red-800)}.bg-red-900{background-color:var(--color-red-900)}.bg-red-900\/60{background-color:#82181a99}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/60{background-color:color-mix(in oklab, var(--color-red-900) 60%, transparent)}}.bg-red-900\/70{background-color:#82181ab3}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/70{background-color:color-mix(in oklab, var(--color-red-900) 70%, transparent)}}.bg-red-900\/80{background-color:#82181acc}@supports (color:color-mix(in lab, red, red)){.bg-red-900\/80{background-color:color-mix(in oklab, var(--color-red-900) 80%, transparent)}}.bg-sky-600{background-color:var(--color-sky-600)}.bg-slate-700{background-color:var(--color-slate-700)}.bg-teal-600\/80{background-color:#009588cc}@supports (color:color-mix(in lab, red, red)){.bg-teal-600\/80{background-color:color-mix(in oklab, var(--color-teal-600) 80%, transparent)}}.bg-teal-900\/60{background-color:#0b4f4a99}@supports (color:color-mix(in lab, red, red)){.bg-teal-900\/60{background-color:color-mix(in oklab, var(--color-teal-900) 60%, transparent)}}.bg-yellow-500\/20{background-color:#edb20033}@supports (color:color-mix(in lab, red, red)){.bg-yellow-500\/20{background-color:color-mix(in oklab, var(--color-yellow-500) 20%, transparent)}}.bg-yellow-600{background-color:var(--color-yellow-600)}.bg-yellow-900\/60{background-color:#733e0a99}@supports (color:color-mix(in lab, red, red)){.bg-yellow-900\/60{background-color:color-mix(in oklab, var(--color-yellow-900) 60%, transparent)}}.bg-yellow-900\/80{background-color:#733e0acc}@supports (color:color-mix(in lab, red, red)){.bg-yellow-900\/80{background-color:color-mix(in oklab, var(--color-yellow-900) 80%, transparent)}}.object-contain{object-fit:contain}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-middle{vertical-align:middle}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-none{--tw-leading:1;line-height:1}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-pre{white-space:pre}.whitespace-pre-wrap{white-space:pre-wrap}.text-amber-300{color:var(--color-amber-300)}.text-amber-400{color:var(--color-amber-400)}.text-blue-300{color:var(--color-blue-300)}.text-blue-400{color:var(--color-blue-400)}.text-cyan-300{color:var(--color-cyan-300)}.text-cyan-400{color:var(--color-cyan-400)}.text-emerald-300{color:var(--color-emerald-300)}.text-emerald-400{color:var(--color-emerald-400)}.text-fuchsia-300{color:var(--color-fuchsia-300)}.text-gray-100{color:var(--color-gray-100)}.text-gray-200{color:var(--color-gray-200)}.text-gray-300{color:var(--color-gray-300)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-green-200{color:var(--color-green-200)}.text-green-300{color:var(--color-green-300)}.text-green-400{color:var(--color-green-400)}.text-indigo-300{color:var(--color-indigo-300)}.text-lime-300{color:var(--color-lime-300)}.text-orange-300{color:var(--color-orange-300)}.text-orange-400{color:var(--color-orange-400)}.text-pink-300{color:var(--color-pink-300)}.text-purple-200{color:var(--color-purple-200)}.text-purple-300{color:var(--color-purple-300)}.text-purple-400{color:var(--color-purple-400)}.text-red-200{color:var(--color-red-200)}.text-red-300{color:var(--color-red-300)}.text-red-400{color:var(--color-red-400)}.text-sky-200{color:var(--color-sky-200)}.text-sky-300{color:var(--color-sky-300)}.text-sky-400{color:var(--color-sky-400)}.text-slate-300{color:var(--color-slate-300)}.text-teal-300{color:var(--color-teal-300)}.text-white{color:var(--color-white)}.text-yellow-200{color:var(--color-yellow-200)}.text-yellow-300{color:var(--color-yellow-300)}.text-yellow-400{color:var(--color-yellow-400)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.accent-blue-500{accent-color:var(--color-blue-500)}.accent-red-500{accent-color:var(--color-red-500)}.opacity-60{opacity:.6}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px var(--tw-shadow-color,#0000001a), 0 2px 4px -2px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:text-gray-300:is(:where(.group):hover *){color:var(--color-gray-300)}.hover\:bg-amber-500:hover{background-color:var(--color-amber-500)}.hover\:bg-blue-500:hover{background-color:var(--color-blue-500)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-cyan-500:hover{background-color:var(--color-cyan-500)}.hover\:bg-emerald-500:hover{background-color:var(--color-emerald-500)}.hover\:bg-gray-500:hover{background-color:var(--color-gray-500)}.hover\:bg-gray-600:hover{background-color:var(--color-gray-600)}.hover\:bg-gray-700:hover{background-color:var(--color-gray-700)}.hover\:bg-gray-900\/50:hover{background-color:#10182880}@supports (color:color-mix(in lab, red, red)){.hover\:bg-gray-900\/50:hover{background-color:color-mix(in oklab, var(--color-gray-900) 50%, transparent)}}.hover\:bg-green-500:hover{background-color:var(--color-green-500)}.hover\:bg-green-700:hover{background-color:var(--color-green-700)}.hover\:bg-red-500:hover{background-color:var(--color-red-500)}.hover\:bg-red-700:hover{background-color:var(--color-red-700)}.hover\:bg-sky-500:hover{background-color:var(--color-sky-500)}.hover\:bg-teal-500:hover{background-color:var(--color-teal-500)}.hover\:bg-yellow-700:hover{background-color:var(--color-yellow-700)}.hover\:text-blue-300:hover{color:var(--color-blue-300)}.hover\:text-gray-300:hover{color:var(--color-gray-300)}.hover\:text-red-300:hover{color:var(--color-red-300)}.hover\:text-sky-300:hover{color:var(--color-sky-300)}.hover\:text-white:hover{color:var(--color-white)}.hover\:underline:hover{text-decoration-line:underline}.hover\:ring-2:hover{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.hover\:ring-blue-500:hover{--tw-ring-color:var(--color-blue-500)}}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width:40rem){.sm\:inline{display:inline}.sm\:h-52{height:calc(var(--spacing) * 52)}.sm\:h-64{height:calc(var(--spacing) * 64)}.sm\:flex-none{flex:none}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:items-end{align-items:flex-end}.sm\:justify-between{justify-content:space-between}.sm\:gap-6{gap:calc(var(--spacing) * 6)}:where(.sm\:space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.sm\:p-4{padding:calc(var(--spacing) * 4)}.sm\:p-5{padding:calc(var(--spacing) * 5)}.sm\:p-6{padding:calc(var(--spacing) * 6)}.sm\:px-5{padding-inline:calc(var(--spacing) * 5)}.sm\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.sm\:text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}}@media (min-width:48rem){.md\:col-span-2{grid-column:span 2/span 2}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}}@media (min-width:80rem){.xl\:col-span-2{grid-column:span 2/span 2}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}}.app-ui:not(.font-mono){font-family:system-ui,Microsoft JhengHei,sans-serif}.app-ui button,.app-ui input,.app-ui select,.app-ui textarea{max-width:100%}.app-ui button:disabled{cursor:default;opacity:.55}.app-ui :is(button,a,input,select,textarea,summary):focus-visible{outline-offset:3px;outline:2px solid #60a5fa}.app-ui nav{flex-wrap:wrap}.app-ui nav a{align-items:center;min-height:36px;display:inline-flex}.page-header{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;display:flex}.page-header h1{flex-shrink:0}.app-ui .flex>*,.app-ui .grid>*{min-width:0}.app-ui [role=dialog],.app-ui #safetyOverlay>div{max-height:calc(100dvh - 32px);overflow-y:auto}.config-page,.login-page{color-scheme:dark;--config-bg:#070b12;--config-surface:#0f172a;--config-surface-2:#111c31;--config-border:#243147;--config-text:#e5edf8;--config-muted:#94a3b8;--config-accent:#3b82f6;background:var(--config-bg);color:var(--config-text)}.config-page{padding:clamp(12px,3vw,32px);display:block}.config-page #configContent{width:100%;max-width:1180px;margin:auto}.config-shell{grid-template-columns:224px minmax(0,1fr);align-items:start;gap:24px;display:grid}.config-sidebar{gap:18px;display:grid;position:sticky;top:20px}.config-brand{align-items:center;gap:10px;padding:2px 4px;display:flex}.config-brand-mark{color:#eff6ff;letter-spacing:.06em;background:#1d4ed8;border-radius:12px;flex:none;justify-content:center;align-items:center;width:38px;height:38px;font-size:12px;font-weight:800;line-height:1;display:inline-flex}.config-brand-copy{gap:2px;min-width:0;display:grid}.config-brand-copy strong{color:#f8fafc;font-size:14px;line-height:1.15;display:block}.config-brand-copy span{color:var(--config-muted);font-size:12px;line-height:1.2;display:block}.config-nav{gap:4px;display:grid}.config-nav a{min-height:38px;color:var(--config-muted);border:1px solid #0000;border-radius:9px;align-items:center;padding:8px 10px;font-size:13px;text-decoration:none;display:flex}.config-nav a:hover{border-color:var(--config-border);background:var(--config-surface-2);color:#e2e8f0}.config-nav a.is-active{color:#dbeafe;background:#172554;border-color:#1d4ed8}.config-sidebar-note{border:1px solid var(--config-border);color:var(--config-muted);border-radius:10px;margin:0;padding:12px;font-size:12px;line-height:1.6}.config-main{min-width:0}.config-header{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:18px;display:flex}.config-header h1{color:#f8fafc;margin:2px 0 4px;font-size:clamp(24px,4vw,32px);line-height:1.2}.config-eyebrow{color:#60a5fa;letter-spacing:.14em;text-transform:uppercase;margin:0;font-size:10px;font-weight:700}.config-subtitle{max-width:62ch;color:var(--config-muted);margin:0;font-size:13px;line-height:1.6}.config-header-actions{flex-wrap:wrap;gap:8px;display:flex}.config-mobile-nav{gap:6px;margin-bottom:14px;padding-bottom:2px;display:none;overflow-x:auto}.config-mobile-nav a{border:1px solid var(--config-border);color:var(--config-muted);border-radius:999px;flex:none;padding:7px 10px;font-size:12px;text-decoration:none}.config-mobile-nav a.is-active{color:#dbeafe;background:#172554;border-color:#1d4ed8}.config-card{border:1px solid var(--config-border);background:var(--config-surface);border-radius:16px;margin-bottom:16px;padding:clamp(16px,2.4vw,24px);scroll-margin-top:18px}.config-card-header{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:16px;display:flex}.config-card h2{color:#f8fafc;margin:2px 0 0;font-size:18px}.config-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:14px 16px;display:grid}.config-span-2{grid-column:span 2}.config-field{align-content:start;gap:6px;min-width:0;display:grid}.config-field label{color:#cbd5e1;letter-spacing:.01em;font-size:12px;font-weight:650}.config-field-end{align-content:end}.config-optional{color:#64748b;font-size:10px;font-weight:500}.config-help{color:var(--config-muted);margin:0;font-size:11px;line-height:1.55}.config-card-header>.config-help{text-align:right;max-width:34ch}.config-input{border:1px solid var(--config-border);width:100%;min-height:40px;color:var(--config-text);font:inherit;background:#0b1220;border-radius:8px;padding:9px 11px;font-size:13px}.config-input:hover{border-color:#334155}.config-input:focus,.config-input:focus-visible{border-color:var(--config-accent);outline:none;box-shadow:0 0 0 3px #3b82f62e}.config-input::placeholder{color:#64748b}.config-select{appearance:auto}.config-textarea{resize:vertical;min-height:84px}.config-input option{color:var(--config-text);background:#0f172a}.config-input code,.config-help code{border:1px solid var(--config-border);color:#bfdbfe;background:#0b1220;border-radius:4px;padding:1px 4px;font-size:10px}.config-advanced{border-top:1px solid var(--config-border);margin-top:16px;padding-top:14px}.config-advanced summary{color:#93c5fd;cursor:pointer;font-size:12px;font-weight:650}.config-advanced summary:hover{color:#bfdbfe}.config-advanced .config-grid{margin-top:14px}.config-auth-actions{flex-wrap:wrap;gap:8px;display:flex}.config-primary-button,.config-secondary-button{min-height:38px;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;padding:8px 14px;font-size:12px;font-weight:650;text-decoration:none;transition:background-color .15s,border-color .15s;display:inline-flex}.config-primary-button{color:#fff;background:#2563eb}.config-primary-button:hover{background:#1d4ed8}.config-secondary-button{border-color:var(--config-border);background:var(--config-surface-2);color:#dbe4f0}.config-secondary-button:hover{background:#1e293b;border-color:#475569}.config-save-bar{z-index:20;border:1px solid var(--config-border);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:#0f172af0;border-radius:14px;justify-content:space-between;align-items:center;gap:16px;margin-top:4px;padding:12px 14px;display:flex;position:sticky;bottom:10px;box-shadow:0 12px 32px #00000059}.config-save-status{gap:2px;display:grid}.config-save-state{color:#cbd5e1;font-size:12px;font-weight:650}.config-save-state.is-dirty{color:#fbbf24}.config-save-state.is-saving{color:#93c5fd}.config-save-actions{flex:none;gap:8px;display:flex}.config-safety-card{border:1px solid var(--config-border);background:var(--config-surface);border-radius:18px;width:min(100%,460px);padding:28px;box-shadow:0 24px 80px #00000080}.config-safety-card h1{color:#f8fafc;margin:4px 0 8px;font-size:22px;line-height:1.25}.config-safety-card .config-subtitle{font-size:13px}.login-page{background:radial-gradient(circle at 50% 0%, #13213b 0, var(--config-bg) 48%);justify-content:center;align-items:center;min-height:100vh;padding:24px;display:flex}.login-card{border:1px solid var(--config-border);background:var(--config-surface);border-radius:18px;width:100%;max-width:380px;padding:30px;box-shadow:0 24px 80px #00000073}.login-card h1{color:#f8fafc;margin:0 0 4px;font-size:22px}.login-hint{color:var(--config-muted);margin:0 0 20px;font-size:12px;line-height:1.6}.login-field{gap:6px;display:grid}.login-field label{color:#cbd5e1;font-size:12px;font-weight:650}.login-error{color:#f87171;min-height:18px;margin:12px 0 0;font-size:12px}@media (max-width:960px){.config-shell{grid-template-columns:1fr;gap:0}.config-sidebar{display:none}.config-mobile-nav{display:flex}.config-header{margin-bottom:14px}.config-card{scroll-margin-top:58px}}@media (max-width:640px){.config-page{padding:12px}.config-header{flex-direction:column}.config-header-actions{width:100%}.config-header-actions>*{flex:1}.config-grid{grid-template-columns:1fr}.config-span-2{grid-column:span 1}.config-card-header{flex-direction:column;gap:6px}.config-card-header>.config-help{text-align:left;max-width:none}.config-save-bar{border-radius:12px 12px 0 0;flex-direction:row;align-items:center;padding:10px 12px;bottom:0}.config-save-status .config-help{display:none}.config-save-actions{flex:none}.config-save-actions .config-primary-button{width:auto}.login-page{padding:16px}.login-card{padding:24px}}.console-panel{height:calc(100dvh - 32px);min-height:360px;padding:clamp(12px,2vw,24px)}.console-panel pre{overflow-wrap:anywhere;min-height:0}.app-ui pre{tab-size:4}@media (max-width:640px){.app-ui{padding:12px}.app-ui nav a,.app-ui button,.app-ui select{min-height:40px}.app-ui h1{font-size:1.4rem}.page-header{align-items:flex-start}.page-header nav{width:100%}.app-ui .mobile-wrap{flex-wrap:wrap;gap:8px}.app-ui .mobile-stack{flex-direction:column;align-items:stretch}.app-ui #safetyOverlay{padding:12px}.app-ui #safetyOverlay>div{margin:0;padding:24px 16px}}.runtime-page{color-scheme:dark;background:#0b1220}.runtime-option{background:#0b1220;border:1px solid #273449;border-radius:10px;gap:6px;padding:10px 12px;display:grid}.runtime-option-label{color:#e2e8f0;cursor:pointer;align-items:center;gap:8px;font-size:13px;display:flex}.runtime-option input[type=checkbox]{accent-color:#3b82f6;width:16px;height:16px}.runtime-account{color:#e2e8f0;width:100%;font:inherit;background:#0f172a;border:1px solid #334155;border-radius:7px;margin-top:2px;padding:7px 9px;font-size:12px}.runtime-account:focus{border-color:#3b82f6;outline:none;box-shadow:0 0 0 3px #3b82f62e}.runtime-page fieldset:disabled .runtime-option{opacity:.72}.runtime-card{background:#111c31;border:1px solid #273449;border-radius:16px;margin-bottom:16px;padding:clamp(16px,2.4vw,24px)}.runtime-card-head{justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:14px;display:flex}.runtime-actions{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.runtime-button{min-height:36px;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;justify-content:center;align-items:center;padding:7px 12px;font-size:13px;font-weight:650;text-decoration:none;display:inline-flex}.runtime-button:disabled{cursor:default;opacity:.5}.runtime-button-primary{color:#fff;background:#2563eb}.runtime-button-primary:hover{background:#1d4ed8}.runtime-button-secondary{color:#dbe4f0;background:#0f172a;border-color:#334155}.runtime-button-secondary:hover{background:#1e293b;border-color:#475569}.runtime-button-danger{color:#fee2e2;background:#7f1d1d;border-color:#991b1b}.runtime-button-danger:hover{background:#991b1b}.runtime-state-dot{background:#64748b;border-radius:999px;flex:none;width:12px;height:12px;margin-top:5px;box-shadow:0 0 0 4px #64748b29}.runtime-state-dot[data-state=running]{background:#22c55e;box-shadow:0 0 0 4px #22c55e29}.runtime-state-dot[data-state=starting]{background:#38bdf8;box-shadow:0 0 0 4px #38bdf829}.runtime-state-dot[data-state=stopping]{background:#f59e0b;box-shadow:0 0 0 4px #f59e0b29}.runtime-state-dot[data-state=failed]{background:#ef4444;box-shadow:0 0 0 4px #ef444429}.runtime-metric-grid{grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:16px;display:grid}.runtime-metric{background:#0b1220;border:1px solid #273449;border-radius:10px;min-width:0;padding:10px 11px}.runtime-metric span{color:#94a3b8;margin-bottom:4px;font-size:11px;display:block}.runtime-metric strong{overflow-wrap:anywhere;color:#f1f5f9;font-size:13px;font-weight:650;display:block}.runtime-source-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:4px;display:grid}.runtime-source{background:#0b1220;border:1px solid #273449;border-radius:12px;min-width:0;padding:14px}.runtime-source-title{color:#f1f5f9;margin:10px 0 4px;font-size:14px;font-weight:650}.runtime-source-detail{color:#94a3b8;margin:0;font-size:12px;line-height:1.6}.runtime-mode-badge{color:#cbd5e1;border:1px solid #334155;border-radius:999px;flex:none;padding:3px 8px;font-size:11px}.runtime-mode-badge[data-mode=native]{color:#bfdbfe;background:#172554;border-color:#1d4ed8}.runtime-mode-badge[data-mode=browser]{color:#ddd6fe;background:#2e1065;border-color:#6d28d9}.runtime-mode-badge[data-mode=manual]{color:#fde68a;background:#451a03;border-color:#b45309}.runtime-toast-region{z-index:60;pointer-events:none;gap:8px;width:min(360px,100vw - 32px);display:grid;position:fixed;bottom:16px;right:16px}.runtime-toast{color:#e2e8f0;background:#111c31;border:1px solid #334155;border-left:3px solid #60a5fa;border-radius:9px;padding:10px 12px;font-size:12px;line-height:1.5;animation:.16s ease-out runtime-toast-in;box-shadow:0 12px 32px #00000059}.runtime-toast-success{border-left-color:#22c55e}.runtime-toast-error{border-left-color:#ef4444}.runtime-toast-info{border-left-color:#60a5fa}@keyframes runtime-toast-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}@media (prefers-reduced-motion:reduce){.runtime-toast{animation:none}}@media (max-width:900px){.runtime-metric-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.runtime-source-grid{grid-template-columns:1fr}}@media (max-width:640px){.runtime-page{padding:12px}.runtime-card{padding:16px}.runtime-card-head{flex-direction:column}.runtime-actions{width:100%}.runtime-actions>*,.runtime-actions form{flex:1}.runtime-actions .runtime-button{width:100%}.runtime-toast-region{width:calc(100vw - 24px);bottom:12px;right:12px}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}\ No newline at end of filediff --git a/assets/app.css.build.json b/assets/app.css.build.jsonindex b9a7769..1fb52cd 100644--- a/assets/app.css.build.json+++ b/assets/app.css.build.json@@ -1,5 +1,5 @@ {   "schema": 2,-  "inputHash": "c92bdb3e4df68e37b55a1a5dfc2bc438413a9c1b6d91a1ffc53fe2ee318dbcc7",-  "outputHash": "df3dae65108a7226d403072ac930cd57de0eadda75b3e839ef5e31ed20d6b3d5"+  "inputHash": "ce3d9ac0160ed455da2fa5193ed580f4f819a84137146044e9628e8a941a1111",+  "outputHash": "5e8e8ac9b7a490acf78fc06ee9e9c0e63e69a0a32e695f93025dce6a24930dce" }diff --git a/assets/diagnostics.js b/assets/diagnostics.jsnew file mode 100644index 0000000..6792126--- /dev/null+++ b/assets/diagnostics.js@@ -0,0 +1,207 @@+(function () {+    'use strict';++    const $ = id => document.getElementById(id);+    const cards = $('diagCards');+    const recent = $('diagRecent');+    const logsEl = $('diagLogs');+    const statusEl = $('diagStatus');+    const callout = $('diagCallout');+    const search = $('diagSearch');+    const auto = $('diagAuto');+    const autoScroll = $('diagAutoScroll');+    const refresh = $('diagRefresh');+    let state = { logs: [], diagnostics: [], recentDiagnostics: [], stats: {}, lastReceived: null };+    let timer = null;+    let searchTimer = null;+    let loading = false;++    function text(node, value) {+        node.textContent = value == null ? '' : String(value);+        return node;+    }++    function relative(ms) {+        if (!ms) return '尚無';+        const seconds = Math.max(0, Math.round((Date.now() - ms) / 1000));+        if (seconds < 60) return seconds + ' 秒前';+        if (seconds < 3600) return Math.floor(seconds / 60) + ' 分前';+        return Math.floor(seconds / 3600) + ' 小時前';+    }++    function showCallout(message, link) {+        callout.replaceChildren();+        callout.hidden = false;+        callout.append(document.createTextNode(message + ' '));+        if (link) {+            const a = document.createElement('a');+            a.href = link;+            a.className = 'text-blue-400 underline';+            a.textContent = '前往登入';+            callout.append(a);+        }+    }++    function hideCallout() {+        callout.hidden = true;+        callout.replaceChildren();+    }++    function renderCards() {+        cards.replaceChildren();+        const diagnostics = state.diagnostics || [];+        if (!diagnostics.length) {+            const empty = document.createElement('div');+            empty.className = 'diag-empty';+            empty.textContent = '尚無 logio 診斷。';+            cards.append(empty);+            return;+        }+        for (const item of diagnostics) {+            const box = document.createElement('div');+            box.className = 'mb-3';+            const title = document.createElement('div');+            title.className = 'text-sm text-gray-200';+            text(title, `${item.subsystem} · ${item.source}`);+            box.append(title);+            for (const [label, value] of [+                ['次數', item.count],+                ['最近錯誤', item.lastError || '(無)'],+                ['最後收到', relative(item.receivedAt)]+            ]) {+                const row = document.createElement('div');+                row.className = 'diag-metric';+                const key = document.createElement('span');+                key.className = 'text-gray-400';+                text(key, label);+                const val = document.createElement('span');+                val.className = 'text-right text-gray-200';+                text(val, value);+                row.append(key, val);+                box.append(row);+            }+            cards.append(box);+        }+    }++    function renderRecent() {+        recent.replaceChildren();+        const items = (state.recentDiagnostics || []).slice(0, 20);+        if (!items.length) {+            const empty = document.createElement('div');+            empty.className = 'diag-empty';+            empty.textContent = '尚無資料。';+            recent.append(empty);+            return;+        }+        for (const item of items) {+            const row = document.createElement('div');+            row.className = 'diag-log';+            const time = document.createElement('time');+            text(time, relative(item.receivedAt));+            const strong = document.createElement('strong');+            text(strong, `${item.subsystem}/${item.source}`);+            const p = document.createElement('p');+            text(p, `count=${item.count} ${item.lastError || ''}`);+            row.append(time, strong, p);+            recent.append(row);+        }+    }++    function scrollLogsToLatest() {+        if (!autoScroll.checked) return;+        requestAnimationFrame(() => {+            logsEl.scrollTop = logsEl.scrollHeight;+        });+    }++    function renderLogs() {+        const previousTop = logsEl.scrollTop;+        logsEl.replaceChildren();+        const logs = state.logs || [];+        if (!logs.length) {+            const empty = document.createElement('div');+            empty.className = 'diag-empty';+            empty.textContent = '沒有符合的日誌。';+            logsEl.append(empty);+            return;+        }+        // store 回傳新到舊;畫面改成舊到新,最新保持在底部。+        for (const entry of [...logs].reverse()) {+            const row = document.createElement('div');+            row.className = 'diag-log';+            const time = document.createElement('time');+            text(time, entry.time || new Date(entry.receivedAt).toLocaleString());+            const strong = document.createElement('strong');+            text(strong, entry.title || 'log');+            const p = document.createElement('p');+            text(p, (entry.body || '').slice(0, 4000));+            row.append(time, strong, p);+            logsEl.append(row);+        }+        if (autoScroll.checked) {+            scrollLogsToLatest();+        } else {+            requestAnimationFrame(() => { logsEl.scrollTop = previousTop; });+        }+    }++    function render() {+        renderCards();+        renderRecent();+        renderLogs();+        const stats = state.stats || {};+        statusEl.textContent = `最後收到:${relative(state.lastReceived)} · 緩衝 ${stats.bufferedLogs || 0} 則 · 診斷 ${stats.bufferedDiagnostics || 0} 筆`;+    }++    async function load() {+        if (loading) return;+        loading = true;+        try {+            const query = search.value.trim();+            const response = await fetch('/api/diagnostics?limit=500' + (query ? '&q=' + encodeURIComponent(query) : ''), {+                cache: 'no-store'+            });+            if (response.status === 401) {+                auto.checked = false;+                schedule();+                showCallout('診斷資料需要主服務登入。', '/login');+                return;+            }+            if (!response.ok) throw new Error('HTTP ' + response.status);+            const data = await response.json();+            if (!data.ok) throw new Error(data.error || '讀取失敗');+            hideCallout();+            state = data;+            render();+        } catch (error) {+            showCallout('讀取診斷失敗:' + error.message, null);+        } finally {+            loading = false;+        }+    }++    function schedule() {+        if (timer) clearInterval(timer);+        timer = null;+        if (!auto.checked) return;+        timer = setInterval(() => {+            if (document.visibilityState === 'visible') load();+        }, 2000);+    }++    search.addEventListener('input', () => {+        clearTimeout(searchTimer);+        searchTimer = setTimeout(load, 250);+    });+    refresh.addEventListener('click', load);+    auto.addEventListener('change', schedule);+    autoScroll.addEventListener('change', () => {+        if (autoScroll.checked) scrollLogsToLatest();+    });+    document.addEventListener('visibilitychange', () => {+        if (document.visibilityState === 'visible' && auto.checked) load();+    });+    schedule();+    load();+})();diff --git a/diagnostics.html b/diagnostics.htmlnew file mode 100644index 0000000..3eaa843--- /dev/null+++ b/diagnostics.html@@ -0,0 +1,66 @@+<!DOCTYPE html>+<html lang="zh-TW">+<head>+  <meta charset="UTF-8" />+  <meta name="viewport" content="width=device-width, initial-scale=1.0">+  <title>應用診斷</title>+  <link rel="stylesheet" href="/assets/app.css">+  <style>+    .diag-wrap { max-width: 1100px; margin: 0 auto; }+    .diag-card { background:#111827; border:1px solid #334155; border-radius:10px; padding:12px; }+    .diag-card h2 { margin:0 0 8px; font-size:14px; color:#93c5fd; }+    .diag-metric { display:flex; justify-content:space-between; gap:12px; font-size:12px; padding:4px 0; border-top:1px solid #1f2937; }+    .diag-metric:first-of-type { border-top:0; }+    .diag-logs { max-height: 50vh; overflow:auto; border:1px solid #334155; border-radius:8px; background:#0b1220; }+    .diag-log { padding:7px 10px; border-bottom:1px solid #1f2937; font-size:12px; }+    .diag-log time { color:#94a3b8; }+    .diag-log strong { color:#fbbf24; margin-left:6px; }+    .diag-log p { margin:4px 0 0; white-space:pre-wrap; overflow-wrap:anywhere; color:#e5e7eb; }+    .diag-empty { padding:14px; color:#94a3b8; font-size:12px; }+    .diag-status { font-size:12px; color:#94a3b8; }+  </style>+</head>+<body class="app-ui bg-gray-900 text-gray-200 font-mono min-h-screen p-4">+  <div class="diag-wrap">+    <header class="flex items-center justify-between flex-wrap gap-2 mb-4">+      <h1 class="text-xl font-bold text-green-400">應用診斷</h1>+      <nav class="flex gap-2 flex-wrap text-sm">+        <a href="/monitor" class="bg-gray-700 hover:bg-gray-600 px-3 py-1.5 rounded-lg">直播訊息</a>+        <a href="/runtime" class="bg-gray-700 hover:bg-gray-600 px-3 py-1.5 rounded-lg">運行狀態</a>+        <a href="/log" class="bg-gray-700 hover:bg-gray-600 px-3 py-1.5 rounded-lg">系統日誌</a>+        <a href="/diagnostics" class="bg-blue-700 px-3 py-1.5 rounded-lg">診斷</a>+      </nav>+    </header>++    <p id="diagCallout" class="diag-card mb-4" hidden></p>++    <div class="grid gap-3 md:grid-cols-2 mb-4">+      <section class="diag-card">+        <h2>Log I/O</h2>+        <div id="diagCards"><div class="diag-empty">尚無 logio 診斷。</div></div>+      </section>+      <section class="diag-card">+        <h2>最近帶外診斷</h2>+        <div id="diagRecent"><div class="diag-empty">尚無資料。</div></div>+      </section>+    </div>++    <section class="diag-card">+      <div class="flex items-center justify-between flex-wrap gap-2 mb-2">+        <h2 class="!mb-0">應用日誌</h2>+        <div class="flex items-center gap-2 text-xs">+          <input id="diagSearch" type="search" placeholder="搜尋標題或內容…"+                 class="bg-gray-900 border border-gray-700 rounded-lg px-2 py-1 min-w-48">+          <label class="inline-flex items-center gap-1"><input id="diagAuto" type="checkbox" checked> 自動更新</label>+          <label class="inline-flex items-center gap-1"><input id="diagAutoScroll" type="checkbox" checked> 自動滾到最新</label>+          <button id="diagRefresh" class="bg-gray-700 hover:bg-gray-600 px-2 py-1 rounded">重新整理</button>+        </div>+      </div>+      <div id="diagStatus" class="diag-status mb-2"></div>+      <div id="diagLogs" class="diag-logs"><div class="diag-empty">載入中…</div></div>+    </section>+  </div>+  <script src="/assets/i18n.js"></script>+  <script src="/assets/diagnostics.js"></script>+</body>+</html>diff --git a/lang/en.json b/lang/en.jsonindex cda045c..f1ac915 100644--- a/lang/en.json+++ b/lang/en.json@@ -211,6 +211,7 @@   "nav.emoji": "😀 Emoji mappings",   "nav.logViewer": "📋 Log files",   "nav.config": "⚙️ Settings",+  "nav.diagnostics": "🩺 Diagnostics",   "nav.sponsor": "📢 Sponsored ads",   "nav.keyword": "📊 Keywords",   "nav.clips": "🎬 Clips",diff --git a/lang/zh-TW.json b/lang/zh-TW.jsonindex 01a62bc..abf80cc 100644--- a/lang/zh-TW.json+++ b/lang/zh-TW.json@@ -211,6 +211,7 @@   "nav.emoji": "😀 表情映射",   "nav.logViewer": "📋 日誌檔案",   "nav.config": "⚙️ 設定",+  "nav.diagnostics": "🩺 診斷",   "nav.sponsor": "📢 贊助廣告",   "nav.keyword": "📊 關鍵字",   "nav.clips": "🎬 剪輯",diff --git a/log.html b/log.htmlindex 6c76e36..7c7525a 100644--- a/log.html+++ b/log.html@@ -18,6 +18,7 @@         <a href="/logViewer" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.logViewer">📋 日誌檔案</a>         <a href="/config" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.config">⚙️ 設定</a>         <a href="/sponsor" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.sponsor">📢 贊助廣告</a>+        <a href="/diagnostics" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.diagnostics">🩺 診斷</a>         <a href="/keyword" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.keyword">📊 關鍵字</a>         <a href="/clips" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.clips">🎬 剪輯</a>         <a href="/autoclip" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.autoclip">📈 剪輯分析</a>diff --git a/monitor.html b/monitor.htmlindex 6d5f6cc..dd0937c 100644--- a/monitor.html+++ b/monitor.html@@ -19,6 +19,7 @@         <a href="/traffic" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="traffic.title">人流分析</a>         <a href="/restream" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.restream">🎥 Restream 指標</a>         <a href="/config" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.config">⚙️ 設定</a>+        <a href="/diagnostics" class="bg-gray-700 text-gray-200 px-3 py-1.5 rounded-lg hover:bg-gray-600 transition" data-i18n="nav.diagnostics">🩺 診斷</a>         <label style="display:inline-flex;gap:8px;align-items:center"><span data-i18n="common.language">語言</span><select data-language-select style="background:#374151;color:#fff;border-radius:6px;padding:6px"><option value="zh-TW">繁體中文</option></select></label>       </nav>     </div>diff --git a/runtime.html b/runtime.htmlindex 98c6e28..347b51a 100644--- a/runtime.html+++ b/runtime.html@@ -19,6 +19,7 @@         <a href="/browser" data-i18n="common.browser">專用瀏覽器</a>         <a href="/restream" data-i18n="nav.restream">🎥 Restream 指標</a>         <a href="/config" data-i18n="common.config">啟動設定</a>+        <a href="/diagnostics" data-i18n="nav.diagnostics">🩺 診斷</a>         <label class="inline-flex items-center gap-2">           <span data-i18n="common.language">語言</span>           <select data-language-select class="bg-gray-800 text-gray-100 border border-gray-700 rounded-md px-2 py-1">diff --git a/todo.md b/todo.mdindex 8384c2b..299c7d8 100644--- a/todo.md+++ b/todo.md@@ -25,6 +25,7 @@ - [x] 圖片媒體快取 Phase 1:本地 `/media/<hash>`、SocketHub 頭像/禮物圖與 AdOverlay 圖片改寫。 - [x] Python overlay 支援 img/giftImg/AdOverlay icon 與訊息內嵌 data URI。 - [x] monitor MediaCache 檢視/本地連結複製與 Raw Socket 測試入口。+- [x] 應用診斷 `/diagnostics`、LogServer 相容 `/post` 與可選 token。 - [ ] 正式環境以實際 child 程序驗證 IPC 結果與倒數(目前 monitor UI 使用假 SSE/fake `/chat`)。  ## 表情映射後續(2026-11-06)