@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 +0 -0
- package/dist/commands/ui-server.d.ts +18 -8
- package/dist/commands/ui-server.js +154 -82
- package/dist/index.js +0 -0
- package/package.json +1 -1
- package/ui/assets/index-DSfS462x.css +1 -0
- package/ui/assets/{index-hWCeym5B.js → index-vHgHmMrf.js} +17 -17
- package/ui/assets/index-vHgHmMrf.js.map +1 -0
- package/ui/index.html +2 -2
- package/ui/assets/index-BPumM0C9.css +0 -1
- package/ui/assets/index-hWCeym5B.js.map +0 -1
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
|
|
5
|
-
* exposes
|
|
6
|
-
*
|
|
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
|
|
10
|
-
* GET /api/
|
|
11
|
-
* GET
|
|
12
|
-
* GET
|
|
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
|
|
5
|
-
* exposes
|
|
6
|
-
*
|
|
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
|
|
10
|
-
* GET /api/
|
|
11
|
-
* GET
|
|
12
|
-
* GET
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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,
|
|
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
|
-
|
|
91
|
-
if (
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
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,
|
|
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:
|
|
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 ${
|
|
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
|
@@ -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}
|