@memfork/cli 0.1.32 → 0.1.34

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/dist/cli.js CHANGED
File without changes
@@ -1,15 +1,25 @@
1
1
  /**
2
2
  * `memfork ui` — local HTTP server.
3
3
  *
4
- * Serves the pre-built React app from apps/visualizer/dist/ as static files and
5
- * exposes two API routes so the React app can discover the current tree
6
- * config and recall MemWal facts without exposing credentials in the
7
- * browser bundle.
4
+ * Serves the pre-built React app from packages/cli/ui/ as static files and
5
+ * exposes API routes so the React app can query MemWal without exposing
6
+ * credentials in the browser bundle.
8
7
  *
9
- * GET /api/config → { treeId, packageId, network, rpcUrl, hasMemwal }
10
- * GET /api/facts → { facts: MemWal results[] } (proxied server-side)
11
- * GET /* index.html (SPA fallback)
12
- * GET /assets/* static file
8
+ * GET /api/config → { treeId, packageId, network, rpcUrl, hasMemwal, rateLimited, retryInSeconds }
9
+ * GET /api/history?branch=<b>[&force=1] → { commits[], branch, rateLimited, retryInSeconds }
10
+ * GET /api/facts?branch=<b>[&force=1] { facts[], rateLimited, retryInSeconds }
11
+ * GET /* index.html (SPA fallback)
12
+ * GET /assets/* → static file
13
+ *
14
+ * Rate-limit strategy (relayer caps at 500 weighted-req/hour):
15
+ * • In-memory per-namespace cache (CACHE_TTL_MS). Most poll ticks served free.
16
+ * • On a 429, parse retry_after_seconds, set global backoff in memory AND
17
+ * persist just the timestamp to .memfork/.ui-backoff.json (a plain number —
18
+ * no decrypted content ever written to disk). Loaded on server start so
19
+ * restarts during a ban don't fire immediately.
20
+ * • ?force=1 bypasses the TTL but still respects the backoff window.
21
+ * • All responses include rateLimited + retryInSeconds so the UI can show a
22
+ * banner instead of looking silently empty.
13
23
  */
14
24
  import http from "node:http";
15
25
  export declare function startUiServer(distDir: string, port?: number): http.Server;
@@ -1,22 +1,34 @@
1
1
  /**
2
2
  * `memfork ui` — local HTTP server.
3
3
  *
4
- * Serves the pre-built React app from apps/visualizer/dist/ as static files and
5
- * exposes two API routes so the React app can discover the current tree
6
- * config and recall MemWal facts without exposing credentials in the
7
- * browser bundle.
4
+ * Serves the pre-built React app from packages/cli/ui/ as static files and
5
+ * exposes API routes so the React app can query MemWal without exposing
6
+ * credentials in the browser bundle.
8
7
  *
9
- * GET /api/config → { treeId, packageId, network, rpcUrl, hasMemwal }
10
- * GET /api/facts → { facts: MemWal results[] } (proxied server-side)
11
- * GET /* index.html (SPA fallback)
12
- * GET /assets/* static file
8
+ * GET /api/config → { treeId, packageId, network, rpcUrl, hasMemwal, rateLimited, retryInSeconds }
9
+ * GET /api/history?branch=<b>[&force=1] → { commits[], branch, rateLimited, retryInSeconds }
10
+ * GET /api/facts?branch=<b>[&force=1] { facts[], rateLimited, retryInSeconds }
11
+ * GET /* index.html (SPA fallback)
12
+ * GET /assets/* → static file
13
+ *
14
+ * Rate-limit strategy (relayer caps at 500 weighted-req/hour):
15
+ * • In-memory per-namespace cache (CACHE_TTL_MS). Most poll ticks served free.
16
+ * • On a 429, parse retry_after_seconds, set global backoff in memory AND
17
+ * persist just the timestamp to .memfork/.ui-backoff.json (a plain number —
18
+ * no decrypted content ever written to disk). Loaded on server start so
19
+ * restarts during a ban don't fire immediately.
20
+ * • ?force=1 bypasses the TTL but still respects the backoff window.
21
+ * • All responses include rateLimited + retryInSeconds so the UI can show a
22
+ * banner instead of looking silently empty.
13
23
  */
14
24
  import http from "node:http";
15
25
  import fs from "node:fs";
26
+ import os from "node:os";
16
27
  import path from "node:path";
17
28
  import { readProjectConfig, readCredentials, MEMWAL_CONSTANTS } from "../config.js";
18
29
  import { MemWal } from "@mysten-incubation/memwal";
19
30
  import { branchNamespace } from "@memfork/core";
31
+ // ─── Static file serving ─────────────────────────────────────────────────────
20
32
  const MIME = {
21
33
  ".html": "text/html; charset=utf-8",
22
34
  ".js": "application/javascript",
@@ -43,40 +55,91 @@ function json(res, data, status = 200) {
43
55
  });
44
56
  res.end(body);
45
57
  }
46
- async function handleApiConfig(res) {
47
- const project = readProjectConfig();
48
- const creds = readCredentials();
49
- const treeId = project?.treeId ?? creds.default ?? null;
50
- const network = (project?.network ?? "mainnet");
51
- const stored = treeId ? creds.trees[treeId] : undefined;
52
- json(res, {
53
- treeId,
54
- packageId: project?.packageId ?? "0xc13cc014fb8084b3468f6e5ffdc272e64ef35b7a912332eba7a0d44dd66b3121",
55
- network,
56
- rpcUrl: project?.rpcUrl ?? null,
57
- hasMemwal: !!(stored?.memwalKey && stored?.memwalAccountId),
58
- });
58
+ // ─── Rate-limit state ─────────────────────────────────────────────────────────
59
+ const CACHE_TTL_MS = 60_000;
60
+ const RECALL_LIMIT = 50;
61
+ const recallCache = new Map();
62
+ let rateLimitedUntil = 0;
63
+ /** Path used to persist the ban timestamp across restarts. Plain number only. */
64
+ function backoffFilePath() {
65
+ try {
66
+ // Walk up from cwd looking for .memfork/ — same logic as config resolution.
67
+ let dir = process.cwd();
68
+ while (true) {
69
+ const candidate = path.join(dir, ".memfork");
70
+ if (fs.existsSync(candidate))
71
+ return path.join(candidate, ".ui-backoff.json");
72
+ const parent = path.dirname(dir);
73
+ if (parent === dir)
74
+ break;
75
+ dir = parent;
76
+ }
77
+ // Fallback: home dir .memfork/
78
+ return path.join(os.homedir(), ".memfork", ".ui-backoff.json");
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ /** Load a persisted backoff timestamp so restarts during a ban stay quiet. */
85
+ function loadPersistedBackoff() {
86
+ try {
87
+ const p = backoffFilePath();
88
+ if (!p || !fs.existsSync(p))
89
+ return;
90
+ const { rateLimitedUntil: saved } = JSON.parse(fs.readFileSync(p, "utf8"));
91
+ if (typeof saved === "number" && saved > Date.now()) {
92
+ rateLimitedUntil = saved;
93
+ console.warn(`[memforks] Rate-limit backoff active from previous run — pausing relayer calls for ${Math.round((saved - Date.now()) / 1000)}s.`);
94
+ }
95
+ }
96
+ catch { /* ignore — bad file, no problem */ }
59
97
  }
98
+ function persistBackoff(until) {
99
+ try {
100
+ const p = backoffFilePath();
101
+ if (!p)
102
+ return;
103
+ fs.mkdirSync(path.dirname(p), { recursive: true });
104
+ fs.writeFileSync(p, JSON.stringify({ rateLimitedUntil: until }));
105
+ }
106
+ catch { /* best-effort */ }
107
+ }
108
+ function parseRetryAfterMs(err) {
109
+ const m = err.match(/retry_after_seconds"?\s*:\s*(\d+)/);
110
+ const secs = m ? Number(m[1]) : 300;
111
+ return secs * 1_000;
112
+ }
113
+ function rateLimitStatus() {
114
+ const remaining = rateLimitedUntil - Date.now();
115
+ return remaining > 0
116
+ ? { rateLimited: true, retryInSeconds: Math.ceil(remaining / 1000) }
117
+ : { rateLimited: false, retryInSeconds: 0 };
118
+ }
119
+ // ─── MemWal recall (cached + rate-limit-aware) ────────────────────────────────
60
120
  /**
61
- * Recall entries from a MemWal namespace using the SDK (signed requests).
62
- *
63
- * The MemWal HTTP API requires a cryptographically signed request; a plain
64
- * Bearer token is rejected, so we use the SDK directly.
121
+ * Recall entries from a MemWal namespace. Uses the SDK so requests are
122
+ * properly signed (plain Bearer tokens are rejected by the relayer).
65
123
  *
66
- * A single broad query at a high limit returns the whole namespace: semantic
67
- * recall returns the top-`limit` nearest entries, and when a namespace holds
68
- * fewer than `limit` commits (the common case) that's all of them regardless
69
- * of relevance. Issuing one query instead of several keeps us well under the
70
- * relayer's 500 weighted-requests/hour budget.
124
+ * - Fresh cache hit (< CACHE_TTL_MS): returns immediately, no relayer call.
125
+ * - In backoff window: returns stale cache (or []), no relayer call.
126
+ * - force=true: bypasses TTL but still respects the backoff window.
127
+ * - On 429: sets + persists backoff, returns stale cache.
71
128
  */
72
- async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
129
+ async function memwalRecall(relayer, key, accountId, namespace, force = false) {
130
+ const now = Date.now();
131
+ const cached = recallCache.get(namespace);
132
+ if (!force && cached && now - cached.ts < CACHE_TTL_MS)
133
+ return cached.data;
134
+ if (now < rateLimitedUntil)
135
+ return cached?.data ?? [];
73
136
  const mw = MemWal.create({ key, accountId, serverUrl: relayer, namespace });
74
137
  const seen = new Set();
75
138
  const out = [];
76
139
  try {
77
140
  const result = await mw.recall({
78
141
  query: "facts decisions conventions setup errors architecture memory",
79
- limit,
142
+ limit: RECALL_LIMIT,
80
143
  });
81
144
  for (const r of result.results) {
82
145
  const blobId = String(r.blob_id ?? "");
@@ -85,69 +148,82 @@ async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
85
148
  out.push({ blob_id: blobId, text: String(r.text ?? ""), distance: r.distance });
86
149
  }
87
150
  }
151
+ recallCache.set(namespace, { ts: now, data: out });
152
+ return out;
88
153
  }
89
154
  catch (e) {
90
- // Surface rate limits so the caller can log; swallow other transient errors.
91
- if (String(e).includes("429")) {
92
- console.warn("[memforks] MemWal rate limit hit — backing off:", String(e));
155
+ const msg = String(e);
156
+ if (msg.includes("429")) {
157
+ const backoff = parseRetryAfterMs(msg);
158
+ rateLimitedUntil = Date.now() + backoff;
159
+ persistBackoff(rateLimitedUntil);
160
+ console.warn(`[memforks] Rate limit hit — pausing relayer calls for ${Math.round(backoff / 1000)}s.`);
93
161
  }
162
+ return cached?.data ?? [];
94
163
  }
95
- return out;
96
164
  }
97
- async function handleApiFacts(res, url) {
98
- const branch = url.searchParams.get("branch") ?? "main";
165
+ function resolveMemwalCreds(branch) {
99
166
  const project = readProjectConfig();
100
167
  const creds = readCredentials();
101
168
  const treeId = project?.treeId ?? creds.default;
102
169
  const network = (project?.network ?? "mainnet");
103
170
  const stored = treeId ? creds.trees[treeId] : undefined;
104
- if (!stored?.memwalKey || !stored?.memwalAccountId || !treeId) {
105
- json(res, { facts: [] });
171
+ if (!stored?.memwalKey || !stored?.memwalAccountId || !treeId)
172
+ return null;
173
+ return {
174
+ relayer: stored.memwalRelayer ?? MEMWAL_CONSTANTS[network].relayer,
175
+ key: stored.memwalKey,
176
+ accountId: stored.memwalAccountId,
177
+ treeId,
178
+ namespace: (b) => branchNamespace(treeId, b),
179
+ };
180
+ }
181
+ // ─── Route handlers ───────────────────────────────────────────────────────────
182
+ async function handleApiConfig(res) {
183
+ const project = readProjectConfig();
184
+ const creds = readCredentials();
185
+ const treeId = project?.treeId ?? creds.default ?? null;
186
+ const network = (project?.network ?? "mainnet");
187
+ const stored = treeId ? creds.trees[treeId] : undefined;
188
+ json(res, {
189
+ treeId,
190
+ packageId: project?.packageId ?? "0xc13cc014fb8084b3468f6e5ffdc272e64ef35b7a912332eba7a0d44dd66b3121",
191
+ network,
192
+ rpcUrl: project?.rpcUrl ?? null,
193
+ hasMemwal: !!(stored?.memwalKey && stored?.memwalAccountId),
194
+ ...rateLimitStatus(),
195
+ });
196
+ }
197
+ async function handleApiFacts(res, url) {
198
+ const branch = url.searchParams.get("branch") ?? "main";
199
+ const force = url.searchParams.get("force") === "1";
200
+ const mc = resolveMemwalCreds(branch);
201
+ if (!mc) {
202
+ json(res, { facts: [], ...rateLimitStatus() });
106
203
  return;
107
204
  }
108
- const relayer = stored.memwalRelayer ?? MEMWAL_CONSTANTS[network].relayer;
109
- const namespace = branchNamespace(treeId, branch);
110
205
  try {
111
- const facts = await memwalRecall(relayer, stored.memwalKey, stored.memwalAccountId, namespace);
112
- json(res, { facts });
206
+ const facts = await memwalRecall(mc.relayer, mc.key, mc.accountId, mc.namespace(branch), force);
207
+ json(res, { facts, ...rateLimitStatus() });
113
208
  }
114
209
  catch (e) {
115
- json(res, { facts: [], error: String(e) });
210
+ json(res, { facts: [], error: String(e), ...rateLimitStatus() });
116
211
  }
117
212
  }
118
- /**
119
- * GET /api/history?branch=<name>&limit=<n>
120
- *
121
- * Returns all off-chain CommitPayload objects stored in MemWal for this branch,
122
- * sorted oldest-first. Each entry includes the MemWal blob_id plus the parsed
123
- * payload fields that the UI needs (branch, author, ts_ms, delta, parent_blob_ids).
124
- *
125
- * The browser cannot call MemWal directly (SEAL-encrypted, key lives server-side),
126
- * so this endpoint acts as the commit-history proxy.
127
- */
128
213
  async function handleApiHistory(res, url) {
129
214
  const branch = url.searchParams.get("branch") ?? "main";
130
- const limit = Math.min(Number(url.searchParams.get("limit") ?? "500"), 1000);
131
- const project = readProjectConfig();
132
- const creds = readCredentials();
133
- const treeId = project?.treeId ?? creds.default;
134
- const network = (project?.network ?? "mainnet");
135
- const stored = treeId ? creds.trees[treeId] : undefined;
136
- if (!stored?.memwalKey || !stored?.memwalAccountId || !treeId) {
137
- json(res, { commits: [] });
215
+ const force = url.searchParams.get("force") === "1";
216
+ const mc = resolveMemwalCreds(branch);
217
+ if (!mc) {
218
+ json(res, { commits: [], ...rateLimitStatus() });
138
219
  return;
139
220
  }
140
- const relayer = stored.memwalRelayer ?? MEMWAL_CONSTANTS[network].relayer;
141
- const namespace = branchNamespace(treeId, branch);
142
221
  try {
143
- const results = await memwalRecall(relayer, stored.memwalKey, stored.memwalAccountId, namespace, limit);
222
+ const results = await memwalRecall(mc.relayer, mc.key, mc.accountId, mc.namespace(branch), force);
144
223
  const commits = results.flatMap((entry) => {
145
- const blobId = entry.blob_id;
146
- const text = entry.text;
147
- // Try to parse the stored text as a CommitPayload JSON.
148
224
  let payload = null;
149
225
  try {
150
- payload = JSON.parse(text);
226
+ payload = JSON.parse(entry.text);
151
227
  }
152
228
  catch {
153
229
  return [];
@@ -155,32 +231,29 @@ async function handleApiHistory(res, url) {
155
231
  if (payload["type"] !== "commit")
156
232
  return [];
157
233
  return [{
158
- blob_id: blobId,
234
+ blob_id: entry.blob_id,
159
235
  branch: String(payload["branch"] ?? branch),
160
236
  ts_ms: Number(payload["ts_ms"] ?? 0),
161
237
  parent_blob_ids: payload["parent_blob_ids"] ?? [],
162
238
  parent_blob_hashes: payload["parent_blob_hashes"] ?? [],
163
- // Extract readable facts from the delta.
164
239
  message: (() => {
165
240
  const delta = payload["delta"];
166
241
  const facts = delta?.["facts"];
167
- return facts?.length ? facts[0] : `commit ${blobId.slice(0, 8)}`;
242
+ return facts?.length ? facts[0] : `commit ${entry.blob_id.slice(0, 8)}`;
168
243
  })(),
169
244
  delta: payload["delta"] ?? {},
170
245
  }];
171
246
  });
172
- // Sort oldest-first by ts_ms.
173
247
  commits.sort((a, b) => a.ts_ms - b.ts_ms);
174
- json(res, { commits, branch });
248
+ json(res, { commits, branch, ...rateLimitStatus() });
175
249
  }
176
250
  catch (e) {
177
- json(res, { commits: [], error: String(e) });
251
+ json(res, { commits: [], error: String(e), ...rateLimitStatus() });
178
252
  }
179
253
  }
254
+ // ─── Static serving ───────────────────────────────────────────────────────────
180
255
  function serveStatic(res, distDir, urlPath) {
181
- // Resolve the requested file path.
182
256
  let filePath = path.join(distDir, urlPath);
183
- // SPA fallback: no extension or file not found → serve index.html.
184
257
  if (!path.extname(filePath) || !fs.existsSync(filePath)) {
185
258
  filePath = path.join(distDir, "index.html");
186
259
  }
@@ -193,17 +266,16 @@ function serveStatic(res, distDir, urlPath) {
193
266
  const isImmutable = urlPath.startsWith("/assets/");
194
267
  res.writeHead(200, {
195
268
  "Content-Type": mimeType,
196
- "Cache-Control": isImmutable
197
- ? "public, max-age=31536000, immutable"
198
- : "no-cache",
269
+ "Cache-Control": isImmutable ? "public, max-age=31536000, immutable" : "no-cache",
199
270
  "Access-Control-Allow-Origin": "*",
200
271
  });
201
272
  fs.createReadStream(filePath).pipe(res);
202
273
  }
274
+ // ─── Server factory ───────────────────────────────────────────────────────────
203
275
  export function startUiServer(distDir, port = 4242) {
276
+ loadPersistedBackoff();
204
277
  const server = http.createServer((req, res) => {
205
278
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
206
- // CORS pre-flight.
207
279
  if (req.method === "OPTIONS") {
208
280
  res.writeHead(204, { "Access-Control-Allow-Origin": "*" });
209
281
  res.end();
package/dist/index.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memfork/cli",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "MemForks CLI — init, commit, recall, merge, install plugins",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1 @@
1
+ @import"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500;600&display=swap";.topbar{display:flex;align-items:center;gap:var(--space-4);height:var(--topbar-height);padding:0 var(--space-5);border-bottom:1px solid var(--border);background:var(--bg-1);flex-shrink:0;position:relative;z-index:10}.topbar-left{display:flex;align-items:center;gap:var(--space-3);flex-shrink:0}.topbar-logo{display:flex;align-items:center;gap:var(--space-2);font-weight:600;font-size:.9rem;color:var(--fg-0);letter-spacing:-.01em}.topbar-tree-id{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3);background:var(--bg-2);border:1px solid var(--border);padding:.15rem .5rem;border-radius:var(--radius-sm)}.topbar-live-badge{display:flex;align-items:center;gap:5px;font-size:.72rem;font-family:var(--font-mono);padding:.15rem .55rem;border-radius:999px;border:1px solid transparent}.topbar-live-badge.live{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.topbar-live-badge.offline{background:var(--bg-2);color:var(--fg-3);border-color:var(--border)}.topbar-live-dot{width:6px;height:6px;border-radius:50%;background:currentColor}.topbar-live-badge.live .topbar-live-dot{animation:pulse-dot 1.8s ease-in-out infinite}@keyframes pulse-dot{0%,to{opacity:1}50%{opacity:.35}}.topbar-views{display:flex;align-items:center;gap:2px;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:3px;flex-shrink:0}.topbar-view-btn{padding:.2rem .75rem;font-size:.78rem;font-weight:500;color:var(--fg-2);border-radius:6px;background:transparent;border:none;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);white-space:nowrap}.topbar-view-btn:hover{color:var(--fg-0);background:var(--bg-3)}.topbar-view-btn.active{background:var(--bg-0);color:var(--fg-0);box-shadow:var(--shadow-sm)}.topbar-spacer{flex:1}.topbar-branch-select{-moz-appearance:none;appearance:none;-webkit-appearance:none;font-family:var(--font-mono);font-size:.74rem;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.25rem 1.6rem .25rem .65rem;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5' viewBox='0 0 8 5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%23667066'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right .6rem center;transition:border-color var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);max-width:200px}.topbar-branch-select:hover{border-color:var(--border-strong);color:var(--fg-0)}.topbar-branch-select:focus-visible{outline:1px solid var(--accent);outline-offset:1px}.topbar-right{display:flex;align-items:center;gap:var(--space-2);flex-shrink:0}.topbar-replay-btn{padding:.2rem .75rem;font-size:.75rem;font-family:var(--font-mono);font-weight:500;color:var(--fg-2);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);white-space:nowrap}.topbar-replay-btn:hover{background:var(--bg-2);color:var(--fg-0);border-color:var(--border-strong)}.topbar-replay-btn.active{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.topbar-refresh-btn{padding:.2rem .5rem;font-size:.85rem;color:var(--fg-2);background:transparent;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out);line-height:1}.topbar-refresh-btn:hover:not(:disabled){background:var(--bg-2);color:var(--fg-0)}.topbar-refresh-btn:disabled{cursor:default}.inspector{display:flex;flex-direction:column;gap:var(--space-5);padding:var(--space-5) var(--space-5) var(--space-8);overflow-y:auto;height:100%}.inspector-header{display:flex;flex-direction:column;gap:var(--space-2);padding-bottom:var(--space-4);border-bottom:1px solid var(--border)}.inspector-title-row{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-2)}.inspector-commit-id{font-family:var(--font-mono);font-size:.88rem;font-weight:600;color:var(--accent);background:var(--accent-dim);border:1px solid var(--accent-border);padding:.2rem .55rem;border-radius:var(--radius-sm);letter-spacing:.03em}.inspector-message{font-size:.875rem;color:var(--fg-0);line-height:1.55;font-weight:500}.inspector-section{display:flex;flex-direction:column;gap:var(--space-2)}.inspector-section-label{font-family:var(--font-mono);font-size:.68rem;letter-spacing:.08em;color:var(--fg-3);text-transform:uppercase;margin-bottom:2px}.inspector-kv{display:grid;grid-template-columns:90px 1fr;align-items:baseline;gap:var(--space-3);font-size:.825rem}.inspector-key{color:var(--fg-2);font-size:.78rem}.inspector-val{color:var(--fg-0);font-family:var(--font-mono);font-size:.78rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inspector-mono-sm{font-size:.7rem;color:var(--fg-1)}.inspector-parents{list-style:none;display:flex;flex-direction:column;gap:var(--space-1)}.inspector-parent-row{list-style:none}.inspector-parent-btn{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);font-size:.8rem;color:var(--fg-1);transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);text-align:left}.inspector-parent-btn:hover{background:var(--bg-3);border-color:var(--accent);color:var(--fg-0)}.inspector-parent-btn code{font-family:var(--font-mono);color:var(--accent);font-size:.78rem}.inspector-parent-branch{flex:1;color:var(--fg-2);font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.inspector-parent-arrow{color:var(--fg-3);font-size:.75rem}.inspector-link{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);color:var(--fg-1);font-size:.8rem;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);text-decoration:none}.inspector-link:hover{background:var(--bg-3);border-color:var(--accent);color:var(--fg-0);text-decoration:none}.inspector-link code{font-family:var(--font-mono);font-size:.72rem;color:var(--accent)}.inspector-link-icon{font-size:.8rem;color:var(--fg-3)}.inspector-link-ext{margin-left:auto;color:var(--fg-3);font-size:.7rem}.inspector-link-sm{padding:var(--space-1) var(--space-2);font-size:.72rem}.inspector-code-block{display:block;font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-3);word-break:break-all;line-height:1.6}.inspector-proposal-summary{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);display:flex;flex-direction:column;gap:var(--space-2)}.inspector-attestations{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.inspector-attest-row{background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:var(--space-2) var(--space-3);display:flex;flex-direction:column;gap:var(--space-1)}.inspector-attest-top{display:flex;align-items:center;flex-wrap:wrap;gap:var(--space-2)}.inspector-attest-signer{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1)}.inspector-attest-time{margin-left:auto;font-size:.7rem;color:var(--fg-3);font-family:var(--font-mono)}.inspector-snapshot{margin-top:auto}.inspector-snapshot-body{background:var(--bg-2);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:var(--radius-md);padding:var(--space-3) var(--space-4);font-size:.825rem;color:var(--fg-0);line-height:1.6}.inspector-snapshot-hint{font-size:.72rem;color:var(--fg-3);font-style:italic}.inspector-empty-hint{font-size:.8rem;color:var(--fg-3);font-style:italic}.inspector-copy-row{display:flex;align-items:center;gap:var(--space-2);width:100%;background:none;border:none;cursor:pointer;padding:var(--space-1) 0;color:var(--fg-1);font-size:.82rem;text-align:left}.inspector-copy-row:hover{color:var(--accent)}.inspector-copy-badge{margin-left:auto;font-size:.7rem;color:var(--fg-3);background:var(--surface-1);border:1px solid var(--border);border-radius:var(--radius-sm);padding:1px 6px;flex-shrink:0}.inspector-copy-row:hover .inspector-copy-badge{color:var(--accent);border-color:var(--accent)}.right-drawer{position:relative;display:flex;flex-direction:column;width:0;overflow:hidden;border-left:1px solid transparent;background:var(--bg-1);transition:width var(--duration-med) var(--ease-out),border-color var(--duration-med) var(--ease-out);flex-shrink:0}.right-drawer.open{width:var(--drawer-width);border-left-color:var(--border)}.drawer-toolbar{display:flex;align-items:center;justify-content:space-between;padding:0 var(--space-4);height:44px;border-bottom:1px solid var(--border);flex-shrink:0}.drawer-title{font-size:.8rem;font-weight:600;color:var(--fg-1);letter-spacing:.02em}.drawer-body{flex:1;overflow:hidden;display:flex;flex-direction:column;min-height:0}.drawer-proposals-footer{border-top:1px solid var(--border);padding:var(--space-3) var(--space-4);flex-shrink:0}.drawer-proposals-label{font-family:var(--font-mono);font-size:.68rem;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-3);margin-bottom:var(--space-2)}.drawer-proposals-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-1)}.drawer-proposal-chip{display:flex;align-items:center;gap:var(--space-2);width:100%;padding:var(--space-2) var(--space-3);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);font-size:.76rem;color:var(--fg-1);text-align:left;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out)}.drawer-proposal-chip:hover{background:var(--bg-3);border-color:var(--warning)}.drawer-proposal-chip>span:nth-child(2){flex:1;font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.drawer-proposal-chip>span:last-child{color:var(--fg-3);font-size:.7rem;flex-shrink:0}.dag-canvas-wrapper,.dag-canvas-scroll{position:relative;flex:1;overflow:auto;background:var(--bg-0);background-image:linear-gradient(rgba(255,255,255,.012) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.012) 1px,transparent 1px);background-size:32px 32px}.dag-canvas-svg,.dag-canvas{display:block;cursor:grab}.dag-canvas-svg:active,.dag-canvas:active{cursor:grabbing}.dag-node{transition:opacity var(--duration-fast) var(--ease-out)}@keyframes dag-pop-in{0%{transform:scale(.3);opacity:0}65%{transform:scale(1.15);opacity:1}to{transform:scale(1);opacity:1}}.dag-node--new{animation:dag-pop-in .45s cubic-bezier(.34,1.56,.64,1) both;transform-box:fill-box;transform-origin:center}.dag-node:focus-visible circle,.dag-node:focus-visible rect{outline:2px solid var(--accent)}.dag-node-hash{opacity:0;transition:opacity var(--duration-fast) var(--ease-out)}.dag-node:hover .dag-node-hash,.dag-node.selected .dag-node-hash{opacity:1}.dag-edge{transition:opacity var(--duration-fast) var(--ease-out),stroke-width var(--duration-fast) var(--ease-out)}.dag-lane-label{transition:opacity var(--duration-fast) var(--ease-out)}.dag-empty{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-2);pointer-events:none;color:var(--fg-3);font-size:.875rem}.dag-empty p{text-align:center}.memory-view{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-0)}.memory-search-row{display:flex;align-items:center;gap:var(--space-4);padding:var(--space-4) var(--space-5);border-bottom:1px solid var(--border);flex-shrink:0}.memory-search-wrap{position:relative;flex:1;max-width:480px}.memory-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:var(--fg-3);pointer-events:none}.memory-search{width:100%;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:.45rem 2rem .45rem 2.1rem;font-family:var(--font-sans);font-size:.85rem;color:var(--fg-0);outline:none;transition:border-color var(--duration-fast) var(--ease-out),box-shadow var(--duration-fast) var(--ease-out)}.memory-search::placeholder{color:var(--fg-3)}.memory-search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-dim)}.memory-search-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);width:18px;height:18px;font-size:14px;line-height:18px;text-align:center;color:var(--fg-3);border-radius:50%;transition:color var(--duration-fast) var(--ease-out)}.memory-search-clear:hover{color:var(--fg-0)}.memory-count-label{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3);white-space:nowrap}.memory-empty{flex:1;display:flex;align-items:center;justify-content:center;color:var(--fg-3);font-size:.875rem}.memory-empty p{text-align:center;line-height:1.8}.memory-groups{flex:1;overflow-y:auto;padding:var(--space-4) var(--space-5) var(--space-8);display:flex;flex-direction:column;gap:var(--space-6)}.memory-group{display:flex;flex-direction:column;gap:var(--space-2)}.memory-group-header{display:flex;align-items:center;gap:var(--space-2);padding-bottom:var(--space-2);border-bottom:1px solid var(--border)}.memory-group-name{font-family:var(--font-mono);font-size:.8rem;font-weight:600;color:var(--fg-1);letter-spacing:.02em}.memory-group-count{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3)}.memory-fact-list{list-style:none;display:flex;flex-direction:column;gap:2px}.memory-fact-row{padding:var(--space-3) var(--space-4);background:var(--bg-1);border:1px solid var(--border);border-radius:var(--radius-md);display:flex;flex-direction:column;gap:var(--space-2);cursor:pointer;transition:border-color var(--duration-fast) var(--ease-out),background var(--duration-fast) var(--ease-out);outline:none}.memory-fact-row:hover{background:var(--bg-2);border-color:var(--accent-border)}.memory-fact-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.memory-fact-content{font-size:.85rem;color:var(--fg-0);line-height:1.6}.memory-fact-meta-row{display:flex;align-items:center;gap:var(--space-2);min-width:0}.memory-fact-branch{font-family:var(--font-mono);font-size:.68rem;font-weight:500;color:var(--accent);background:var(--accent-dim);border-radius:var(--radius-sm);padding:1px 6px;flex-shrink:0}.memory-fact-blob{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3);flex-shrink:0}.memory-fact-time{margin-left:auto;font-size:.7rem;font-family:var(--font-mono);color:var(--fg-3);white-space:nowrap;flex-shrink:0;-webkit-user-select:none;user-select:none}.history-view{flex:1;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-0)}.history-header{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-5);border-bottom:1px solid var(--border);flex-shrink:0}.history-header-count{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.history-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-3);color:var(--fg-3);font-size:.875rem;padding:var(--space-8);text-align:center}.history-empty-sub{font-size:.78rem;color:var(--fg-3);max-width:420px;line-height:1.6}.history-empty-sub code{color:var(--fg-2)}.history-list{flex:1;overflow-y:auto;list-style:none;padding:var(--space-2) var(--space-3)}.history-row{display:grid;grid-template-columns:28px 1fr;gap:var(--space-3);padding:var(--space-2) var(--space-3);border:1px solid transparent;border-left:3px solid transparent;border-radius:var(--radius-md);transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);outline:none}.history-row--commit{cursor:pointer}.history-row--commit:hover{background:var(--bg-2)}.history-row--commit.selected{background:var(--bg-2);border-left-color:var(--border-strong)}.history-row--commit:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.history-row--fork{cursor:default;background:#ffffff04}.history-row--anchor{cursor:pointer;border-left-color:var(--accent-border);background:#1f9d720a}.history-row--anchor:hover{background:#1f9d7214;border-left-color:var(--accent)}.history-row--anchor.selected{background:var(--accent-dim);border-left-color:var(--accent)}.history-row--anchor:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}@keyframes row-pop-in{0%{opacity:0;transform:translateY(-6px)}to{opacity:1;transform:none}}.is-new{animation:row-pop-in .35s var(--ease-out) both}.history-gutter{position:relative;display:flex;flex-direction:column;align-items:center}.history-line{width:2px;flex:1;background:var(--border-strong);min-height:6px}.history-node{width:10px;height:10px;border-radius:50%;border:2px solid currentColor;background:var(--bg-0);flex-shrink:0;z-index:1}.history-node--fork{width:9px;height:9px;border-radius:2px;border:1.5px dashed currentColor;background:var(--bg-0);transform:rotate(45deg);flex-shrink:0;z-index:1}.history-node--anchor.merge{width:12px;height:12px;border-radius:3px;border:2px solid var(--accent);background:var(--accent-dim);transform:rotate(45deg);flex-shrink:0;z-index:1}.history-row--anchor.selected .history-node--anchor{background:var(--accent)}.history-node.branch-green,.history-node--fork.branch-green{color:var(--lane-0)}.history-node.branch-blue,.history-node--fork.branch-blue{color:var(--lane-1)}.history-node.branch-orange,.history-node--fork.branch-orange{color:var(--lane-3)}.history-node.branch-red,.history-node--fork.branch-red{color:var(--danger)}.history-node.branch-purple,.history-node--fork.branch-purple{color:var(--lane-4)}.history-node.branch-muted,.history-node--fork.branch-muted{color:var(--fg-3)}.history-content{display:flex;flex-direction:column;gap:var(--space-1);min-width:0;padding:3px 0}.history-top-row{display:flex;align-items:center;gap:var(--space-2);min-width:0}.history-fork-top{flex-wrap:wrap;row-gap:var(--space-1)}.history-flex-spacer{flex:1}.history-short-id{font-family:var(--font-mono);font-size:.72rem;color:var(--accent);background:var(--bg-1);border:1px solid var(--border);padding:.05rem .4rem;border-radius:var(--radius-sm);flex-shrink:0}.history-row--anchor.selected .history-short-id{background:var(--accent-dim)}.history-message{font-size:.83rem;font-weight:500;color:var(--fg-0);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.history-row--commit .history-message{color:var(--fg-1);font-weight:400}.history-meta-row{display:flex;align-items:center;gap:var(--space-2);font-size:.72rem;color:var(--fg-2);flex-wrap:wrap}.history-author{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-1)}.history-sep{color:var(--fg-3);-webkit-user-select:none;user-select:none}.history-time{color:var(--fg-2)}.history-parents-hint,.history-resolver-label{font-family:var(--font-mono);color:var(--fg-3);font-size:.68rem}.history-resolver-label{color:var(--fg-2)}.history-tx{font-family:var(--font-mono);font-size:.66rem;color:var(--fg-3)}.history-fork-glyph{font-size:.85rem;color:var(--fg-3);flex-shrink:0;line-height:1;margin-right:1px}.history-fork-label{font-size:.78rem;color:var(--fg-3);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.history-fork-label strong{color:var(--fg-1);font-weight:500}.history-onchain-glyph{color:var(--fg-3);font-size:.75rem;flex-shrink:0;line-height:1}.history-onchain-glyph--anchor{color:var(--accent);font-size:.85rem}.history-message strong{font-weight:600}.merges-view{flex:1;display:flex;flex-direction:column;overflow-y:auto;gap:var(--space-6);padding:var(--space-5);background:var(--bg-0)}.merges-empty{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:var(--space-3);color:var(--fg-3);font-size:.875rem;text-align:center;padding:var(--space-8)}.merges-empty-sub{font-size:.78rem;color:var(--fg-3);max-width:400px;line-height:1.6}.merges-empty-sub code{color:var(--fg-2)}.merges-section{display:flex;flex-direction:column;gap:var(--space-3)}.merges-section-header{display:flex;align-items:center;gap:var(--space-3);padding-bottom:var(--space-2);border-bottom:1px solid var(--border)}.merges-section-title{font-family:var(--font-mono);font-size:.72rem;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-3)}.merges-ceremonies{display:flex;flex-direction:column;gap:var(--space-4)}.ceremony-card{display:flex;flex-direction:column;gap:var(--space-4);border:1px solid var(--border);border-left:3px solid var(--warning);border-radius:var(--radius-lg);background:var(--bg-1);padding:var(--space-4) var(--space-5)}.ceremony-header{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3)}.ceremony-header-left{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.ceremony-icon{font-size:1rem;color:var(--warning)}.ceremony-resolver-label{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.ceremony-route-text{display:flex;align-items:center;gap:var(--space-2);font-size:.85rem;color:var(--fg-1)}.ceremony-route-text strong{color:var(--fg-0);font-weight:600}.ceremony-arrow{color:var(--fg-3);font-size:.8rem}.ceremony-expiry{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-2)}.ceremony-expiry--warn{color:var(--warning)}.ceremony-attests{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.ceremony-attests-empty{font-size:.8rem;color:var(--fg-3);font-style:italic;padding:var(--space-2) 0}.ceremony-attest-row{display:flex;align-items:center;gap:var(--space-2);padding:var(--space-2) var(--space-3);border-radius:var(--radius-md);font-size:.8rem;flex-wrap:wrap}.ceremony-attest-row--cast{background:var(--bg-2);border:1px solid var(--border)}.ceremony-attest-row--pending{background:transparent;border:1px dashed var(--border);opacity:.65}@keyframes pulse-dot{0%,to{opacity:.3;transform:scale(.85)}50%{opacity:1;transform:scale(1.1)}}.ceremony-attest-pulse{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--warning);animation:pulse-dot 1.4s ease-in-out infinite;flex-shrink:0}.ceremony-attest-check{color:var(--accent);font-size:.82rem;flex-shrink:0}.ceremony-attest-label{font-family:var(--font-mono);font-size:.76rem;color:var(--fg-1);min-width:60px}.ceremony-attest-model{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-3);background:var(--bg-3);border:1px solid var(--border);border-radius:var(--radius-sm);padding:0 .3rem}.ceremony-attest-vote-text{font-size:.74rem;color:var(--fg-2);max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ceremony-attest-voting{font-size:.75rem;color:var(--fg-3);font-style:italic}.ceremony-attest-time-spacer{flex:1}.ceremony-attest-reltime{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3);white-space:nowrap}.ceremony-threshold{display:flex;flex-direction:column;gap:var(--space-2)}.ceremony-threshold-bar{height:4px;border-radius:2px;background:var(--bg-3);overflow:hidden}.ceremony-threshold-fill{height:100%;background:var(--accent);border-radius:2px;transition:width .5s var(--ease-out)}.ceremony-threshold-label{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-2)}.ceremony-threshold-note{color:var(--fg-3)}.merges-settled-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-2)}.settled-row{display:flex;align-items:center;justify-content:space-between;gap:var(--space-3);padding:var(--space-2) var(--space-3);border:1px solid var(--border);border-radius:var(--radius-md);cursor:pointer;transition:background var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out);flex-wrap:wrap}.settled-row:hover{background:var(--bg-2);border-color:var(--accent-border)}.settled-row:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.settled-row-left{display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap;min-width:0}.settled-check{color:var(--accent);font-size:.82rem;flex-shrink:0}.settled-route{display:flex;align-items:center;gap:var(--space-2);font-size:.82rem;color:var(--fg-1);min-width:0}.settled-route strong{color:var(--fg-0);font-weight:500}.settled-arrow{color:var(--fg-3);font-size:.78rem}.settled-resolver{font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3)}.settled-row-right{display:flex;align-items:center;gap:var(--space-3);flex-shrink:0}.settled-time{font-family:var(--font-mono);font-size:.72rem;color:var(--fg-3)}.settled-view-hint{font-size:.72rem;color:var(--fg-3);opacity:0;transition:opacity var(--duration-fast) var(--ease-out)}.settled-row:hover .settled-view-hint{opacity:1}.merges-section--graveyard .merges-section-title{color:var(--danger);opacity:.75}.merges-graveyard-note{font-size:.78rem;color:var(--fg-3);line-height:1.6;font-style:italic;max-width:560px}.merges-graveyard-list{list-style:none;display:flex;flex-direction:column;gap:var(--space-3)}.graveyard-row{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-3) var(--space-4);background:var(--bg-1);border:1px solid var(--border);border-left:3px solid var(--danger);border-radius:var(--radius-md);opacity:.8;transition:opacity var(--duration-fast) var(--ease-out)}.graveyard-row:hover{opacity:1}.graveyard-row-top{display:flex;align-items:center;gap:var(--space-2);flex-wrap:wrap}.graveyard-cross{color:var(--danger);font-size:.82rem;flex-shrink:0}.graveyard-time{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-3);margin-left:auto}.graveyard-ask-btn{font-family:var(--font-mono);font-size:.7rem;color:var(--fg-2);background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-sm);padding:.15rem .55rem;cursor:pointer;transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out),border-color var(--duration-fast) var(--ease-out)}.graveyard-ask-btn:hover{background:var(--bg-3);border-color:var(--accent-border);color:var(--accent)}.graveyard-rationale{font-size:.78rem;color:var(--fg-2);line-height:1.55;padding-left:var(--space-2);border-left:2px solid var(--border-strong)}:root{--bg-0: #0c0e13;--bg-1: #12151c;--bg-2: #181c25;--bg-3: #20252f;--fg-0: #e4e7ed;--fg-1: #9aa1ae;--fg-2: #69707d;--fg-3: #474d58;--accent: #1f9d72;--accent-hover: #27b886;--accent-dim: rgba(31, 157, 114, .13);--accent-glow: rgba(31, 157, 114, .22);--accent-border: rgba(31, 157, 114, .3);--border: rgba(255, 255, 255, .06);--border-strong: rgba(255, 255, 255, .11);--danger: #d65b5b;--danger-dim: rgba(214, 91, 91, .13);--danger-border: rgba(214, 91, 91, .3);--warning: #d2972f;--warning-dim: rgba(210, 151, 47, .13);--warning-border: rgba(210, 151, 47, .3);--purple: #8a82c9;--purple-dim: rgba(138, 130, 201, .13);--purple-border: rgba(138, 130, 201, .3);--lane-0: #1f9d72;--lane-1: #5683c4;--lane-2: #c06b95;--lane-3: #c1813f;--lane-4: #8a82c9;--lane-5: #3f9d8a;--lane-6: #b39a3c;--lane-7: #a76eb0;--radius-sm: 4px;--radius-md: 7px;--radius-lg: 11px;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .4);--shadow-md: 0 4px 12px rgba(0, 0, 0, .45);--shadow-lg: 0 14px 40px rgba(0, 0, 0, .55);--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-5: 1.25rem;--space-6: 1.5rem;--space-8: 2rem;--ease-out: cubic-bezier(.2, .8, .2, 1);--duration-fast: .11s;--duration-med: .22s;--duration-slow: .4s;--topbar-height: 52px;--drawer-width: 420px;--font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", ui-monospace, monospace;--font-sans: "Inter", "Segoe UI", system-ui, sans-serif}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html,body,#root{height:100%;width:100%;overflow:hidden}body{font-family:var(--font-sans);font-size:14px;line-height:1.5;background:var(--bg-0);color:var(--fg-0);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}button{font-family:inherit;cursor:pointer;border:none;background:none;color:inherit}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code,kbd,pre{font-family:var(--font-mono)}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-strong);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--fg-3)}.chip{display:inline-flex;align-items:center;gap:3px;padding:.1rem .45rem;font-family:var(--font-mono);font-size:.68rem;font-weight:500;border-radius:999px;border:1px solid transparent;white-space:nowrap;line-height:1.6}.chip.green{background:var(--accent-dim);color:var(--accent);border-color:var(--accent-border)}.chip.blue{background:#5683c421;color:var(--lane-1);border-color:#5683c44d}.chip.orange{background:var(--warning-dim);color:var(--warning);border-color:var(--warning-border)}.chip.red{background:var(--danger-dim);color:var(--danger);border-color:var(--danger-border)}.chip.purple{background:var(--purple-dim);color:var(--purple);border-color:var(--purple-border)}.chip.muted{background:var(--bg-2);color:var(--fg-2);border-color:var(--border)}.icon-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:var(--radius-sm);color:var(--fg-2);transition:background var(--duration-fast) var(--ease-out),color var(--duration-fast) var(--ease-out)}.icon-btn:hover{background:var(--bg-3);color:var(--fg-0)}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.app-root{display:flex;flex-direction:column;height:100%;width:100%;overflow:hidden}.app-body{display:flex;flex:1;overflow:hidden;min-height:0}