codex-copilot-dx 0.8.6 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ One in-process adapter listens on `127.0.0.1:2026` by default and exposes:
12
12
  - `POST /v1/responses/compact`
13
13
  - `GET /v1/models`
14
14
 
15
- Responses-only models and compaction use Copilot's Responses transport directly. For GPT models that advertise only Chat Completions, CCDX converts the supported text, image, and function-tool subset and rejects incompatible request shapes explicitly. Both streaming SSE and non-streaming responses are supported.
15
+ Responses-only models and compaction use Copilot's Responses transport directly. For requests routed through Chat Completions, CCDX converts the supported text, image, and function-tool subset and rejects incompatible message content rather than turning it into JSON text. Chat `length` and `content_filter` finish reasons become incomplete Responses in both streaming and non-streaming modes. Both streaming SSE and non-streaming responses are supported.
16
16
 
17
17
  The adapter handles response history, encrypted reasoning state, function/custom tools, images, compaction, cancellation, retries, and usage metadata within the compatibility boundaries below. Routing prefers endpoint metadata from Copilot's live or cached catalog; when metadata is unavailable, a small built-in set identifies known Responses-only GPT models.
18
18
 
@@ -213,6 +213,12 @@ If a configured npm mirror has not synchronized the current release yet, use the
213
213
 
214
214
  ## Diagnostics
215
215
 
216
+ ### Local dashboard
217
+
218
+ After starting `ccdx`, open [http://127.0.0.1:2026/](http://127.0.0.1:2026/) in a browser. If `ADAPTER_PORT` is customized, use that port instead. The dashboard shares the existing adapter listener; CCDX does not open a browser automatically or start another service. The page and its assets require both a loopback connection and a loopback Host header, even when LAN binding is enabled.
219
+
220
+ The dashboard loads local status, a live GitHub Copilot model directory, and a summary of local usage metadata once when opened. Each section has an explicit Refresh control; nothing polls in the background. The live model table uses the same fresh account validation and selectable GPT filtering as `ccdx models`, never labels cached fallback data as online, and makes no inference request. A catalog listing does not guarantee inference access. The Usage table combines the current and rotated local usage logs, shows totals and up to 100 model rows, and contains no prompt, response body, or credential fields. The animation selector previews the existing themes and saves through the same settings writer as `ccdx animation`; changes take effect on the next adapter start, and `CCDX_TERMINAL_ANIMATION=off` still disables terminal motion. Dashboard data requests require a custom header that cross-site simple requests cannot send; animation writes also require a matching same-origin `Origin`. The dashboard does not initialize the optional image handler. The overview's known-model count is the adapter's current in-memory catalog, not the live table. A missing image-generation status does not prove the feature is disabled: use `ccdx image-status` to check setup. Recent upstream error prose may contain sensitive details, so review the page before sharing screenshots.
221
+
216
222
  Inspect only the local Codex configuration:
217
223
 
218
224
  ```bash
@@ -259,7 +265,7 @@ For the complete machine-readable status payload:
259
265
  curl -s http://127.0.0.1:2026/_ccdx/status | jq
260
266
  ```
261
267
 
262
- The status endpoint is restricted to the socket's real loopback address even when LAN binding is explicitly enabled. It reports fixed-size request counters, bounded TTFT/TPOT histograms, admission pressure, memory use, response-history size, image queue/cache and adaptive-history state, model-cache counts, token expiry state, and the ten most recent upstream `response.failed` diagnostics retained by the running process. Failure diagnostics include the event type, model, error code/message, response/request IDs when supplied upstream, and retry outcome. Common credential forms, long opaque encrypted values, and explicitly labelled input/prompt/tool-content echoes are redacted. They are cleared when the adapter restarts. No request/response bodies, credential fields, or account profiles are copied into status. Arbitrary upstream prose cannot be guaranteed free of sensitive content; review diagnostics before sharing them.
268
+ The status endpoint is restricted to the socket's real loopback address even when LAN binding is explicitly enabled. It reports fixed-size HTTP request counters separately from bounded `completed` / `incomplete` / `failed` / `cancelled` response outcomes by model and origin, plus bounded incomplete-reason counts, TTFT/TPOT histograms, admission pressure, memory use, response-history size/tree concentration/lookup misses, image queue/cache and adaptive-history state, model-cache counts, token expiry state, and the ten most recent upstream `response.failed` diagnostics retained by the running process. HTTP errors without a model terminal remain "without terminal" and retain their known origin; they are not counted as model failures. History eviction markers are not failed-request counts; `ccdx status` warns near the cache limit without deleting or compressing history. Failure diagnostics include the event type, model, error code/message, response/request IDs when supplied upstream, and retry outcome. Common credential forms, long opaque encrypted values, and explicitly labelled input/prompt/tool-content echoes are redacted. They are cleared when the adapter restarts. No request/response bodies, credential fields, or account profiles are copied into status. Arbitrary upstream prose cannot be guaranteed free of sensitive content; review diagnostics before sharing them.
263
269
 
264
270
  `image_mcp` has its own HTTP route counter. After the image handler has been used, `image_generation` separately reports active, succeeded, failed, busy, cancelled, and chat-file delivery-failure counts, including tool errors returned with HTTP 200. A delivery failure can accompany a successful generation; it never causes another generation. Before handler initialization this field is `null`, meaning not observed, not necessarily disabled. Status never initializes that handler, reads its provider credential, installs guidance, or probes the provider. Input-image optimization metrics remain separate. `logging` reports pending records/bytes, queue caps, overflow drops, and write failures for usage and enabled debug-file logging.
265
271
 
@@ -367,7 +373,7 @@ Set `CCDX_LOG_PATH=1` to mirror terminal logs to `~/.local/share/codex-copilot-d
367
373
 
368
374
  Long computer-use sessions can accumulate screenshots that are sent again on later turns. This can increase local preparation time, upstream handshake latency, and the chance of a `413 Payload Too Large` response.
369
375
 
370
- The response-history cache is process-local and byte/entry bounded. Restarting the adapter discards it; a `previous_response_id` stored only in that process becomes unavailable. CCDX does not persist conversation history to disk. Clients can still submit explicit history in the request body, subject to the same limits.
376
+ The response-history cache is process-local and byte/entry bounded. Restarting the adapter discards it; a `previous_response_id` stored only in that process becomes unavailable. CCDX does not persist conversation history to disk. Near-limit status diagnostics report the largest tree and actual lookup misses, but do not recover an already evicted ID or change the cache budget. Clients can still submit explicit history in the request body, subject to the same limits.
371
377
 
372
378
  CCDX attempts to downsample inline base64 images to model-appropriate pixel bounds, with a 2048 px long-edge fallback, and initially encodes them as WebP quality 82. It never replaces an image with a larger encoding and uses one global concurrency limit across direct images and function/custom-tool outputs. GIFs and non-inline references are not re-encoded. An encoding/decode failure keeps the original image; the final request-byte limit still applies.
373
379
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-copilot-dx",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "Use Codex App with GitHub Copilot",
5
5
  "author": "Dale Xiao",
6
6
  "type": "module",
package/src/adapter.mjs CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  classifyAdapterRoute,
8
8
  createRequestMetrics,
9
9
  isLoopbackAddress,
10
+ isLoopbackHostHeader,
10
11
  runtimeStatusPayload,
11
12
  } from "./observability.mjs";
12
13
  import { createRequestId, runWithRequestContext } from "./request-context.mjs";
@@ -63,6 +64,8 @@ export {
63
64
  } from "./responses-request.mjs";
64
65
 
65
66
  const ADAPTER_RUNTIME_CONFIG = loadRuntimeConfig();
67
+ const DASHBOARD_PATHS = new Set(["/", "/ui.css", "/ui.js"]);
68
+ const DASHBOARD_API_PATHS = new Set(["/_ccdx/ui/animation", "/_ccdx/ui/models/live", "/_ccdx/ui/usage"]);
66
69
 
67
70
  export function requestPath(reqUrl) {
68
71
  return new URL(reqUrl || "/", "http://localhost").pathname;
@@ -200,6 +203,14 @@ export function createAdapterHandler(options = {}) {
200
203
  upstreamTimeoutMs,
201
204
  });
202
205
  const dispatch = async (req, res, pathname) => {
206
+ if (DASHBOARD_API_PATHS.has(pathname)) {
207
+ const { handleDashboardApi } = await import("./dashboard-api.mjs");
208
+ return handleDashboardApi(req, res, pathname, options.dashboardOptions);
209
+ }
210
+ if ((req.method === "GET" || req.method === "HEAD") && DASHBOARD_PATHS.has(pathname)) {
211
+ const { serveDashboard } = await import("./dashboard.mjs");
212
+ return serveDashboard(req, res, pathname);
213
+ }
203
214
  if (pathname === "/mcp/image") {
204
215
  if (imageMcpHandler) return imageMcpHandler(req, res);
205
216
  return import("./image-mcp.mjs").then(({ createImageMcpHandler }) => {
@@ -208,7 +219,7 @@ export function createAdapterHandler(options = {}) {
208
219
  });
209
220
  }
210
221
  if (req.method === "GET" && pathname === ADAPTER_STATUS_PATH) {
211
- if (!isLoopbackAddress(req.socket?.remoteAddress)) {
222
+ if (!isLoopbackAddress(req.socket?.remoteAddress) || !isLoopbackHostHeader(req.headers?.host)) {
212
223
  res.writeHead(403, { "Content-Type": "application/json" });
213
224
  res.end(JSON.stringify({ error: "Runtime status is available only from loopback" }));
214
225
  return;
@@ -235,7 +246,7 @@ export function createAdapterHandler(options = {}) {
235
246
  }
236
247
 
237
248
  if (pathname === ADAPTER_CACHE_PATH) {
238
- if (!isLoopbackAddress(req.socket?.remoteAddress)) {
249
+ if (!isLoopbackAddress(req.socket?.remoteAddress) || !isLoopbackHostHeader(req.headers?.host)) {
239
250
  res.writeHead(403, { "Content-Type": "application/json" });
240
251
  res.end(JSON.stringify({ error: "Cache control is available only from loopback" }));
241
252
  return;
@@ -373,7 +384,9 @@ export function createAdapterHandler(options = {}) {
373
384
 
374
385
  const trackRequest = pathname !== ADAPTER_HEALTH_PATH
375
386
  && pathname !== ADAPTER_STATUS_PATH
376
- && pathname !== ADAPTER_CACHE_PATH;
387
+ && pathname !== ADAPTER_CACHE_PATH
388
+ && !DASHBOARD_API_PATHS.has(pathname)
389
+ && !((req.method === "GET" || req.method === "HEAD") && DASHBOARD_PATHS.has(pathname));
377
390
  const routeName = classifyAdapterRoute(req.method, pathname);
378
391
  const complete = trackRequest ? requestMetrics.begin(routeName) : () => {};
379
392
  const streamPerformance = trackRequest ? streamPerformanceMetrics.begin(routeName) : null;
@@ -389,7 +402,7 @@ export function createAdapterHandler(options = {}) {
389
402
  if (requestFinished) return;
390
403
  requestFinished = true;
391
404
  try { complete({ statusCode, aborted }); } catch {}
392
- try { streamPerformance?.finish({ failed: aborted || statusCode >= 400 }); } catch {}
405
+ try { streamPerformance?.finish({ failed: aborted || statusCode >= 400, aborted }); } catch {}
393
406
  try { finishTerminalActivity(); } catch {}
394
407
  };
395
408
  const containUnexpectedError = (error) => {
@@ -132,6 +132,7 @@ export async function readAdapterStatus({
132
132
  export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", cliVersion = data.version } = {}) {
133
133
  const requests = data.requests || {};
134
134
  const performance = data.stream_performance?.by_route?.responses || {};
135
+ const terminalOutcomes = performance.terminal_outcomes?.totals;
135
136
  const admission = data.admission || {};
136
137
  const history = data.response_history || {};
137
138
  const images = data.image_optimization || {};
@@ -150,11 +151,18 @@ export function formatAdapterStatus({ baseUrl, data }, { commandName = "ccdx", c
150
151
  adapterLine,
151
152
  status("info", `Requests: ${count(requests.completed)}/${count(requests.total)} completed, ${count(requests.active)} active, ${count(requests.status_4xx)} 4xx, ${count(requests.status_5xx)} 5xx, ${count(requests.aborted)} aborted`),
152
153
  status("info", `Responses stream: TTFT avg ${latency(performance.ttft_ms?.avg)} (${count(performance.ttft_ms?.samples)} samples), TPOT avg ${tpot(performance.tpot_us)}`),
154
+ ...(terminalOutcomes ? [status("info", `Model outcomes: ${count(terminalOutcomes.completed)} completed, ${count(terminalOutcomes.incomplete)} incomplete, ${count(terminalOutcomes.failed)} failed, ${count(terminalOutcomes.cancelled)} cancelled, ${count(terminalOutcomes.unknown)} without terminal`)] : []),
153
155
  status("info", `Admission: ${count(admission.activeRequests)} active, ${count(admission.queued)} queued, ${count(admission.rejected)} rejected, ${count(admission.timedOut)} timed out, wait avg ${latency(admission.waitMsAvg)}`),
154
156
  status("info", `Memory: RSS ${mebibytes(processStats.rss_bytes)}, heap ${mebibytes(processStats.heap_used_bytes)}`),
155
- status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted`),
157
+ status("info", `History: ${mebibytes(history.bytes)} / ${mebibytes(limits.response_history_max_bytes)}, ${count(history.entries)} entries, ${count(history.evicted)} evicted ID markers`),
156
158
  status("info", `Image cache: ${count(images.cache_entries)} entries, ${cacheHitRate(images)} hits, ${mebibytes(images.cache_bytes)} / ${mebibytes(images.cache_max_bytes)}`),
157
159
  ];
160
+ if (Number.isFinite(history.tree_count)) {
161
+ const limit = Number(limits.response_history_max_bytes || history.maxBytes);
162
+ const nearLimit = Number.isFinite(limit) && limit > 0 && history.bytes / limit >= 0.9;
163
+ lines.push(status(nearLimit ? "warn" : "info",
164
+ `${nearLimit ? "History near limit" : "History diagnostics"}: largest tree ${mebibytes(history.largest_tree_bytes)} across ${count(history.tree_count)} trees; ${count(history.lookup_misses)} lookup misses, ${count(history.evicted_lookup_misses)} for evicted IDs`));
165
+ }
158
166
  if (imageHistory && typeof imageHistory === "object") {
159
167
  lines.push(status("info", `Visual history: ${count(imageHistory.active_recovery_trees)} recovery trees, ${count(imageHistory.adapted_requests)} adapted requests, ${count(imageHistory.historical_images_omitted)} older images omitted, ${count(imageHistory.timeouts_recorded)} timeouts`));
160
168
  }
@@ -0,0 +1,78 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="color-scheme" content="dark">
7
+ <link rel="icon" href="data:,">
8
+ <link rel="stylesheet" href="/ui.css">
9
+ <script src="/ui.js" defer></script>
10
+ <title>CCDX · Local dashboard</title>
11
+ </head>
12
+ <body>
13
+ <div class="shell">
14
+ <header class="topbar">
15
+ <div class="brand">CCDX <span class="brand-detail">/ LOCAL STATUS</span></div>
16
+ <div class="topbar-actions"><span id="connection" class="badge neutral" role="status">READING</span><button id="refresh" type="button">Refresh</button></div>
17
+ </header>
18
+
19
+ <main>
20
+ <section class="intro">
21
+ <h1><span aria-hidden="true">$</span> ccdx status</h1>
22
+ <p id="updated">Reading status…</p>
23
+ </section>
24
+
25
+ <section class="overview" aria-label="Overview">
26
+ <article class="card"><h2>ADAPTER</h2><strong id="version">—</strong><p id="uptime">—</p></article>
27
+ <article class="card"><h2>COPILOT</h2><strong id="copilot">—</strong><p id="token">—</p></article>
28
+ <article class="card"><h2>KNOWN MODELS</h2><strong id="models">—</strong><p id="model-source">—</p></article>
29
+ <article class="card"><h2>REQUESTS</h2><strong id="requests">—</strong><p id="request-errors">—</p></article>
30
+ </section>
31
+
32
+ <section class="details" aria-label="Runtime details">
33
+ <article class="panel">
34
+ <div class="panel-heading"><h2>MODEL OUTCOMES</h2><span>RESPONSES</span></div>
35
+ <dl class="stats"><div><dt>Completed</dt><dd id="completed">—</dd></div><div><dt>Incomplete</dt><dd id="incomplete">—</dd></div><div><dt>Failed</dt><dd id="failed">—</dd></div><div><dt>Cancelled</dt><dd id="cancelled">—</dd></div></dl>
36
+ <p class="hint">HTTP 2xx != model completed.</p>
37
+ </article>
38
+ <article class="panel">
39
+ <div class="panel-heading"><h2>LOCAL HISTORY</h2><span>MEMORY</span></div>
40
+ <strong id="history">—</strong><progress id="history-meter" class="meter" max="100" value="0" aria-label="History cache usage"></progress>
41
+ <p id="history-detail" class="hint">—</p>
42
+ </article>
43
+ <article class="panel">
44
+ <div class="panel-heading"><h2>IMAGE GENERATION</h2><span>OPTIONAL</span></div>
45
+ <strong id="image">—</strong><p id="image-detail" class="hint">—</p>
46
+ </article>
47
+ </section>
48
+
49
+ <section class="panel dashboard-section" aria-label="Terminal animation">
50
+ <div class="panel-heading"><h2>TERMINAL ANIMATION</h2><span id="animation-state">READING</span></div>
51
+ <div id="animation-options" class="animation-options" role="group" aria-label="Animation theme"></div>
52
+ <div class="section-actions"><button id="save-animation" type="button" disabled>Save selection</button><span id="animation-note" role="status">Applies on next ccdx start.</span></div>
53
+ </section>
54
+
55
+ <section class="panel dashboard-section" aria-label="Live upstream models">
56
+ <div class="panel-heading"><h2>UPSTREAM GPT MODELS</h2><div class="heading-actions"><span id="models-state">CHECKING LIVE</span><button id="refresh-models" type="button">Refresh</button></div></div>
57
+ <p id="models-note" class="table-note">Querying GitHub Copilot…</p>
58
+ <div class="table-wrap"><table><thead><tr><th scope="col">MODEL</th><th scope="col">VENDOR</th><th scope="col">API</th><th scope="col">FLAG</th></tr></thead><tbody id="live-models-body"><tr><td colspan="4">Loading…</td></tr></tbody></table></div>
59
+ <p class="hint">Catalog listing does not guarantee inference access.</p>
60
+ </section>
61
+
62
+ <section class="panel dashboard-section" aria-label="Usage by model">
63
+ <div class="panel-heading"><h2>USAGE</h2><div class="heading-actions"><span id="usage-state">READING LOG</span><button id="refresh-usage" type="button">Refresh</button></div></div>
64
+ <p id="usage-note" class="table-note">Aggregating local usage metadata…</p>
65
+ <div class="table-wrap"><table class="usage-table"><thead><tr><th scope="col">MODEL</th><th scope="col">CALLS</th><th scope="col">INPUT</th><th scope="col">CACHE READ</th><th scope="col">OUTPUT</th><th scope="col">TOTAL</th></tr></thead><tbody id="usage-body"><tr><td colspan="6">Loading…</td></tr></tbody></table></div>
66
+ </section>
67
+
68
+ <section class="lower" aria-label="Recent failures">
69
+ <div class="panel-heading"><h2>RECENT UPSTREAM FAILURES</h2><span>response.failed</span></div>
70
+ <ol id="failures"><li class="empty">Reading…</li></ol>
71
+ <p class="hint">Upstream messages may contain sensitive text.</p>
72
+ </section>
73
+ </main>
74
+
75
+ <footer>LOCAL ONLY / NO BACKGROUND POLLING</footer>
76
+ </div>
77
+ </body>
78
+ </html>
@@ -0,0 +1,83 @@
1
+ :root { color-scheme: dark; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
2
+ * { box-sizing: border-box; }
3
+ body { margin: 0; color: #e0e9df; background: #0b110d; font-size: 13px; }
4
+ .shell { max-width: 1220px; margin: 0 auto; padding: 0 24px; }
5
+ .topbar { min-height: 68px; display: flex; justify-content: space-between; align-items: center; gap: 20px; border-bottom: 1px solid #2a3a2e; }
6
+ .brand { color: #a6f5a8; font-size: 16px; font-weight: 750; letter-spacing: .07em; white-space: nowrap; }
7
+ .brand-detail { color: #78917d; font-size: 11px; font-weight: 500; letter-spacing: .03em; }
8
+ .topbar-actions { display: flex; align-items: center; gap: 20px; }
9
+ .badge { color: #a9b9aa; white-space: nowrap; font-size: 11px; font-weight: 650; text-transform: uppercase; }
10
+ .badge::before { content: "●"; margin-right: 8px; }
11
+ .badge.good { color: #84e99e; }
12
+ .badge.bad { color: #ff947e; }
13
+ button { border: 1px solid #45634b; border-radius: 2px; background: #152219; color: #b2f1bd; padding: 8px 13px; font: inherit; font-size: 11px; font-weight: 700; text-transform: uppercase; cursor: pointer; }
14
+ button:hover { background: #203526; }
15
+ button:focus-visible { outline: 2px solid #a6f5a8; outline-offset: 3px; }
16
+ button:disabled { opacity: .55; cursor: wait; }
17
+ .intro { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; padding: 38px 0 25px; }
18
+ h1 { margin: 0; font-size: clamp(20px, 3vw, 27px); font-weight: 650; letter-spacing: -.035em; }
19
+ h1 span { color: #84e99e; }
20
+ .intro p { margin: 0; color: #78917d; font-size: 11px; }
21
+ .overview { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 11px; }
22
+ .card, .panel, .lower { min-width: 0; background: #111a13; border: 1px solid #2b3b2f; border-radius: 2px; }
23
+ .card { min-height: 139px; padding: 18px; }
24
+ .card h2, .panel h2, .lower h2 { color: #83b98b; font-size: 11px; font-weight: 700; letter-spacing: .08em; margin: 0; }
25
+ .card strong, .panel > strong { display: block; margin-top: 21px; color: #f0f6ec; font-size: 23px; font-weight: 650; line-height: 1.25; letter-spacing: -.035em; overflow-wrap: anywhere; }
26
+ .card p { margin: 9px 0 0; color: #9aada0; font-size: 11px; line-height: 1.45; overflow-wrap: anywhere; }
27
+ .details { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 11px; margin-top: 11px; }
28
+ .panel { min-height: 205px; padding: 18px; }
29
+ .panel-heading { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
30
+ .panel-heading span { color: #6d8371; font-size: 10px; white-space: nowrap; }
31
+ .stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 17px; margin: 26px 0 0; }
32
+ .stats div { display: flex; justify-content: space-between; border-bottom: 1px solid #2b3b2f; padding-bottom: 8px; gap: 8px; }
33
+ .stats dt { color: #9aada0; font-size: 11px; }
34
+ .stats dd { margin: 0; font-size: 14px; font-weight: 650; }
35
+ #incomplete { color: #e3c77c; }
36
+ #failed { color: #ff9f91; }
37
+ .hint { color: #78917d; font-size: 11px; line-height: 1.5; margin: 15px 0 0; overflow-wrap: anywhere; }
38
+ .meter { display: block; width: 100%; height: 6px; border: 0; border-radius: 0; background: #2b3b2f; margin-top: 20px; appearance: none; }
39
+ .meter::-webkit-progress-bar { background: #2b3b2f; }
40
+ .meter::-webkit-progress-value { background: #84e99e; }
41
+ .meter::-moz-progress-bar { background: #84e99e; }
42
+ .dashboard-section { min-height: 0; margin-top: 11px; }
43
+ .heading-actions { display: flex; align-items: center; gap: 16px; }
44
+ .heading-actions button { padding: 5px 9px; }
45
+ .animation-options { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 18px; }
46
+ .theme-option { display: grid; grid-template-columns: 18px 76px minmax(0, 1fr); align-items: center; gap: 8px; min-width: 0; width: 100%; border: 1px solid #2b3b2f; padding: 10px; background: transparent; color: #e0e9df; text-align: left; text-transform: none; font-size: 11px; cursor: pointer; }
47
+ .theme-option::before { content: "○"; color: #78917d; font-size: 17px; line-height: 1; }
48
+ .theme-option[aria-pressed="true"] { border-color: #84e99e; background: #192a1d; }
49
+ .theme-option[aria-pressed="true"]::before { content: "●"; color: #84e99e; }
50
+ .theme-option:hover { background: #1a2a1e; }
51
+ .theme-name { color: #e0e9df; font-size: 11px; white-space: nowrap; }
52
+ .theme-name small { display: block; color: #78917d; font-size: 9px; margin-top: 2px; }
53
+ .theme-preview { color: #657d69; white-space: pre; overflow: hidden; font-size: 11px; letter-spacing: 0; }
54
+ .ansi-white { color: #f5fff5; }
55
+ .ansi-159 { color: #9af5e9; }
56
+ .ansi-123 { color: #75e4d9; }
57
+ .ansi-87 { color: #5ad0d4; }
58
+ .ansi-51 { color: #42b9d8; }
59
+ .ansi-45 { color: #3ca2d0; }
60
+ .ansi-39 { color: #4389c2; }
61
+ .ansi-33 { color: #4775b2; }
62
+ .section-actions { display: flex; align-items: center; gap: 15px; margin-top: 16px; }
63
+ .section-actions span { color: #9aada0; font-size: 11px; }
64
+ .table-note { margin: 15px 0 8px; color: #9aada0; font-size: 11px; }
65
+ .table-wrap { width: 100%; overflow-x: auto; }
66
+ table { width: 100%; border-collapse: collapse; font-size: 11px; }
67
+ th { color: #83b98b; text-align: left; font-weight: 700; letter-spacing: .05em; }
68
+ th, td { padding: 9px 12px 9px 0; border-bottom: 1px solid #2b3b2f; white-space: nowrap; }
69
+ th:last-child, td:last-child { padding-right: 0; }
70
+ td { color: #d8e2d8; }
71
+ .usage-table th:not(:first-child), .usage-table td:not(:first-child) { text-align: right; }
72
+ .usage-table tr.total-row td { color: #a6f5a8; font-weight: 700; }
73
+ .lower { margin-top: 11px; padding: 18px; }
74
+ .lower ol { list-style: none; margin: 16px 0 0; padding: 0; }
75
+ .lower li { display: grid; grid-template-columns: minmax(150px, 1fr) minmax(0, 2fr); gap: 14px; padding: 9px 0; border-top: 1px solid #2b3b2f; font-size: 11px; }
76
+ .lower li strong, .lower li span { overflow-wrap: anywhere; }
77
+ .lower li strong { color: #e0e9df; font-weight: 650; }
78
+ .lower li span, .lower li.empty { color: #9aada0; line-height: 1.5; }
79
+ footer { color: #617966; font-size: 10px; letter-spacing: .05em; padding: 22px 0 30px; }
80
+ @media (max-width: 1000px) { .animation-options { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
81
+ @media (max-width: 900px) { .overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } .details { grid-template-columns: repeat(2, minmax(0, 1fr)); } .details .panel:last-child { grid-column: 1 / -1; } }
82
+ @media (max-width: 680px) { .animation-options { grid-template-columns: 1fr; } }
83
+ @media (max-width: 620px) { .shell { padding: 0 16px; } .topbar { min-height: 60px; } .brand-detail { display: none; } .topbar-actions { gap: 11px; } .intro { display: block; padding: 29px 0 21px; } .intro p { margin-top: 9px; } .overview, .details { grid-template-columns: 1fr; } .details .panel:last-child { grid-column: auto; } .card { min-height: 122px; } .card strong { margin-top: 14px; } .lower li { display: block; } .lower li span { display: block; margin-top: 5px; } }