@memfork/cli 0.1.33 → 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,52 +55,82 @@ 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
- });
59
- }
60
- /**
61
- * Per-namespace cache + global rate-limit backoff. The relayer caps usage at
62
- * 500 weighted-requests/hour, so we must avoid redundant calls:
63
- *
64
- * • Cache each namespace's recall result for CACHE_TTL_MS — the /api/facts
65
- * and /api/history endpoints both recall the same namespace, and the UI
66
- * polls on a timer, so most requests are served from cache for free.
67
- * • On a 429, parse retry_after_seconds and refuse ALL relayer calls until
68
- * it elapses, serving stale cache instead. This stops the polling loop
69
- * from continually re-arming the ban.
70
- */
58
+ // ─── Rate-limit state ─────────────────────────────────────────────────────────
71
59
  const CACHE_TTL_MS = 60_000;
60
+ const RECALL_LIMIT = 50;
72
61
  const recallCache = new Map();
73
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 */ }
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
+ }
74
108
  function parseRetryAfterMs(err) {
75
109
  const m = err.match(/retry_after_seconds"?\s*:\s*(\d+)/);
76
110
  const secs = m ? Number(m[1]) : 300;
77
111
  return secs * 1_000;
78
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) ────────────────────────────────
79
120
  /**
80
- * Recall entries from a MemWal namespace using the SDK (signed requests),
81
- * with caching and 429 backoff. A single broad query at a high limit returns
82
- * the whole namespace, since recall returns the top-`limit` nearest entries
83
- * and namespaces typically hold fewer than `limit` commits.
121
+ * Recall entries from a MemWal namespace. Uses the SDK so requests are
122
+ * properly signed (plain Bearer tokens are rejected by the relayer).
123
+ *
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.
84
128
  */
85
- async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
129
+ async function memwalRecall(relayer, key, accountId, namespace, force = false) {
86
130
  const now = Date.now();
87
131
  const cached = recallCache.get(namespace);
88
- // Fresh cache hit no relayer call needed.
89
- if (cached && now - cached.ts < CACHE_TTL_MS)
132
+ if (!force && cached && now - cached.ts < CACHE_TTL_MS)
90
133
  return cached.data;
91
- // In a rate-limit backoff window — serve stale cache (or empty), don't call.
92
134
  if (now < rateLimitedUntil)
93
135
  return cached?.data ?? [];
94
136
  const mw = MemWal.create({ key, accountId, serverUrl: relayer, namespace });
@@ -97,7 +139,7 @@ async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
97
139
  try {
98
140
  const result = await mw.recall({
99
141
  query: "facts decisions conventions setup errors architecture memory",
100
- limit,
142
+ limit: RECALL_LIMIT,
101
143
  });
102
144
  for (const r of result.results) {
103
145
  const blobId = String(r.blob_id ?? "");
@@ -114,66 +156,74 @@ async function memwalRecall(relayer, key, accountId, namespace, limit = 200) {
114
156
  if (msg.includes("429")) {
115
157
  const backoff = parseRetryAfterMs(msg);
116
158
  rateLimitedUntil = Date.now() + backoff;
117
- console.warn(`[memforks] MemWal rate limit hit — pausing relayer calls for ${Math.round(backoff / 1000)}s, serving cached data.`);
159
+ persistBackoff(rateLimitedUntil);
160
+ console.warn(`[memforks] Rate limit hit — pausing relayer calls for ${Math.round(backoff / 1000)}s.`);
118
161
  }
119
- // Serve stale cache if we have it; otherwise empty.
120
162
  return cached?.data ?? [];
121
163
  }
122
164
  }
123
- async function handleApiFacts(res, url) {
124
- const branch = url.searchParams.get("branch") ?? "main";
165
+ function resolveMemwalCreds(branch) {
125
166
  const project = readProjectConfig();
126
167
  const creds = readCredentials();
127
168
  const treeId = project?.treeId ?? creds.default;
128
169
  const network = (project?.network ?? "mainnet");
129
170
  const stored = treeId ? creds.trees[treeId] : undefined;
130
- if (!stored?.memwalKey || !stored?.memwalAccountId || !treeId) {
131
- 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() });
132
203
  return;
133
204
  }
134
- const relayer = stored.memwalRelayer ?? MEMWAL_CONSTANTS[network].relayer;
135
- const namespace = branchNamespace(treeId, branch);
136
205
  try {
137
- const facts = await memwalRecall(relayer, stored.memwalKey, stored.memwalAccountId, namespace);
138
- json(res, { facts });
206
+ const facts = await memwalRecall(mc.relayer, mc.key, mc.accountId, mc.namespace(branch), force);
207
+ json(res, { facts, ...rateLimitStatus() });
139
208
  }
140
209
  catch (e) {
141
- json(res, { facts: [], error: String(e) });
210
+ json(res, { facts: [], error: String(e), ...rateLimitStatus() });
142
211
  }
143
212
  }
144
- /**
145
- * GET /api/history?branch=<name>&limit=<n>
146
- *
147
- * Returns all off-chain CommitPayload objects stored in MemWal for this branch,
148
- * sorted oldest-first. Each entry includes the MemWal blob_id plus the parsed
149
- * payload fields that the UI needs (branch, author, ts_ms, delta, parent_blob_ids).
150
- *
151
- * The browser cannot call MemWal directly (SEAL-encrypted, key lives server-side),
152
- * so this endpoint acts as the commit-history proxy.
153
- */
154
213
  async function handleApiHistory(res, url) {
155
214
  const branch = url.searchParams.get("branch") ?? "main";
156
- const limit = Math.min(Number(url.searchParams.get("limit") ?? "500"), 1000);
157
- const project = readProjectConfig();
158
- const creds = readCredentials();
159
- const treeId = project?.treeId ?? creds.default;
160
- const network = (project?.network ?? "mainnet");
161
- const stored = treeId ? creds.trees[treeId] : undefined;
162
- if (!stored?.memwalKey || !stored?.memwalAccountId || !treeId) {
163
- json(res, { commits: [] });
215
+ const force = url.searchParams.get("force") === "1";
216
+ const mc = resolveMemwalCreds(branch);
217
+ if (!mc) {
218
+ json(res, { commits: [], ...rateLimitStatus() });
164
219
  return;
165
220
  }
166
- const relayer = stored.memwalRelayer ?? MEMWAL_CONSTANTS[network].relayer;
167
- const namespace = branchNamespace(treeId, branch);
168
221
  try {
169
- 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);
170
223
  const commits = results.flatMap((entry) => {
171
- const blobId = entry.blob_id;
172
- const text = entry.text;
173
- // Try to parse the stored text as a CommitPayload JSON.
174
224
  let payload = null;
175
225
  try {
176
- payload = JSON.parse(text);
226
+ payload = JSON.parse(entry.text);
177
227
  }
178
228
  catch {
179
229
  return [];
@@ -181,32 +231,29 @@ async function handleApiHistory(res, url) {
181
231
  if (payload["type"] !== "commit")
182
232
  return [];
183
233
  return [{
184
- blob_id: blobId,
234
+ blob_id: entry.blob_id,
185
235
  branch: String(payload["branch"] ?? branch),
186
236
  ts_ms: Number(payload["ts_ms"] ?? 0),
187
237
  parent_blob_ids: payload["parent_blob_ids"] ?? [],
188
238
  parent_blob_hashes: payload["parent_blob_hashes"] ?? [],
189
- // Extract readable facts from the delta.
190
239
  message: (() => {
191
240
  const delta = payload["delta"];
192
241
  const facts = delta?.["facts"];
193
- return facts?.length ? facts[0] : `commit ${blobId.slice(0, 8)}`;
242
+ return facts?.length ? facts[0] : `commit ${entry.blob_id.slice(0, 8)}`;
194
243
  })(),
195
244
  delta: payload["delta"] ?? {},
196
245
  }];
197
246
  });
198
- // Sort oldest-first by ts_ms.
199
247
  commits.sort((a, b) => a.ts_ms - b.ts_ms);
200
- json(res, { commits, branch });
248
+ json(res, { commits, branch, ...rateLimitStatus() });
201
249
  }
202
250
  catch (e) {
203
- json(res, { commits: [], error: String(e) });
251
+ json(res, { commits: [], error: String(e), ...rateLimitStatus() });
204
252
  }
205
253
  }
254
+ // ─── Static serving ───────────────────────────────────────────────────────────
206
255
  function serveStatic(res, distDir, urlPath) {
207
- // Resolve the requested file path.
208
256
  let filePath = path.join(distDir, urlPath);
209
- // SPA fallback: no extension or file not found → serve index.html.
210
257
  if (!path.extname(filePath) || !fs.existsSync(filePath)) {
211
258
  filePath = path.join(distDir, "index.html");
212
259
  }
@@ -219,17 +266,16 @@ function serveStatic(res, distDir, urlPath) {
219
266
  const isImmutable = urlPath.startsWith("/assets/");
220
267
  res.writeHead(200, {
221
268
  "Content-Type": mimeType,
222
- "Cache-Control": isImmutable
223
- ? "public, max-age=31536000, immutable"
224
- : "no-cache",
269
+ "Cache-Control": isImmutable ? "public, max-age=31536000, immutable" : "no-cache",
225
270
  "Access-Control-Allow-Origin": "*",
226
271
  });
227
272
  fs.createReadStream(filePath).pipe(res);
228
273
  }
274
+ // ─── Server factory ───────────────────────────────────────────────────────────
229
275
  export function startUiServer(distDir, port = 4242) {
276
+ loadPersistedBackoff();
230
277
  const server = http.createServer((req, res) => {
231
278
  const url = new URL(req.url ?? "/", `http://localhost:${port}`);
232
- // CORS pre-flight.
233
279
  if (req.method === "OPTIONS") {
234
280
  res.writeHead(204, { "Access-Control-Allow-Origin": "*" });
235
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.33",
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}