@memfork/cli 0.1.36 → 0.1.38

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
@@ -25,7 +25,7 @@ const { version } = createRequire(import.meta.url)("../package.json");
25
25
  import { cmdInit } from "./commands/init.js";
26
26
  import { cmdDoctor, cmdDoctorEnv } from "./commands/doctor.js";
27
27
  import { cmdInstall } from "./commands/install.js";
28
- import { cmdStatus, cmdLog, cmdRecall, cmdCommit, cmdMerge, cmdProposals, cmdResolverCreate, cmdPrComment, cmdUi, cmdShow, cmdDiff, cmdDelegates, cmdGrant, cmdGrantMemwal, cmdRevoke, cmdBranch, cmdCheckout, } from "./commands/ops.js";
28
+ import { cmdStatus, cmdLog, cmdRecall, cmdCommit, cmdCat, cmdMerge, cmdProposals, cmdResolverCreate, cmdPrComment, cmdUi, cmdShow, cmdDiff, cmdDelegates, cmdGrant, cmdGrantMemwal, cmdRevoke, cmdBranch, cmdCheckout, } from "./commands/ops.js";
29
29
  import { cmdJoin } from "./commands/join.js";
30
30
  const program = new Command();
31
31
  program
@@ -87,7 +87,19 @@ program
87
87
  .option("-f, --facts <facts...>", "one or more fact strings")
88
88
  .option("--from-response <text>", "extract facts from a full response text")
89
89
  .option("--auto-extract", "use LLM to extract durable facts (requires --from-response)")
90
- .action(wrap((opts) => cmdCommit(opts)));
90
+ .option("--file <path>", "attach a file as a Walrus artifact (repeatable). Requires artifacts.enabled = true in config.", (v, acc) => [...acc, v], [])
91
+ .option("--epochs <n>", "Walrus storage epochs for this commit's artifacts (overrides config default)", parseInt)
92
+ .action(wrap((opts) => cmdCommit({
93
+ ...opts,
94
+ files: opts.file?.map((p) => ({ filePath: p, epochs: opts.epochs })),
95
+ })));
96
+ program
97
+ .command("cat <blobId>")
98
+ .description("retrieve an artifact from Walrus by its blob ID")
99
+ .option("-o, --output <path>", "write bytes to this file (default: print to stdout)")
100
+ .option("--sha256 <hex>", "verify integrity against this SHA-256 hex digest")
101
+ .option("--network <name>", "Walrus network: mainnet (default) or testnet")
102
+ .action(wrap((blobId, opts) => cmdCat(blobId, opts)));
91
103
  program
92
104
  .command("merge <from> <into>")
93
105
  .description("merge memory from one branch into another")
@@ -175,6 +175,42 @@ export async function cmdDoctor() {
175
175
  fix: "Check your network. MemWal read/write will be unavailable.",
176
176
  });
177
177
  }
178
+ // ── 8. Artifact storage (optional) ──────────────────────────────────────────
179
+ if (cfg.artifacts.enabled) {
180
+ // Artifacts require WAL tokens. Fetch the signer's WAL coin balance.
181
+ // The WAL package ID is identical on mainnet and testnet.
182
+ // Source: https://docs.wal.app/docs/network-reference
183
+ const walType = "0x356a26eb9e012a68958082340d4c4116e7f55615cf27affcff209cf0ae544f59::wal::WAL";
184
+ try {
185
+ const addr = client.keypair.toSuiAddress();
186
+ const walBalance = await client.suiClient.getBalance({ owner: addr, coinType: walType });
187
+ const walAmount = Number(walBalance.totalBalance) / 1e9;
188
+ const walLow = walAmount < 0.5;
189
+ checks.push({
190
+ label: "Artifact storage (WAL balance)",
191
+ status: walLow ? "warn" : "ok",
192
+ detail: `${walAmount.toFixed(4)} WAL (artifacts.enabled = true)`,
193
+ fix: walLow
194
+ ? `Fund ${addr.slice(0, 10)}… with WAL on ${cfg.network}. See docs/architecture/artifacts.md.`
195
+ : undefined,
196
+ });
197
+ }
198
+ catch {
199
+ checks.push({
200
+ label: "Artifact storage (WAL balance)",
201
+ status: "warn",
202
+ detail: "could not fetch WAL balance",
203
+ fix: "Check your Sui RPC connection.",
204
+ });
205
+ }
206
+ }
207
+ else {
208
+ checks.push({
209
+ label: "Artifact storage",
210
+ status: "skip",
211
+ detail: "disabled (set artifacts.enabled = true to enable Walrus artifact persistence)",
212
+ });
213
+ }
178
214
  // ── Print all checks ─────────────────────────────────────────────────────────
179
215
  console.log("");
180
216
  checks.forEach(printCheck);
@@ -18,6 +18,27 @@ export declare function cmdCommit(opts: {
18
18
  facts?: string[];
19
19
  fromResponse?: string;
20
20
  autoExtract?: boolean;
21
+ /**
22
+ * Files to persist as Walrus artifacts attached to this commit.
23
+ * Each entry is { filePath: local FS path, mime?: MIME type, epochs?: override }.
24
+ * Requires artifacts.enabled = true in config and a WAL-funded keypair.
25
+ */
26
+ files?: Array<{
27
+ filePath: string;
28
+ mime?: string;
29
+ epochs?: number;
30
+ }>;
31
+ }): Promise<void>;
32
+ /**
33
+ * `memfork cat <blobId>` — retrieve an artifact blob from Walrus.
34
+ *
35
+ * Writes bytes to `--output <path>` or, when no output is specified, prints
36
+ * the blob as UTF-8 text. Pass `--sha256 <hex>` to verify integrity.
37
+ */
38
+ export declare function cmdCat(blobId: string, opts?: {
39
+ output?: string;
40
+ sha256?: string;
41
+ network?: string;
21
42
  }): Promise<void>;
22
43
  export declare function cmdMerge(from: string, into: string, opts: {
23
44
  resolver?: string;
@@ -129,18 +129,109 @@ export async function cmdCommit(opts) {
129
129
  console.error(chalk.red("No facts to commit. Pass --facts or --from-response."));
130
130
  process.exit(1);
131
131
  }
132
- const { blobId } = await client.commit(branch, { facts, message: opts.message });
133
- const out = { blobId, branch };
132
+ // Build artifact descriptors from --file flags.
133
+ const artifacts = [];
134
+ if (opts.files && opts.files.length > 0) {
135
+ if (!cfg.artifacts.enabled) {
136
+ console.error(chalk.red("Artifact storage is disabled.") +
137
+ chalk.dim("\n Set artifacts.enabled = true in .memfork/config.json and fund your signer with WAL.\n" +
138
+ " See docs/architecture/artifacts.md for setup instructions."));
139
+ process.exit(1);
140
+ }
141
+ for (const f of opts.files) {
142
+ if (!fs.existsSync(f.filePath)) {
143
+ console.error(chalk.red(`File not found: ${f.filePath}`));
144
+ process.exit(1);
145
+ }
146
+ const bytes = new Uint8Array(fs.readFileSync(f.filePath));
147
+ artifacts.push({
148
+ path: path.basename(f.filePath),
149
+ bytes,
150
+ mime: f.mime,
151
+ epochs: f.epochs,
152
+ });
153
+ }
154
+ }
155
+ const { blobId, artifacts: refs } = await client.commit(branch, {
156
+ facts,
157
+ message: opts.message,
158
+ ...(artifacts.length > 0 ? { artifacts } : {}),
159
+ });
160
+ const out = { blobId, branch, artifacts: refs };
134
161
  if (process.stdout.isTTY) {
135
162
  console.log("");
136
163
  console.log(chalk.green("✓") + " Committed to " + chalk.bold(branch));
137
164
  console.log(chalk.dim(` blob: ${blobId}`));
165
+ if (refs.length > 0) {
166
+ console.log("");
167
+ console.log(chalk.bold(" Artifacts:"));
168
+ for (const ref of refs) {
169
+ console.log(` ${chalk.cyan(ref.path)} ` +
170
+ chalk.dim(`${(ref.size / 1024).toFixed(1)} KiB `) +
171
+ chalk.dim(`blob: ${ref.blobId.slice(0, 16)}… `) +
172
+ chalk.dim(`sha256: ${ref.sha256.slice(0, 16)}…`));
173
+ console.log(chalk.dim(` Retrieve with: `) +
174
+ chalk.white(`memfork cat ${ref.blobId} --output ${ref.path} --sha256 ${ref.sha256}`));
175
+ }
176
+ }
138
177
  console.log("");
139
178
  }
140
179
  else {
141
180
  console.log(JSON.stringify(out));
142
181
  }
143
182
  }
183
+ // ─── cat ──────────────────────────────────────────────────────────────────────
184
+ /**
185
+ * `memfork cat <blobId>` — retrieve an artifact blob from Walrus.
186
+ *
187
+ * Writes bytes to `--output <path>` or, when no output is specified, prints
188
+ * the blob as UTF-8 text. Pass `--sha256 <hex>` to verify integrity.
189
+ */
190
+ export async function cmdCat(blobId, opts = {}) {
191
+ // Basic blobId sanity: Walrus blob IDs are base64url (~43 chars) or integer strings.
192
+ if (!blobId || blobId.length < 8 || /\s/.test(blobId)) {
193
+ console.error(chalk.red(`Invalid blob ID: "${blobId}". Expected a Walrus blob ID (base64url).`));
194
+ process.exit(1);
195
+ }
196
+ const { getArtifact, ArtifactStorageError } = await import("@memfork/core");
197
+ const network = (opts.network ?? resolveConfig().network ?? "mainnet");
198
+ process.stdout.write(chalk.dim(`Fetching blob ${blobId.slice(0, 16)}… from Walrus (${network}) `));
199
+ // sha256 is optional: when omitted, getArtifact skips the integrity check.
200
+ let bytes;
201
+ try {
202
+ bytes = await getArtifact({ blobId, sha256: opts.sha256 ?? "" }, network);
203
+ }
204
+ catch (err) {
205
+ console.log(chalk.red("failed"));
206
+ if (err instanceof ArtifactStorageError) {
207
+ // Structured errors: print the reason-specific message cleanly.
208
+ console.error("\n" + chalk.red(err.message) + "\n");
209
+ if (err.reason === "not_found") {
210
+ console.error(chalk.dim(" Tip: re-run with --sha256 to verify integrity once found,\n" +
211
+ " or check the original commit with: memfork log"));
212
+ }
213
+ else if (err.reason === "integrity") {
214
+ console.error(chalk.yellow(" Warning: the blob exists but its contents may be corrupted or tampered with."));
215
+ }
216
+ }
217
+ else {
218
+ console.error(chalk.red(String(err)));
219
+ }
220
+ process.exit(1);
221
+ }
222
+ console.log(chalk.green("done") + chalk.dim(` (${bytes.length} bytes)`));
223
+ if (opts.output) {
224
+ fs.writeFileSync(opts.output, bytes);
225
+ console.log(chalk.green("✓") + " Saved to " + chalk.bold(opts.output));
226
+ }
227
+ else {
228
+ // Print as UTF-8.
229
+ process.stdout.write(new TextDecoder().decode(bytes));
230
+ if (!bytes[bytes.length - 1] || bytes[bytes.length - 1] !== 10) {
231
+ process.stdout.write("\n");
232
+ }
233
+ }
234
+ }
144
235
  // ─── merge ────────────────────────────────────────────────────────────────────
145
236
  export async function cmdMerge(from, into, opts) {
146
237
  const cfg = resolveConfig();
@@ -230,18 +230,18 @@ async function handleApiHistory(res, url) {
230
230
  }
231
231
  if (payload["type"] !== "commit")
232
232
  return [];
233
+ const delta = payload["delta"];
234
+ const facts = delta?.["facts"];
235
+ const artifacts = delta?.["artifacts"];
233
236
  return [{
234
237
  blob_id: entry.blob_id,
235
238
  branch: String(payload["branch"] ?? branch),
236
239
  ts_ms: Number(payload["ts_ms"] ?? 0),
237
240
  parent_blob_ids: payload["parent_blob_ids"] ?? [],
238
241
  parent_blob_hashes: payload["parent_blob_hashes"] ?? [],
239
- message: (() => {
240
- const delta = payload["delta"];
241
- const facts = delta?.["facts"];
242
- return facts?.length ? facts[0] : `commit ${entry.blob_id.slice(0, 8)}`;
243
- })(),
242
+ message: facts?.length ? facts[0] : `commit ${entry.blob_id.slice(0, 8)}`,
244
243
  delta: payload["delta"] ?? {},
244
+ ...(artifacts?.length ? { artifacts } : {}),
245
245
  }];
246
246
  });
247
247
  commits.sort((a, b) => a.ts_ms - b.ts_ms);
package/dist/config.d.ts CHANGED
@@ -49,6 +49,20 @@ export interface ProjectConfig {
49
49
  packageId?: string;
50
50
  /** Gas sponsor URL. When set, all on-chain txs are sponsored (no SUI balance needed). */
51
51
  sponsorUrl?: string;
52
+ /**
53
+ * Opt-in artifact storage config. When enabled, commit() can persist files
54
+ * as standalone Walrus blobs. The signer keypair must hold WAL + SUI.
55
+ * Overrideable via MEMFORK_ARTIFACTS_* env vars.
56
+ */
57
+ artifacts?: {
58
+ enabled?: boolean;
59
+ /** Walrus epochs to purchase. Default: 12. */
60
+ epochs?: number;
61
+ /** Max artifact size in bytes. Default: 10 MiB. */
62
+ maxBytes?: number;
63
+ /** Optional Walrus upload relay URL. */
64
+ uploadRelayUrl?: string;
65
+ };
52
66
  }
53
67
  export interface TreeCredential {
54
68
  /** Ed25519 private key in bech32 suiprivkey1… format. */
@@ -77,6 +91,13 @@ export interface ResolvedConfig {
77
91
  rpcUrl?: string;
78
92
  packageId?: string;
79
93
  sponsorUrl?: string;
94
+ /** Resolved artifact storage config. enabled=false by default. */
95
+ artifacts: {
96
+ enabled: boolean;
97
+ epochs: number;
98
+ maxBytes: number;
99
+ uploadRelayUrl?: string;
100
+ };
80
101
  }
81
102
  export declare function projectConfigPath(cwd?: string): string;
82
103
  export declare function credentialsPath(): string;
@@ -113,4 +134,10 @@ export declare function toClientConfig(r: ResolvedConfig): {
113
134
  delegateKey: string;
114
135
  serverUrl: string;
115
136
  };
137
+ artifacts: {
138
+ enabled: boolean;
139
+ epochs: number;
140
+ maxBytes: number;
141
+ uploadRelayUrl?: string;
142
+ };
116
143
  };
package/dist/config.js CHANGED
@@ -167,6 +167,17 @@ export function resolveConfig(opts = {}) {
167
167
  const network = (env['MEMFORK_NETWORK'] ??
168
168
  project?.network ??
169
169
  'mainnet');
170
+ // ── Artifact storage config ─────────────────────────────────────────────────
171
+ const artifactsEnabled = env['MEMFORK_ARTIFACTS_ENABLED'] === 'true' ||
172
+ (env['MEMFORK_ARTIFACTS_ENABLED'] === undefined &&
173
+ (project?.artifacts?.enabled ?? false));
174
+ const artifactsEpochs = env['MEMFORK_ARTIFACTS_EPOCHS'] != null
175
+ ? parseInt(env['MEMFORK_ARTIFACTS_EPOCHS'], 10)
176
+ : (project?.artifacts?.epochs ?? 12);
177
+ const artifactsMaxBytes = env['MEMFORK_ARTIFACTS_MAX_BYTES'] != null
178
+ ? parseInt(env['MEMFORK_ARTIFACTS_MAX_BYTES'], 10)
179
+ : (project?.artifacts?.maxBytes ?? 10 * 1024 * 1024);
180
+ const artifactsUploadRelayUrl = env['MEMFORK_ARTIFACTS_UPLOAD_RELAY_URL'] ?? project?.artifacts?.uploadRelayUrl;
170
181
  return {
171
182
  treeId,
172
183
  privateKey,
@@ -181,6 +192,12 @@ export function resolveConfig(opts = {}) {
181
192
  packageId: env['MEMFORK_PACKAGE_ID'] ?? project?.packageId,
182
193
  sponsorUrl: env['MEMFORK_SPONSOR_URL'] ??
183
194
  project?.sponsorUrl,
195
+ artifacts: {
196
+ enabled: artifactsEnabled,
197
+ epochs: artifactsEpochs,
198
+ maxBytes: artifactsMaxBytes,
199
+ uploadRelayUrl: artifactsUploadRelayUrl,
200
+ },
184
201
  };
185
202
  }
186
203
  /**
@@ -200,5 +217,6 @@ export function toClientConfig(r) {
200
217
  delegateKey: r.memwalKey,
201
218
  serverUrl: r.memwalRelayer,
202
219
  },
220
+ artifacts: r.artifacts,
203
221
  };
204
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memfork/cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "MemForks CLI — init, commit, recall, merge, install plugins",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,33 +2,57 @@
2
2
  name: memory-fork
3
3
  description: >-
4
4
  Fork agent memory into parallel branches to explore competing hypotheses.
5
- Use when the user asks to explore multiple approaches, compare options, or
6
- says phrases like "explore both", "try two paths", "compare A vs B".
5
+ Use whenever a task has two or more genuinely competing approaches worth
6
+ testing in isolation whether the user names them explicitly ("explore
7
+ both", "compare A vs B") OR asks an open decision question (e.g. "what's the
8
+ fastest way to X?") where you can identify two credible, mutually-exclusive
9
+ solutions.
7
10
  ---
8
11
 
9
12
  # Memory Fork
10
13
 
11
- When you detect a multi-hypothesis prompt the user wants to explore two or
12
- more competing approaches fork the MemForks memory tree so each path stays
13
- isolated. Never collapse competing ideas into a single stream.
14
+ Fork the MemForks memory tree whenever a task has two or more genuinely
15
+ competing approaches that deserve to be tested in isolation. Never collapse
16
+ competing ideas into a single stream. The decision to fork is **yours** — you
17
+ do not need the user to ask for branching; you need only to recognise that two
18
+ real alternatives exist.
14
19
 
15
20
  ## When to trigger
16
21
 
17
- Trigger this skill when the user prompt contains signals like:
22
+ Trigger this skill in either case:
23
+
24
+ **Explicit multi-hypothesis prompts** — the user names the alternatives:
18
25
  - "explore both paths" / "try both" / "compare X and Y"
19
26
  - "what if we did X instead of Y" (two real alternatives)
20
27
  - "should we do A or B?" (genuine decision fork, not a rhetorical question)
21
- - Any request to investigate multiple competing solutions side-by-side
28
+
29
+ **Implicit decision questions** — the user asks an open question and *you*
30
+ identify two credible competing approaches:
31
+ - "What's the fastest way to cut our auth latency?" → e.g. Redis caching vs.
32
+ bcrypt cost tuning
33
+ - "How should we make this query cheaper?" → e.g. add an index vs. denormalise
34
+ - Any open "how do we improve / fix / speed up X?" where two distinct,
35
+ mutually-exclusive strategies are worth measuring before committing
36
+
37
+ **Do NOT fork** when there is only one sensible approach, when the question is
38
+ rhetorical or informational, or when the alternatives are trivial variations of
39
+ each other. Forking is for real, competing, separately-testable hypotheses —
40
+ not for every question.
41
+
42
+ When you fork off an implicit question, name the two approaches you inferred so
43
+ the user can see your reasoning (see the announce format below).
22
44
 
23
45
  ## Procedure
24
46
 
25
47
  ### 1. Announce the fork
26
48
 
27
- Print exactly:
49
+ Name the two approaches you identified, then announce the fork. Print exactly
50
+ this shape (substitute the real approaches and current branch):
28
51
 
29
52
  ```
30
- [memforks] Multi-hypothesis detected.
31
- [memforks] Forking agent memory from <current-branch>@HEAD
53
+ [memforks] Two viable approaches detected — <approach A> vs. <approach B>.
54
+ [memforks] Branching memory so each can be tested without contamination.
55
+ [memforks] Forking from <current-branch>@HEAD
32
56
  ```
33
57
 
34
58
  Then list the branches you will create, one per hypothesis:
@@ -39,7 +63,11 @@ Then list the branches you will create, one per hypothesis:
39
63
  ```
40
64
 
41
65
  Use kebab-case branch names derived from the hypothesis (e.g. `dev/redis-first`,
42
- `dev/bcrypt-cost`, `dev/approach-a`).
66
+ `dev/bcrypt-first`, `dev/approach-a`).
67
+
68
+ > Naming the two approaches in the first line is what makes the fork read as
69
+ > *your* decision rather than a command you were handed — especially when the
70
+ > user asked an open question and never named the alternatives.
43
71
 
44
72
  ### 2. Create the branches
45
73
 
@@ -55,11 +55,30 @@ Do **not** save: current task state, in-progress work, temporary findings.
55
55
 
56
56
  ## Forking — exploring competing approaches
57
57
 
58
- When the user asks you to explore multiple competing approaches (e.g. "try both",
59
- "compare X vs Y", "explore both paths"), fork the memory tree so each hypothesis
60
- stays isolated. Never collapse competing ideas into a single branch.
61
-
62
- **Step 1 announce and create branches:**
58
+ Fork the memory tree whenever a task has two or more genuinely competing
59
+ approaches worth testing in isolation and the decision to fork is **yours**.
60
+ Trigger in either case:
61
+
62
+ - **Explicit**the user names the alternatives ("try both", "compare X vs Y",
63
+ "explore both paths", "should we do A or B?").
64
+ - **Implicit** — the user asks an open decision question ("what's the fastest way
65
+ to cut our auth latency?", "how do we make this cheaper?") and *you* identify
66
+ two credible, mutually-exclusive strategies worth measuring separately. You do
67
+ not need the user to ask for branching; you only need to recognise that two
68
+ real alternatives exist.
69
+
70
+ Do **not** fork when there is one sensible approach, the question is rhetorical
71
+ or informational, or the alternatives are trivial variations. Never collapse
72
+ genuinely competing ideas into a single branch.
73
+
74
+ **Step 1 — name the approaches, then announce and create branches.** When you
75
+ fork off an implicit question, name the two approaches you inferred so the user
76
+ sees your reasoning:
77
+
78
+ ```text
79
+ [memforks] Two viable approaches detected — <approach A> vs. <approach B>.
80
+ [memforks] Branching memory so each can be tested without contamination.
81
+ ```
63
82
 
64
83
  ```bash
65
84
  memfork branch dev/<hypothesis-a>
@@ -68,7 +87,7 @@ memfork branch dev/<hypothesis-b>
68
87
 
69
88
  Both fork from the current Git branch by default; add `--from <branch>` to fork from another.
70
89
 
71
- Use short kebab-case names (`dev/redis-first`, `dev/bcrypt-cost`).
90
+ Use short kebab-case names (`dev/redis-first`, `dev/bcrypt-first`).
72
91
 
73
92
  **Step 2 — investigate each path and commit evidence:**
74
93
 
@@ -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}.time-scrubber{padding:8px 16px 10px;border-bottom:1px solid var(--border);background:var(--surface-0);animation:ts-slide-in .12s var(--ease-out) both;overflow:hidden}@keyframes ts-slide-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.time-scrubber-track-row{display:flex;align-items:center;gap:8px}.time-scrubber-past-label,.time-scrubber-live-label{font-size:.68rem;color:var(--fg-3);white-space:nowrap;flex-shrink:0}.time-scrubber-live-label.active{color:var(--accent)}.time-scrubber-input{flex:1;-moz-appearance:none;appearance:none;-webkit-appearance:none;height:3px;border-radius:2px;background:linear-gradient(to right,var(--accent) 0%,var(--accent) calc(var(--fill-pct, 50) * 1%),var(--border-strong) calc(var(--fill-pct, 50) * 1%),var(--border-strong) 100%);outline:none;cursor:pointer}.time-scrubber-input::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--accent);border:2px solid var(--surface-0);box-shadow:0 0 0 1px var(--accent);cursor:grab;transition:transform 80ms var(--ease-out)}.time-scrubber-input:active::-webkit-slider-thumb{cursor:grabbing;transform:scale(1.2)}.time-scrubber-input::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--accent);border:2px solid var(--surface-0);box-shadow:0 0 0 1px var(--accent);cursor:grab}.time-scrubber-label{margin-top:5px;font-size:.72rem;color:var(--fg-2);display:flex;align-items:center;gap:5px;min-height:1.2em;font-variant-numeric:tabular-nums}.time-scrubber-label--live{color:var(--accent)}.time-scrubber-live-dot{width:6px;height:6px;border-radius:50%;background:var(--accent);display:inline-block;flex-shrink:0;animation:scrubber-pulse 2s ease-in-out infinite}.time-scrubber-at-glyph{opacity:.6;flex-shrink:0}@keyframes scrubber-pulse{0%,to{opacity:1}50%{opacity:.4}}.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-tt-btn{position:relative;display:flex;align-items:center;justify-content:center;width:26px;height:26px;flex-shrink:0;border:none;border-radius:6px;background:transparent;color:var(--fg-3);cursor:pointer;transition:background .12s,color .12s}.memory-tt-btn:hover,.memory-tt-btn.open{background:var(--surface-1);color:var(--fg-1)}.memory-tt-btn.active{color:var(--accent)}.memory-tt-badge{position:absolute;top:4px;right:4px;width:5px;height:5px;border-radius:50%;background:var(--accent);border:1px solid var(--bg-0);pointer-events:none}.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-tt-btn{margin-left:auto;position:relative;display:flex;align-items:center;justify-content:center;width:26px;height:26px;border:none;border-radius:6px;background:transparent;color:var(--fg-3);cursor:pointer;transition:background .12s,color .12s;flex-shrink:0}.history-tt-btn:hover,.history-tt-btn.open{background:var(--surface-1);color:var(--fg-1)}.history-tt-btn.active{color:var(--accent)}.history-tt-badge{position:absolute;top:4px;right:4px;width:5px;height:5px;border-radius:50%;background:var(--accent);border:1px solid var(--bg-0);pointer-events:none}.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}