app · e28eed9

Dev index Recent changes / versions Changelog About TODO

feat(monitor): MediaCache 檢視與 Raw Socket 測試

e28eed9 · 2026-11-10 20:25 · app
Docs/ChangeHistory.mdDocs/MEDIA_CACHE.mdDocs/TEST_FILES_REFERENCE.mdMediaRoutes.cjsScriptLib/media/cache.cjsServer.jsSocketTestRoutes.cjsTest/media_cache.test.cjsTest/socket_test_routes.test.cjsTikTok.jsassets/app.cssassets/app.css.build.jsonassets/monitor.cssassets/monitor.jslang/en.jsonlang/zh-TW.jsonmonitor.htmltodo.md
commit e28eed9b7eb99cee5205347bcd4e1918cfd1c146Author: Nuclear0709 <agp05432@gmail.com>Date:   Tue Nov 10 20:25:58 2026 +0800     feat(monitor): MediaCache 檢視與 Raw Socket 測試        - 新增需登入的 /api/media、/api/media/stats 與 monitor 媒體資源面板    - 支援搜尋/篩選/分頁、預覽、複製本地 media 連結    - 新增 Raw Socket 測試入口,經 IPC 直接 socketHub.send,可選正式 MediaCache/emoji 轉換    - Raw 測試不經 /chat、filter、dedup、traffic、AutoClip,結果走 test-results SSE    - 補路由測試、i18n、CSS 指紋與文件 diff --git a/Docs/ChangeHistory.md b/Docs/ChangeHistory.mdindex b9ccfa8..71ad45d 100644--- a/Docs/ChangeHistory.md+++ b/Docs/ChangeHistory.md@@ -1,5 +1,27 @@ # ChangeHistory +## 2026.11.10 20:24 monitor MediaCache 檢視與 Raw Socket 測試++**類型**: 功能 · **檔案**: `MediaRoutes.cjs`、`ScriptLib/media/cache.cjs`、`SocketTestRoutes.cjs`、`Server.js`、`TikTok.js`、`monitor.html`、`assets/monitor.js`、`assets/monitor.css`、`lang/*.json`、`Test/media_cache.test.cjs`、`Test/socket_test_routes.test.cjs`、`Docs/MEDIA_CACHE.md`、`Docs/TEST_FILES_REFERENCE.md`++### 修改++- MediaCache 新增 `list()`/`stats()` 與需登入的 `/api/media`、`/api/media/stats`。+- monitor 新增 MediaCache 面板:統計、搜尋、型別/狀態篩選、分頁、預覽、複製本地連結與開啟。+- 新增 Raw Socket 測試面板與 `POST /api/socket/test`(需登入):子程序直接 `socketHub.send`,可選 MediaCache/emoji 正式轉換。+- Raw 測試一律 `isTest`,不經 `/chat`、filter、dedup、traffic、AutoClip;結果經既有 test-results SSE 回報。+- 補 MediaCache/Raw Socket 測試與 i18n、CSS fingerprint。++### 驗證++- `node --test Test/media_cache.test.cjs Test/socket_test_routes.test.cjs`+- `node Test/monitor_audience_ui.cjs`+- `npm run test:core`(325 項)+- `python -m pytest OtherTool/live_engine/tests -q`(62 項)+- `npm run lint`、`npm run check:css`++---+ ## 2026.11.10 20:20 Python overlay data URI 支援  **類型**: 功能 · **檔案**: `OtherTool/live_engine/core/emoji_parser.py`、`OtherTool/live_engine/renderer/texture_loader.py`、`OtherTool/live_engine/core/engine.py`、`OtherTool/live_engine/renderer/overlay.py`、`OtherTool/live_engine/tests/test_emoji_parser.py`、`OtherTool/live_engine/tests/test_ad_overlay.py`、`OtherTool/live_engine/tests/test_texture_loader.py`、`OtherTool/live_engine/README.md`diff --git a/Docs/MEDIA_CACHE.md b/Docs/MEDIA_CACHE.mdindex 19f19a4..90f53ff 100644--- a/Docs/MEDIA_CACHE.md+++ b/Docs/MEDIA_CACHE.md@@ -45,6 +45,13 @@ MediaCache 將圖片持久化到本機,訊息只傳本地 URL: - Live 顯示保留 inline;歷史持久化不應直接寫 base64,必要時 materialize 成 `/media/<content-hash>`。 - Python overlay 已在 texture worker 本機解碼 png/jpeg/webp/gif data URI;日誌只記錄類型與長度。 +## 檢視與手動測試++- `/monitor` 的「MediaCache 資源」面板可查看快取統計、搜尋、預覽、複製本地 `/media/<hash>` 連結與開啟圖片。+- `GET /api/media`(需登入)支援 `page`、`pageSize`、`q`、`type`、`status`;只回傳 hash、host、型別、大小、時間與狀態,不暴露完整來源 URL。+- `GET /api/media/stats`(需登入)回傳總數、bytes、cached/failed/missing。+- `/monitor` 另有 Raw Socket 測試入口:`POST /api/socket/test` 經子程序直接送 `socketHub`,可選擇是否套用 MediaCache/emoji 正式轉換;不經 `/chat`、filter、dedup、traffic 或 AutoClip。+ ## 安全限制  - 僅接受 HTTP(S),拒絕 `data:`、`blob:`、`file:` 與含帳密的 URL。diff --git a/Docs/TEST_FILES_REFERENCE.md b/Docs/TEST_FILES_REFERENCE.mdindex ade8b03..2b2580b 100644--- a/Docs/TEST_FILES_REFERENCE.md+++ b/Docs/TEST_FILES_REFERENCE.md@@ -64,6 +64,7 @@ node Test/xxx.mjs | `Test/video_tk_ui.cjs` | videoTK 劇院樣式還原、可拖動按鈕與拖曳不誤觸 | | `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/notify.test.mjs` | 通知格式化與 Bark/ntfy 逐通道結果 | | `Test/traffic_planner_ui.cjs` | 人流分類、批次補分類、預覽/套用/復原與手機寬度 | | `Test/traffic_planner_collecting_ui.cjs` | 資料累積建議、時段分組提醒與一週計畫 UI |diff --git a/MediaRoutes.cjs b/MediaRoutes.cjsindex 4619b36..07ba146 100644--- a/MediaRoutes.cjs+++ b/MediaRoutes.cjs@@ -21,4 +21,45 @@ function createMediaRoutes(cache = defaultMediaCache) {     }; } -module.exports = { createMediaRoutes };+/**+ * MediaCache 管理/檢視 API;需要主服務登入。+ * @param {import('./ScriptLib/media/cache.cjs').MediaCache} [cache]+ * @returns {Function}+ */+function createMediaAdminRoutes(cache = defaultMediaCache) {+    const sendJson = (res, status, value) => {+        res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });+        res.end(JSON.stringify(value));+    };+    return function serveMediaAdmin(req, res, authorized = false) {+        const url = new URL(req.url, 'http://localhost');+        if (!['/api/media', '/api/media/stats'].includes(url.pathname)) return false;+        if (!authorized) {+            sendJson(res, 401, { ok: false, error: '請先登入' });+            return true;+        }+        if (req.method !== 'GET') {+            sendJson(res, 405, { ok: false, error: '只支援 GET' });+            return true;+        }+        try {+            if (url.pathname === '/api/media/stats') {+                sendJson(res, 200, { ok: true, stats: cache.stats() });+                return true;+            }+            const result = cache.list({+                page: Number(url.searchParams.get('page')) || 1,+                pageSize: Number(url.searchParams.get('pageSize')) || 50,+                q: url.searchParams.get('q') || '',+                type: url.searchParams.get('type') || '',+                status: url.searchParams.get('status') || ''+            });+            sendJson(res, 200, { ok: true, ...result });+        } catch (error) {+            sendJson(res, 400, { ok: false, error: error.message });+        }+        return true;+    };+}++module.exports = { createMediaRoutes, createMediaAdminRoutes };diff --git a/ScriptLib/media/cache.cjs b/ScriptLib/media/cache.cjsindex 1a38b0e..ef30646 100644--- a/ScriptLib/media/cache.cjs+++ b/ScriptLib/media/cache.cjs@@ -230,6 +230,59 @@ class MediaCache {         }     } +    statusOf(row) {+        if (!row.content_type || !fs.existsSync(this.filePath(row.hash))) return 'missing';+        if (row.fail_count > 0) return 'failed';+        return 'cached';+    }++    mapRow(row) {+        let host = '';+        try { host = new URL(row.url).hostname; } catch { /* ignore */ }+        return {+            hash: row.hash,+            localUrl: `${this.baseUrl}/media/${row.hash}`,+            host,+            contentType: row.content_type || '',+            byteSize: Number(row.byte_size) || 0,+            fetchedAt: row.fetched_at || null,+            lastAccess: row.last_access || null,+            failCount: row.fail_count || 0,+            status: this.statusOf(row)+        };+    }++    stats() {+        const rows = this.db.prepare('SELECT hash,url,content_type,byte_size,fail_count FROM media').all();+        const result = { total: rows.length, bytes: 0, cached: 0, failed: 0, missing: 0, byType: {} };+        for (const row of rows) {+            result.bytes += Number(row.byte_size) || 0;+            result[this.statusOf(row)] += 1;+            if (row.content_type) result.byType[row.content_type] = (result.byType[row.content_type] || 0) + 1;+        }+        return result;+    }++    list({ page = 1, pageSize = 50, q = '', type = '', status = '' } = {}) {+        const size = Math.min(Math.max(Number(pageSize) || 50, 1), 200);+        const query = String(q || '').trim().toLowerCase();+        const rows = this.db.prepare('SELECT * FROM media ORDER BY last_access DESC, hash DESC').all()+            .map(row => this.mapRow(row))+            .filter(row => {+                if (type && row.contentType !== type) return false;+                if (status && row.status !== status) return false;+                if (!query) return true;+                return row.hash.includes(query) || row.host.toLowerCase().includes(query);+            });+        const pages = Math.max(1, Math.ceil(rows.length / size));+        const current = Math.max(1, Math.min(pages, Number(page) || 1));+        return {+            items: rows.slice((current - 1) * size, current * size),+            total: rows.length, page: current, pageSize: size, pages,+            stats: this.stats()+        };+    }+     close() {         try { this.db.close(); } catch { /* ignore */ }     }diff --git a/Server.js b/Server.jsindex 22adfa4..3aa0428 100644--- a/Server.js+++ b/Server.js@@ -58,7 +58,9 @@ const audience = createAudienceTracker({     timezone: () => planner.settings().timezone }); const serveAudience = createAudienceRoutes(audience);-const serveMedia = require('./MediaRoutes.cjs').createMediaRoutes();+const mediaRoutes = require('./MediaRoutes.cjs');+const serveMedia = mediaRoutes.createMediaRoutes();+const serveMediaAdmin = mediaRoutes.createMediaAdminRoutes(); const audiencePrune = setInterval(() => {     try { audience.prune(180); } catch (error) { console.error('[Audience] 清理失敗:', error.message); } }, 24 * 3600000);@@ -126,6 +128,8 @@ function pushTestResult(payload) {     const notify = payload.notify && typeof payload.notify === 'object' ? payload.notify : {};     const entry = {         t: Date.now(), testId: payload.testId,+        mode: payload.mode === 'raw' ? 'raw' : 'chat',+        endpoints: Number.isFinite(Number(payload.endpoints)) ? Math.max(0, Number(payload.endpoints)) : null,         status: typeof payload.status === 'string' ? payload.status : 'unknown',         remainingMs: Number.isFinite(payload.remainingMs) ? Math.max(0, payload.remainingMs) : 0,         notify: {@@ -296,6 +300,12 @@ function sendToTikTok(obj) {  } +const serveSocketTest = require('./SocketTestRoutes.cjs').createSocketTestRoutes({+    sendToTikTok,+    pushTestResult,+    hasChild: () => Boolean(tiktokProcess && !tiktokProcess.killed)+});+   // server.js@@ -615,6 +625,8 @@ 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 (serveMediaAdmin(req, res, isValidToken(parseCookies(req).authToken))) return;+    if (serveSocketTest(req, res, isValidToken(parseCookies(req).authToken))) return;     if (serveMedia(req, res)) return;     if (require('./WebAssets.cjs').serveWebAsset(req, res)) return;     if (require('./EmojiRoutes.cjs').serveEmoji(req, res, isValidToken(parseCookies(req).authToken))) return;diff --git a/SocketTestRoutes.cjs b/SocketTestRoutes.cjsnew file mode 100644index 0000000..0a581a5--- /dev/null+++ b/SocketTestRoutes.cjs@@ -0,0 +1,73 @@+const MAX_BODY_BYTES = 2 * 1024 * 1024;++/**+ * Raw Socket 測試入口:需登入,payload 由子程序直接送進 socketHub,+ * 不經過 /chat 的 filter、去重、traffic 或 AutoClip。+ * @param {Object} deps+ * @param {(payload: Object) => void} deps.sendToTikTok+ * @param {(payload: Object) => void} deps.pushTestResult+ * @param {() => boolean} deps.hasChild+ * @returns {Function}+ */+function createSocketTestRoutes({ sendToTikTok, pushTestResult, hasChild }) {+    return function serveSocketTest(req, res, authorized = false) {+        if (req.url.split('?')[0] !== '/api/socket/test') return false;+        const send = (status, body) => {+            res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });+            res.end(JSON.stringify(body));+        };+        if (!authorized) {+            send(401, { ok: false, error: '請先登入' });+            return true;+        }+        if (req.method !== 'POST') {+            send(405, { ok: false, error: '只支援 POST' });+            return true;+        }+        let body = '';+        let tooLarge = false;+        req.on('data', chunk => {+            body += chunk;+            if (Buffer.byteLength(body) > MAX_BODY_BYTES) tooLarge = true;+        });+        req.on('end', () => {+            if (tooLarge) {+                send(413, { ok: false, error: 'payload 過大' });+                return;+            }+            try {+                const data = JSON.parse(body);+                const testId = typeof data.testId === 'string' ? data.testId.trim() : '';+                if (!testId) {+                    send(400, { ok: false, error: '缺少 testId' });+                    return;+                }+                if (!data.payload || typeof data.payload !== 'object' || Array.isArray(data.payload)) {+                    send(400, { ok: false, error: 'payload 必須是物件' });+                    return;+                }+                if (!['StreamMessage', 'AdOverlay'].includes(data.payload.type)) {+                    send(400, { ok: false, error: '只支援 StreamMessage 或 AdOverlay' });+                    return;+                }+                if (!hasChild()) {+                    pushTestResult({ testId, status: 'offline', mode: 'raw', endpoints: 0 });+                    send(409, { ok: false, error: 'TikTok 子程序未運行' });+                    return;+                }+                sendToTikTok({+                    type: 'RAW_SOCKET_TEST',+                    testId,+                    normalize: data.normalize !== false,+                    payload: data.payload+                });+                send(202, { ok: true });+            } catch (error) {+                send(400, { ok: false, error: 'JSON 解析失敗:' + error.message });+            }+        });+        return true;+    };+}++module.exports = { createSocketTestRoutes };diff --git a/Test/media_cache.test.cjs b/Test/media_cache.test.cjsindex f178da1..90b5fce 100644--- a/Test/media_cache.test.cjs+++ b/Test/media_cache.test.cjs@@ -6,7 +6,7 @@ const os = require('node:os'); const path = require('node:path'); const http = require('node:http'); const { MediaCache, hashUrl, normalizeImageUrl } = require('../ScriptLib/media/cache.cjs');-const { createMediaRoutes } = require('../MediaRoutes.cjs');+const { createMediaRoutes, createMediaAdminRoutes } = require('../MediaRoutes.cjs'); const { SocketHub } = require('../ScriptLib/socket/hub.mjs');  const PNG = Buffer.from('89504e470d0a1a0a0000000d49484452', 'hex');@@ -62,6 +62,12 @@ test('MediaCache 把外部 URL 換成本地 hash,下載後可再從快取供     assert.equal(first.headers['Content-Type'], 'image/png');     assert.deepEqual(Buffer.from(first.body), PNG);     assert.equal(fs.existsSync(cache.filePath(hash)), true);+    const list = cache.list({ q: '127.0.0.1', status: 'cached' });+    assert.equal(list.total, 1);+    assert.equal(list.items[0].hash, hash);+    assert.equal(list.items[0].host, '127.0.0.1');+    assert.equal(list.stats.total, 1);+    assert.ok(list.stats.bytes > 0);      const second = await fakeServe(cache, hash);     assert.equal(second.status, 200);@@ -90,6 +96,25 @@ test('MediaCache 拒絕私有位址、SVG 與非 HTTP URL', async t => {     assert.equal(normalizeImageUrl('file:///etc/passwd'), ''); }); +test('MediaAdmin 需要登入並回傳統計與列表', async () => {+    const cache = {+        stats: () => ({ total: 1, bytes: 10, cached: 1, failed: 0, missing: 0 }),+        list: value => ({ items: [{ hash: 'a'.repeat(64) }], total: 1, page: value.page, pages: 1, stats: {} })+    };+    const route = createMediaAdminRoutes(cache);+    const call = (url, authorized) => new Promise(resolve => {+        const req = { url, method: 'GET', headers: {} };+        const res = { writeHead(status) { this.status = status; }, end(body) { resolve({ status: this.status, body: JSON.parse(body) }); } };+        route(req, res, authorized);+    });+    assert.equal((await call('/api/media', false)).status, 401);+    assert.equal((await call('/api/media/stats', true)).body.stats.total, 1);+    const list = await call('/api/media?page=1', true);+    assert.equal(list.status, 200);+    assert.equal(list.body.items.length, 1);+    assert.equal(route({ url: '/other', method: 'GET' }, {}, true), false);+});+ test('MediaRoutes 只接管 /media/<hash> 並呼叫快取', async () => {     const calls = [];     const route = createMediaRoutes({diff --git a/Test/socket_test_routes.test.cjs b/Test/socket_test_routes.test.cjsnew file mode 100644index 0000000..0056a47--- /dev/null+++ b/Test/socket_test_routes.test.cjs@@ -0,0 +1,67 @@+const test = require('node:test');+const assert = require('node:assert/strict');+const { EventEmitter } = require('node:events');+const { createSocketTestRoutes } = require('../SocketTestRoutes.cjs');++function call(route, { url = '/api/socket/test', method = 'POST', body = '', authorized = true } = {}) {+    return new Promise(resolve => {+        const req = new EventEmitter();+        req.url = url;+        req.method = method;+        const res = {+            writeHead(status) { this.status = status; },+            end(value) { resolve({ status: this.status, body: value ? JSON.parse(value) : null }); }+        };+        const served = route(req, res, authorized);+        if (!served) { resolve({ served: false }); return; }+        setImmediate(() => {+            if (body) req.emit('data', Buffer.from(body));+            req.emit('end');+        });+    });+}++const payload = { type: 'StreamMessage', user: 'u', message: 'hello' };++test('SocketTestRoutes 需要登入與 TikTok 子程序', async () => {+    const sent = [];+    const results = [];+    const route = createSocketTestRoutes({+        sendToTikTok: value => sent.push(value),+        pushTestResult: value => results.push(value),+        hasChild: () => false+    });+    assert.equal((await call(route, { authorized: false, body: JSON.stringify({ testId: 't', payload }) })).status, 401);+    const offline = await call(route, { body: JSON.stringify({ testId: 't', payload }) });+    assert.equal(offline.status, 409);+    assert.equal(results[0].status, 'offline');+    assert.equal(results[0].mode, 'raw');+    assert.equal(sent.length, 0);+});++test('SocketTestRoutes 將 raw payload 交給子程序', async () => {+    const sent = [];+    const route = createSocketTestRoutes({+        sendToTikTok: value => sent.push(value),+        pushTestResult: () => {},+        hasChild: () => true+    });+    const response = await call(route, {+        body: JSON.stringify({ testId: 'raw-1', normalize: false, payload })+    });+    assert.equal(response.status, 202);+    assert.equal(sent.length, 1);+    assert.equal(sent[0].type, 'RAW_SOCKET_TEST');+    assert.equal(sent[0].normalize, false);+    assert.equal(sent[0].payload.message, 'hello');+});++test('SocketTestRoutes 拒絕非支援的 payload 與非 POST', async () => {+    const route = createSocketTestRoutes({+        sendToTikTok: () => {}, pushTestResult: () => {}, hasChild: () => true+    });+    assert.equal((await call(route, {+        body: JSON.stringify({ testId: 't', payload: { type: 'Other' } })+    })).status, 400);+    assert.equal((await call(route, { method: 'GET' })).status, 405);+});diff --git a/TikTok.js b/TikTok.jsindex e08d4a6..fd24297 100644--- a/TikTok.js+++ b/TikTok.js@@ -543,6 +543,11 @@ process.stdin.on('data', async (chunk) => {                 return;             } +            if (json.type === 'RAW_SOCKET_TEST') {+                handleRawSocketTest(json);+                return;+            }+             if (json.type === 'StreamMessage') {                 // 先存原始值供去重比對                 const origUser = json.user;@@ -929,15 +934,43 @@ function sendSocketMessage(user, message, { img = '', giftImg = '', isMain = tru     return socketHub.sendMessage(user, message, { img, giftImg, isMain, userNum: CacheUserNum, userList: CacheUserList, category, platform, dedupUser, dedupMessage, dedupVariant, session, isTest }); } +/** Raw Socket 測試:可選擇套用正式 MediaCache/emoji 轉換,直接送 socketHub。 */+function handleRawSocketTest(message) {+    try {+        const payload = { ...(message.payload || {}) };+        if (message.normalize !== false) {+            if (payload.type === 'StreamMessage') {+                payload.message = replaceEmojis(String(payload.message ?? ''));+                payload.img = cachedMediaUrl(payload.img);+                payload.giftImg = cachedMediaUrl(payload.giftImg);+            } else if (payload.type === 'AdOverlay') {+                payload.text = String(payload.text ?? payload.message ?? '')+                    .replace(AD_IMAGE_URL_RE, url => cachedMediaUrl(url));+                payload.iconURL = cachedMediaUrl(payload.iconURL);+                if (!payload.category) payload.category = 'ad';+            }+        }+        payload.isTest = true;+        const endpoints = socketHub.endpoints.length;+        socketHub.send(payload);+        reportTestResult(message.testId, endpoints > 0 ? 'sent' : 'noendpoints', 0,+            { status: 'skipped', channels: [] }, { mode: 'raw', endpoints });+    } catch (error) {+        reportTestResult(message.testId, 'error', 0, { status: 'skipped', channels: [] },+            { mode: 'raw', endpoints: 0, error: error.message });+    }+}+ /** 把測試訊息結果回報給主服務;沒有 testId 或 IPC 時安靜略過。 */-function reportTestResult(testId, status, remainingMs = 0, notify = null) {+function reportTestResult(testId, status, remainingMs = 0, notify = null, extra = null) {     if (!testId || typeof process.send !== 'function') return;     try {         process.send({             type: 'TEST_RESULT',             data: {                 testId, status, remainingMs,-                notify: notify || { status: 'skipped', channels: [] }+                notify: notify || { status: 'skipped', channels: [] },+                ...(extra && typeof extra === 'object' ? extra : {})             }         });     } catch { /* IPC 關閉 */ }diff --git a/assets/app.css b/assets/app.cssindex 3a7ec0d..ed568a4 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-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\.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 filediff --git a/assets/app.css.build.json b/assets/app.css.build.jsonindex c1e7f2c..b9a7769 100644--- a/assets/app.css.build.json+++ b/assets/app.css.build.json@@ -1,5 +1,5 @@ {   "schema": 2,-  "inputHash": "45c33eab6bf71988e6e8b5a768ef2f1e53337b263598aa9ef0cb246a4a2b6b9d",-  "outputHash": "3905b4bbe6018f16e969ba0ea683c6fa6d89a1c1e556d0e258f0e71d7c3f62b3"+  "inputHash": "c92bdb3e4df68e37b55a1a5dfc2bc438413a9c1b6d91a1ffc53fe2ee318dbcc7",+  "outputHash": "df3dae65108a7226d403072ac930cd57de0eadda75b3e839ef5e31ed20d6b3d5" }diff --git a/assets/monitor.css b/assets/monitor.cssindex f139568..ced5eb0 100644--- a/assets/monitor.css+++ b/assets/monitor.css@@ -283,3 +283,94 @@ body.app-ui {     color: #94a3b8;     font-size: 0.8rem; }++/* MediaCache 檢視 */+.mon-media-toolbar {+    display: flex;+    align-items: center;+    flex-wrap: wrap;+    gap: 0.4rem;+    margin-bottom: 0.5rem;+    font-size: 0.75rem;+}+.mon-media-toolbar input,+.mon-media-toolbar select {+    padding: 0.3rem 0.5rem;+    border: 1px solid #475569;+    border-radius: 0.375rem;+    background: #0f172a;+    color: #e5e7eb;+    font: inherit;+}+.mon-media-toolbar input {+    min-width: 14rem;+    flex: 1;+}+.mon-media-toolbar button,+.mon-media-pager button {+    padding: 0.3rem 0.6rem;+    border: 1px solid #475569;+    border-radius: 0.375rem;+    background: #1e293b;+    color: #e5e7eb;+    cursor: pointer;+}+.mon-media-toolbar button:disabled,+.mon-media-pager button:disabled {+    opacity: 0.45;+    cursor: default;+}+.mon-media-stats,+.mon-media-pager {+    color: #94a3b8;+    font-size: 0.75rem;+}+.mon-media-table-wrap {+    max-height: 18rem;+    overflow: auto;+    border: 1px solid #334155;+    border-radius: 0.5rem;+}+.mon-media-table {+    width: 100%;+    border-collapse: collapse;+    font-size: 0.75rem;+}+.mon-media-table th,+.mon-media-table td {+    padding: 0.35rem 0.5rem;+    border-bottom: 1px solid #1e293b;+    text-align: left;+    vertical-align: middle;+    white-space: nowrap;+}+.mon-media-table th {+    position: sticky;+    top: 0;+    background: #0f172a;+    color: #cbd5e1;+}+.mon-media-table img {+    width: 32px;+    height: 32px;+    object-fit: cover;+    border-radius: 4px;+    background: #111827;+}+.mon-media-table code {+    color: #93c5fd;+}+.mon-media-pager {+    display: flex;+    align-items: center;+    flex-wrap: wrap;+    gap: 0.4rem;+    margin-top: 0.5rem;+}+.mon-raw-normalize {+    display: flex;+    align-items: center;+    gap: 0.4rem;+    color: #cbd5e1;+    font-size: 0.75rem;+}diff --git a/assets/monitor.js b/assets/monitor.jsindex e2064b9..7b7f28b 100644--- a/assets/monitor.js+++ b/assets/monitor.js@@ -68,6 +68,26 @@     const testSend = document.getElementById('monTestSend');     const testClear = document.getElementById('monTestClear');     const testStatus = document.getElementById('monTestStatus');+    const mediaPanel = document.getElementById('monMediaPanel');+    const mediaSearch = document.getElementById('monMediaSearch');+    const mediaType = document.getElementById('monMediaType');+    const mediaStatusFilter = document.getElementById('monMediaStatus');+    const mediaRefresh = document.getElementById('monMediaRefresh');+    const mediaStats = document.getElementById('monMediaStats');+    const mediaRows = document.getElementById('monMediaRows');+    const mediaPage = document.getElementById('monMediaPage');+    const mediaPrev = document.getElementById('monMediaPrev');+    const mediaNext = document.getElementById('monMediaNext');+    const mediaStatusText = document.getElementById('monMediaStatusText');+    const rawType = document.getElementById('monRawType');+    const rawUser = document.getElementById('monRawUser');+    const rawCategory = document.getElementById('monRawCategory');+    const rawMessage = document.getElementById('monRawMessage');+    const rawImage = document.getElementById('monRawImage');+    const rawGift = document.getElementById('monRawGift');+    const rawNormalize = document.getElementById('monRawNormalize');+    const rawSend = document.getElementById('monRawSend');+    const rawStatus = document.getElementById('monRawStatus');     const TEST_PREFS_KEY = 'ttw-monitor-test-prefs';      let emojiMap = null;@@ -88,6 +108,7 @@         session: null,         mode: 'live',         history: { sessions: [], sessionId: '', page: 1, pages: 1, total: 0, messages: [], loading: false },+        media: { page: 1, pages: 1, total: 0, items: [], q: '', type: '', status: '', loading: false, loaded: false },         statusKey: 'monitor.reconnecting'     };     let lastUserScrollIntent = 0;@@ -435,6 +456,7 @@         pendingTestTimer = null;         pendingTestId = null;         testSend.disabled = false;+        rawSend.disabled = false;     }      function clearTestCooldown() {@@ -480,6 +502,20 @@     function handleTestResult(result) {         if (!result || result.testId !== pendingTestId) return;         clearTestPending();+        if (result.mode === 'raw') {+            const endpoints = Number(result.endpoints) || 0;+            if (result.status === 'sent') {+                setRawStatus(t('monitor.raw.sent', { endpoints }, `已送出(${endpoints} 個端點)。`));+            } else if (result.status === 'noendpoints') {+                setRawStatus(t('monitor.raw.noendpoints', {}, '沒有啟用中的 Socket 端點。'), true);+            } else if (result.status === 'offline') {+                setRawStatus(t('monitor.raw.offline', {}, 'TikTok 子程序未運行。'), true);+            } else {+                setRawStatus(t('monitor.raw.error', { error: result.status || 'unknown' },+                    'Raw Socket 失敗:' + (result.status || 'unknown')), true);+            }+            return;+        }         const notifyText = describeNotify(result.notify);         const suffix = notifyText ? ` · ${t('monitor.test.notifyLabel', {}, '通知')}:${notifyText}` : '';         switch (result.status) {@@ -504,6 +540,184 @@         }     } +    function setRawStatus(text, error = false) {+        rawStatus.textContent = text;+        rawStatus.className = 'text-xs ' + (error ? 'text-red-400' : 'text-green-400');+    }++    function rawPayload() {+        const type = rawType.value === 'AdOverlay' ? 'AdOverlay' : 'StreamMessage';+        const user = rawUser.value.trim() || t('monitor.test.userDefault', {}, '測試使用者');+        const message = rawMessage.value;+        if (type === 'AdOverlay') {+            return {+                type, category: rawCategory.value.trim() || 'ad', user, text: message,+                iconURL: rawGift.value.trim() || rawImage.value.trim(), useTTS: false+            };+        }+        return {+            type, category: rawCategory.value.trim() || 'chat', platform: 'unknown', user,+            message, img: rawImage.value.trim(), giftImg: rawGift.value.trim()+        };+    }++    async function sendRawSocketTest() {+        const testId = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`;+        pendingTestId = testId;+        rawSend.disabled = true;+        setRawStatus(t('monitor.raw.waiting', {}, '等待 Raw Socket 結果…'));+        pendingTestTimer = setTimeout(() => {+            if (!pendingTestId) return;+            clearTestPending();+            setRawStatus(t('monitor.test.resultTimeout', {}, '未收到處理結果;子程序或結果 SSE 可能中斷。'), true);+        }, 5000);+        try {+            const response = await fetch('/api/socket/test', {+                method: 'POST',+                headers: { 'Content-Type': 'application/json' },+                body: JSON.stringify({+                    testId,+                    normalize: rawNormalize.checked,+                    payload: rawPayload()+                })+            });+            if (response.status === 401) {+                clearTestPending();+                setRawStatus(t('monitor.raw.login', {}, 'Raw Socket 測試需要主服務登入。'), true);+                return;+            }+            if (!response.ok) {+                const data = await response.json().catch(() => ({}));+                const offline = response.status === 409;+                clearTestPending();+                setRawStatus(offline+                    ? t('monitor.raw.offline', {}, 'TikTok 子程序未運行。')+                    : t('monitor.raw.error', { error: data.error || ('HTTP ' + response.status) },+                        'Raw Socket 失敗:' + (data.error || ('HTTP ' + response.status))), true);+            }+        } catch (error) {+            if (pendingTestId === testId) {+                clearTestPending();+                setRawStatus(t('monitor.raw.error', { error: error.message },+                    'Raw Socket 失敗:' + error.message), true);+            }+        }+    }++    function formatBytes(bytes) {+        const value = Number(bytes) || 0;+        if (value < 1024) return value + ' B';+        if (value < 1024 * 1024) return (value / 1024).toFixed(1) + ' KiB';+        return (value / 1024 / 1024).toFixed(1) + ' MiB';+    }++    function setMediaStatus(text, error = false) {+        mediaStatusText.textContent = text;+        mediaStatusText.className = error ? 'text-xs text-red-400' : 'text-xs text-gray-400';+    }++    async function copyMediaLink(link) {+        try {+            await navigator.clipboard.writeText(link);+            setMediaStatus(t('monitor.media.copied', {}, '已複製本地連結。'));+        } catch {+            setMediaStatus(t('monitor.media.copyFailed', {}, '複製失敗。'), true);+        }+    }++    function mediaStatusLabel(status) {+        if (status === 'cached') return t('monitor.media.cached', {}, '已快取');+        if (status === 'failed') return t('monitor.media.failed', {}, '失敗過');+        return t('monitor.media.missing', {}, '缺少檔案');+    }++    function renderMedia() {+        const media = state.media;+        mediaRows.replaceChildren();+        if (!media.items.length) {+            const tr = document.createElement('tr');+            const td = document.createElement('td');+            td.colSpan = 8;+            td.className = 'text-gray-400';+            td.textContent = t('monitor.media.empty', {}, '沒有符合的媒體。');+            tr.append(td);+            mediaRows.append(tr);+        }+        for (const item of media.items) {+            const tr = document.createElement('tr');+            const preview = document.createElement('td');+            const image = document.createElement('img');+            image.src = item.localUrl;+            image.loading = 'lazy';+            image.alt = '';+            image.addEventListener('error', () => { image.hidden = true; });+            preview.append(image);+            const hash = document.createElement('td');+            const code = document.createElement('code');+            code.textContent = item.hash.slice(0, 12) + '…';+            code.title = item.hash;+            hash.append(code);+            const type = document.createElement('td');+            type.textContent = item.contentType || '—';+            const size = document.createElement('td');+            size.textContent = formatBytes(item.byteSize);+            const host = document.createElement('td');+            host.textContent = item.host || '—';+            const accessed = document.createElement('td');+            accessed.textContent = item.lastAccess ? new Date(item.lastAccess).toLocaleString(window.TTWI18n.language) : '—';+            const status = document.createElement('td');+            status.textContent = mediaStatusLabel(item.status);+            const actions = document.createElement('td');+            const copy = document.createElement('button');+            copy.type = 'button';+            copy.textContent = t('monitor.media.copy', {}, '複製連結');+            copy.addEventListener('click', () => copyMediaLink(item.localUrl));+            const open = document.createElement('a');+            open.href = item.localUrl;+            open.target = '_blank';+            open.rel = 'noopener';+            open.textContent = t('monitor.media.open', {}, '開啟');+            actions.append(copy, ' ', open);+            tr.append(preview, hash, type, size, host, accessed, status, actions);+            mediaRows.append(tr);+        }+        const stats = media.stats || {};+        mediaStats.textContent = `${stats.total || 0} · ${formatBytes(stats.bytes)} · cached ${stats.cached || 0} · failed ${stats.failed || 0} · missing ${stats.missing || 0}`;+        mediaPage.textContent = t('monitor.history.page', { page: media.page, pages: media.pages },+            `第 ${media.page} / ${media.pages} 頁`);+        mediaPrev.disabled = media.page <= 1;+        mediaNext.disabled = media.page >= media.pages;+        setMediaStatus(`${t('monitor.media.title', {}, 'MediaCache 資源')} · ${media.total}`);+    }++    async function loadMedia() {+        if (state.media.loading) return;+        state.media.loading = true;+        try {+            const params = new URLSearchParams({+                page: String(state.media.page), pageSize: '50',+                q: state.media.q, type: state.media.type, status: state.media.status+            });+            const response = await fetch('/api/media?' + params, { cache: 'no-store' });+            if (response.status === 401) {+                mediaRows.replaceChildren();+                setMediaStatus(t('monitor.media.login', {}, 'MediaCache 需要主服務登入。'), true);+                return;+            }+            const data = await response.json();+            if (!response.ok || !data.ok) throw new Error(data.error || ('HTTP ' + response.status));+            state.media = {+                ...state.media, page: data.page, pages: data.pages, total: data.total,+                items: data.items || [], stats: data.stats || {}, loading: false, loaded: true+            };+            renderMedia();+        } catch (error) {+            setMediaStatus(t('monitor.media.error', { error: error.message }, '讀取失敗:' + error.message), true);+        } finally {+            state.media.loading = false;+        }+    }+     async function sendSocketTest() {         const message = testMessage.value;         if (!message.trim()) {@@ -809,6 +1023,39 @@     historyNextEl.addEventListener('click', () => {         if (state.history.page < state.history.pages) loadHistoryMessages(state.history.page + 1);     });+    mediaPanel.addEventListener('toggle', () => {+        if (mediaPanel.open && !state.media.loaded) loadMedia();+    });+    mediaSearch.addEventListener('input', () => {+        clearTimeout(state.media.timer);+        state.media.timer = setTimeout(() => {+            state.media.q = mediaSearch.value.trim().toLowerCase();+            state.media.page = 1;+            loadMedia();+        }, 250);+    });+    mediaType.addEventListener('change', () => {+        state.media.type = mediaType.value;+        state.media.page = 1;+        loadMedia();+    });+    mediaStatusFilter.addEventListener('change', () => {+        state.media.status = mediaStatusFilter.value;+        state.media.page = 1;+        loadMedia();+    });+    mediaRefresh.addEventListener('click', () => loadMedia());+    mediaPrev.addEventListener('click', () => {+        if (state.media.page > 1) { state.media.page--; loadMedia(); }+    });+    mediaNext.addEventListener('click', () => {+        if (state.media.page < state.media.pages) { state.media.page++; loadMedia(); }+    });+    rawType.addEventListener('change', () => {+        rawCategory.value = rawType.value === 'AdOverlay' ? 'ad' : 'chat';+    });+    rawSend.addEventListener('click', sendRawSocketTest);+     testAdd.addEventListener('click', addTestMessage);     testSend.addEventListener('click', sendSocketTest);     testClear.addEventListener('click', clearTestPrefs);@@ -832,6 +1079,7 @@         buildTestCategories();         buildAvatarOptions();         updateAvatarPreview();+        if (state.media.loaded) renderMedia();         if (state.mode === 'history') {             loadHistorySessions();         } else {diff --git a/lang/en.json b/lang/en.jsonindex 0954fbc..cda045c 100644--- a/lang/en.json+++ b/lang/en.json@@ -236,6 +236,42 @@   "monitor.title": "Live message monitor",   "monitor.test.title": "Test message (local preview)",   "monitor.test.note": "Preview is local only; Send test uses the selected targets via /chat and requires the TikTok subprocess. Socket/overlay and Bark/ntfy are independent; selecting both does not send Socket twice.",+  "monitor.media.title": "MediaCache resources",+  "monitor.media.search": "Search hash or source host…",+  "monitor.media.allTypes": "All types",+  "monitor.media.allStatus": "All statuses",+  "monitor.media.cached": "Cached",+  "monitor.media.failed": "Failed before",+  "monitor.media.missing": "Missing file",+  "monitor.media.refresh": "Refresh",+  "monitor.media.preview": "Preview",+  "monitor.media.type": "Type",+  "monitor.media.size": "Size",+  "monitor.media.host": "Source",+  "monitor.media.lastAccess": "Last access",+  "monitor.media.status": "Status",+  "monitor.media.actions": "Actions",+  "monitor.media.note": "Only HTTP(S) downloads appear here; data URIs are inline and do not enter the cache.",+  "monitor.media.login": "MediaCache requires signing in to the main service.",+  "monitor.media.error": "Load failed: {error}",+  "monitor.media.empty": "No matching media.",+  "monitor.media.copy": "Copy link",+  "monitor.media.open": "Open",+  "monitor.media.copied": "Local link copied.",+  "monitor.media.copyFailed": "Copy failed.",+  "monitor.raw.title": "Raw Socket test (bypass /chat)",+  "monitor.raw.type": "Payload",+  "monitor.raw.category": "Category",+  "monitor.raw.giftIcon": "Gift / Icon URL",+  "monitor.raw.normalize": "Apply production MediaCache/emoji normalization",+  "monitor.raw.send": "Send Raw Socket",+  "monitor.raw.note": "Sent directly through socketHub, bypassing /chat, filters, dedup, traffic and AutoClip. Sign-in required.",+  "monitor.raw.waiting": "Waiting for Raw Socket result…",+  "monitor.raw.sent": "Sent ({endpoints} endpoints).",+  "monitor.raw.noendpoints": "No enabled Socket endpoints.",+  "monitor.raw.offline": "TikTok subprocess is not running.",+  "monitor.raw.error": "Raw Socket failed: {error}",+  "monitor.raw.login": "Raw Socket test requires signing in to the main service.",   "monitor.test.user": "User",   "monitor.test.avatar": "Avatar URL",   "monitor.test.avatarPick": "Pick avatar from emoji",diff --git a/lang/zh-TW.json b/lang/zh-TW.jsonindex 3c887bb..01a62bc 100644--- a/lang/zh-TW.json+++ b/lang/zh-TW.json@@ -236,6 +236,42 @@   "monitor.title": "直播訊息監控",   "monitor.test.title": "測試訊息(本機預覽)",   "monitor.test.note": "加入預覽只影響本機;送出測試會依勾選目標經 /chat,需 TikTok 子程序。Socket/overlay 與 Bark/ntfy 是獨立輸出;同時勾選不會重複送 Socket。",+  "monitor.media.title": "MediaCache 資源",+  "monitor.media.search": "搜尋 hash 或來源 host…",+  "monitor.media.allTypes": "全部類型",+  "monitor.media.allStatus": "全部狀態",+  "monitor.media.cached": "已快取",+  "monitor.media.failed": "失敗過",+  "monitor.media.missing": "缺少檔案",+  "monitor.media.refresh": "重新整理",+  "monitor.media.preview": "預覽",+  "monitor.media.type": "類型",+  "monitor.media.size": "大小",+  "monitor.media.host": "來源",+  "monitor.media.lastAccess": "最後存取",+  "monitor.media.status": "狀態",+  "monitor.media.actions": "操作",+  "monitor.media.note": "只列出 HTTP(S) 下載快取;data URI 屬於 inline 直傳,不會出現在這裡。",+  "monitor.media.login": "MediaCache 需要主服務登入。",+  "monitor.media.error": "讀取失敗:{error}",+  "monitor.media.empty": "沒有符合的媒體。",+  "monitor.media.copy": "複製連結",+  "monitor.media.open": "開啟",+  "monitor.media.copied": "已複製本地連結。",+  "monitor.media.copyFailed": "複製失敗。",+  "monitor.raw.title": "Raw Socket 測試(繞過 /chat)",+  "monitor.raw.type": "Payload",+  "monitor.raw.category": "分類",+  "monitor.raw.giftIcon": "禮物/Icon URL",+  "monitor.raw.normalize": "套用 MediaCache/emoji 正式轉換",+  "monitor.raw.send": "送出 Raw Socket",+  "monitor.raw.note": "直接送進 socketHub,不經 /chat、filter、去重、traffic、AutoClip;仍需登入。",+  "monitor.raw.waiting": "等待 Raw Socket 結果…",+  "monitor.raw.sent": "已送出({endpoints} 個端點)。",+  "monitor.raw.noendpoints": "沒有啟用中的 Socket 端點。",+  "monitor.raw.offline": "TikTok 子程序未運行。",+  "monitor.raw.error": "Raw Socket 失敗:{error}",+  "monitor.raw.login": "Raw Socket 測試需要主服務登入。",   "monitor.test.user": "使用者",   "monitor.test.avatar": "頭貼 URL",   "monitor.test.avatarPick": "從 emoji 選頭貼",diff --git a/monitor.html b/monitor.htmlindex d1f8ad1..6d5f6cc 100644--- a/monitor.html+++ b/monitor.html@@ -86,6 +86,67 @@       <small data-i18n="monitor.test.note">加入預覽只影響本機;送出測試會依勾選目標經 /chat,需 TikTok 子程序。Socket/overlay 與 Bark/ntfy 是獨立輸出;同時勾選不會重複送 Socket。</small>     </details> +    <details id="monMediaPanel" class="mon-test-panel">+      <summary data-i18n="monitor.media.title">MediaCache 資源</summary>+      <div class="mon-media-toolbar">+        <input id="monMediaSearch" type="search" maxlength="200" data-i18n-placeholder="monitor.media.search" placeholder="搜尋 hash 或來源 host…">+        <select id="monMediaType">+          <option value="" data-i18n="monitor.media.allTypes">全部類型</option>+          <option value="image/png">PNG</option><option value="image/jpeg">JPEG</option>+          <option value="image/webp">WebP</option><option value="image/gif">GIF</option>+        </select>+        <select id="monMediaStatus">+          <option value="" data-i18n="monitor.media.allStatus">全部狀態</option>+          <option value="cached" data-i18n="monitor.media.cached">已快取</option>+          <option value="failed" data-i18n="monitor.media.failed">失敗過</option>+          <option value="missing" data-i18n="monitor.media.missing">缺少檔案</option>+        </select>+        <button id="monMediaRefresh" type="button" data-i18n="monitor.media.refresh">重新整理</button>+        <span id="monMediaStats" class="mon-media-stats"></span>+      </div>+      <div class="mon-media-table-wrap">+        <table class="mon-media-table">+          <thead><tr>+            <th data-i18n="monitor.media.preview">預覽</th><th>Hash</th><th data-i18n="monitor.media.type">類型</th>+            <th data-i18n="monitor.media.size">大小</th><th data-i18n="monitor.media.host">來源</th>+            <th data-i18n="monitor.media.lastAccess">最後存取</th><th data-i18n="monitor.media.status">狀態</th>+            <th data-i18n="monitor.media.actions">操作</th>+          </tr></thead>+          <tbody id="monMediaRows"></tbody>+        </table>+      </div>+      <div class="mon-media-pager">+        <button id="monMediaPrev" type="button" data-i18n="monitor.history.prev">上一頁</button>+        <span id="monMediaPage"></span>+        <button id="monMediaNext" type="button" data-i18n="monitor.history.next">下一頁</button>+        <span id="monMediaStatusText" role="status"></span>+      </div>+      <small data-i18n="monitor.media.note">只列出 HTTP(S) 下載快取;data URI 屬於 inline 直傳,不會出現在這裡。</small>+    </details>++    <details id="monRawPanel" class="mon-test-panel">+      <summary data-i18n="monitor.raw.title">Raw Socket 測試(繞過 /chat)</summary>+      <div class="mon-test-grid">+        <label><span data-i18n="monitor.raw.type">Payload</span>+          <select id="monRawType">+            <option value="StreamMessage">StreamMessage</option>+            <option value="AdOverlay">AdOverlay</option>+          </select>+        </label>+        <label><span data-i18n="monitor.test.user">使用者</span><input id="monRawUser" type="text" maxlength="80" autocomplete="off"></label>+        <label><span data-i18n="monitor.raw.category">分類</span><input id="monRawCategory" type="text" maxlength="40" value="chat" autocomplete="off"></label>+        <label class="mon-test-message"><span data-i18n="monitor.test.message">訊息</span><textarea id="monRawMessage" rows="2" maxlength="786432"></textarea></label>+        <label><span data-i18n="monitor.test.avatar">頭像/圖片 URL</span><input id="monRawImage" type="text" maxlength="262144" autocomplete="off"></label>+        <label><span data-i18n="monitor.raw.giftIcon">禮物/Icon URL</span><input id="monRawGift" type="text" maxlength="262144" autocomplete="off"></label>+        <label class="mon-raw-normalize"><input id="monRawNormalize" type="checkbox" checked> <span data-i18n="monitor.raw.normalize">套用 MediaCache/emoji 正式轉換</span></label>+        <div class="mon-test-actions">+          <button id="monRawSend" type="button" class="mon-test-send" data-i18n="monitor.raw.send">送出 Raw Socket</button>+          <span id="monRawStatus" role="status"></span>+        </div>+      </div>+      <small data-i18n="monitor.raw.note">直接送進 socketHub,不經 /chat、filter、去重、traffic、AutoClip;仍需登入。</small>+    </details>+     <!-- 訊息列:時間 / 平台 / 分類 / 使用者 / 內容 -->     <div id="monScroll" class="bg-black rounded-lg flex-1 min-h-0 overflow-auto w-full">       <div id="monRows" class="text-sm min-h-full" role="log" aria-live="polite"></div>diff --git a/todo.md b/todo.mdindex 1935f78..8384c2b 100644--- a/todo.md+++ b/todo.md@@ -24,6 +24,7 @@ - [x] Signer TikTok 導航節流與 WAF 403 冷卻,避免 fetch page 重建風暴。 - [x] 圖片媒體快取 Phase 1:本地 `/media/<hash>`、SocketHub 頭像/禮物圖與 AdOverlay 圖片改寫。 - [x] Python overlay 支援 img/giftImg/AdOverlay icon 與訊息內嵌 data URI。+- [x] monitor MediaCache 檢視/本地連結複製與 Raw Socket 測試入口。 - [ ] 正式環境以實際 child 程序驗證 IPC 結果與倒數(目前 monitor UI 使用假 SSE/fake `/chat`)。  ## 表情映射後續(2026-11-06)