@jefuriiij/synthra 0.6.0 → 0.7.0

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/index.js CHANGED
@@ -18,7 +18,7 @@ var init_package = __esm({
18
18
  "package.json"() {
19
19
  package_default = {
20
20
  name: "@jefuriiij/synthra",
21
- version: "0.6.0",
21
+ version: "0.7.0",
22
22
  publishConfig: {
23
23
  access: "public"
24
24
  },
@@ -145,12 +145,230 @@ function isFree(port) {
145
145
  });
146
146
  }
147
147
 
148
+ // src/dashboard/arsenal.ts
149
+ import { readFile, readdir } from "fs/promises";
150
+ import { homedir } from "os";
151
+ import { basename, dirname, join } from "path";
152
+ var DESC_MAX = 300;
153
+ var TOOLS_MAX = 200;
154
+ async function readText(path) {
155
+ try {
156
+ return await readFile(path, "utf8");
157
+ } catch {
158
+ return null;
159
+ }
160
+ }
161
+ async function readJson(path) {
162
+ const text = await readText(path);
163
+ if (text === null) return null;
164
+ try {
165
+ return JSON.parse(text);
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ async function listNames(dir) {
171
+ try {
172
+ return await readdir(dir);
173
+ } catch {
174
+ return [];
175
+ }
176
+ }
177
+ function clip(s, max) {
178
+ const t = s.trim();
179
+ return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
180
+ }
181
+ function parseFrontmatter(md) {
182
+ const m = md.match(/^?\s*---\r?\n([\s\S]*?)\r?\n---/);
183
+ if (!m) return {};
184
+ const lines = (m[1] ?? "").split(/\r?\n/);
185
+ const out = {};
186
+ for (let i = 0; i < lines.length; i++) {
187
+ const line = lines[i] ?? "";
188
+ const kv = line.match(/^([A-Za-z][\w-]*):\s?(.*)$/);
189
+ if (!kv) continue;
190
+ const key = kv[1] ?? "";
191
+ if (!key) continue;
192
+ let val = kv[2] ?? "";
193
+ if (val.startsWith('"') && !/[^\\]"\s*$/.test(val.slice(1)) || val === '"') {
194
+ const buf = [val];
195
+ while (i + 1 < lines.length && !/"\s*$/.test(buf[buf.length - 1] ?? "")) {
196
+ i += 1;
197
+ buf.push(lines[i] ?? "");
198
+ }
199
+ val = buf.join(" ");
200
+ }
201
+ val = val.trim().replace(/^["']|["']$/g, "").trim();
202
+ out[key] = val;
203
+ }
204
+ return out;
205
+ }
206
+ function skillItem(fm, fallbackName, scope, source) {
207
+ const meta = {};
208
+ if (fm["argument-hint"]) meta.argument_hint = fm["argument-hint"];
209
+ if (fm["user-invocable"]) meta.user_invocable = fm["user-invocable"];
210
+ return {
211
+ name: fm.name || fallbackName,
212
+ description: clip(fm.description || "", DESC_MAX),
213
+ scope,
214
+ ...source ? { source } : {},
215
+ ...Object.keys(meta).length ? { meta } : {}
216
+ };
217
+ }
218
+ function agentItem(fm, fallbackName, scope, source) {
219
+ const meta = {};
220
+ if (fm.tools) meta.tools = clip(fm.tools, TOOLS_MAX);
221
+ if (fm.model) meta.model = fm.model;
222
+ return {
223
+ name: fm.name || fallbackName,
224
+ description: clip(fm.description || "", DESC_MAX),
225
+ scope,
226
+ ...source ? { source } : {},
227
+ ...Object.keys(meta).length ? { meta } : {}
228
+ };
229
+ }
230
+ async function scanSkillsDir(dir, scope, source, out) {
231
+ for (const name of await listNames(dir)) {
232
+ const md = await readText(join(dir, name, "SKILL.md"));
233
+ if (md === null) continue;
234
+ out.push(skillItem(parseFrontmatter(md), name, scope, source));
235
+ }
236
+ }
237
+ async function scanAgentsDir(dir, scope, source, out) {
238
+ for (const file of await listNames(dir)) {
239
+ if (!file.endsWith(".md")) continue;
240
+ const md = await readText(join(dir, file));
241
+ if (md === null) continue;
242
+ out.push(agentItem(parseFrontmatter(md), basename(file, ".md"), scope, source));
243
+ }
244
+ }
245
+ function mcpItemsFrom(json, scope, source) {
246
+ if (!json || typeof json !== "object") return [];
247
+ const record = json;
248
+ const servers = record.mcpServers && typeof record.mcpServers === "object" ? record.mcpServers : record;
249
+ const items = [];
250
+ for (const [name, raw] of Object.entries(servers)) {
251
+ if (!raw || typeof raw !== "object") continue;
252
+ const cfg = raw;
253
+ const type = typeof cfg.type === "string" ? cfg.type : cfg.command ? "stdio" : "http";
254
+ const url = typeof cfg.url === "string" ? cfg.url.split("?")[0] : typeof cfg.command === "string" ? cfg.command : "";
255
+ const meta = { type };
256
+ if (url) meta.url = url;
257
+ items.push({ name, description: "", scope, ...source ? { source } : {}, meta });
258
+ }
259
+ return items;
260
+ }
261
+ var SCOPE_ORDER = { project: 0, personal: 1, plugin: 2 };
262
+ function sortItems(items) {
263
+ return items.sort((a, b) => {
264
+ if (a.scope !== b.scope) return SCOPE_ORDER[a.scope] - SCOPE_ORDER[b.scope];
265
+ const sa = a.source ?? "";
266
+ const sb = b.source ?? "";
267
+ if (sa !== sb) return sa < sb ? -1 : 1;
268
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
269
+ });
270
+ }
271
+ var cache = null;
272
+ var CACHE_TTL_MS = 15e3;
273
+ async function computeArsenal(projectRoot, homeDir = homedir()) {
274
+ const key = `${projectRoot}\0${homeDir}`;
275
+ const now = Date.now();
276
+ if (cache && cache.key === key && now - cache.at < CACHE_TTL_MS) return cache.data;
277
+ const homeClaude = join(homeDir, ".claude");
278
+ const projClaude = join(projectRoot, ".claude");
279
+ const skills = [];
280
+ const agents = [];
281
+ const mcp = [];
282
+ await scanSkillsDir(join(projClaude, "skills"), "project", void 0, skills);
283
+ await scanSkillsDir(join(homeClaude, "skills"), "personal", void 0, skills);
284
+ await scanAgentsDir(join(projClaude, "agents"), "project", void 0, agents);
285
+ await scanAgentsDir(join(homeClaude, "agents"), "personal", void 0, agents);
286
+ mcp.push(...mcpItemsFrom(await readJson(join(projectRoot, ".mcp.json")), "project", void 0));
287
+ mcp.push(
288
+ ...mcpItemsFrom(
289
+ (await readJson(join(homeDir, ".claude.json")))?.mcpServers,
290
+ "personal",
291
+ void 0
292
+ )
293
+ );
294
+ const installedRaw = await readJson(
295
+ join(homeClaude, "plugins", "installed_plugins.json")
296
+ );
297
+ const pluginsMap = installedRaw?.plugins ?? installedRaw ?? {};
298
+ const settings = await readJson(
299
+ join(homeClaude, "settings.json")
300
+ );
301
+ const enabledMap = settings?.enabledPlugins ?? {};
302
+ let pluginCount = 0;
303
+ {
304
+ for (const [pluginKey, entries] of Object.entries(pluginsMap)) {
305
+ const entry = Array.isArray(entries) ? entries[0] : void 0;
306
+ if (!entry?.installPath) continue;
307
+ pluginCount += 1;
308
+ const pluginName = pluginKey.split("@")[0];
309
+ const enabled = enabledMap[pluginKey] !== false;
310
+ const root = entry.installPath;
311
+ const manifest = await readJson(
312
+ join(root, ".claude-plugin", "plugin.json")
313
+ );
314
+ const agentFiles = /* @__PURE__ */ new Set();
315
+ for (const f of await listNames(join(root, "agents"))) {
316
+ if (f.endsWith(".md")) agentFiles.add(join(root, "agents", f));
317
+ }
318
+ for (const rel of manifest?.agents ?? []) agentFiles.add(join(root, rel));
319
+ const pAgents = [];
320
+ for (const file of agentFiles) {
321
+ const md = await readText(file);
322
+ if (md !== null) pAgents.push(agentItem(parseFrontmatter(md), basename(file, ".md"), "plugin", pluginName));
323
+ }
324
+ const skillMds = /* @__PURE__ */ new Set();
325
+ for (const name of await listNames(join(root, "skills"))) {
326
+ skillMds.add(join(root, "skills", name, "SKILL.md"));
327
+ }
328
+ for (const rel of manifest?.skills ?? []) {
329
+ skillMds.add(rel.endsWith(".md") ? join(root, rel) : join(root, rel, "SKILL.md"));
330
+ }
331
+ const pSkills = [];
332
+ for (const md of skillMds) {
333
+ const text = await readText(md);
334
+ if (text !== null) pSkills.push(skillItem(parseFrontmatter(text), basename(dirname(md)), "plugin", pluginName));
335
+ }
336
+ const pMcp = mcpItemsFrom(await readJson(join(root, ".mcp.json")), "plugin", pluginName);
337
+ for (const it of [...pSkills, ...pAgents, ...pMcp]) it.enabled = enabled;
338
+ skills.push(...pSkills);
339
+ agents.push(...pAgents);
340
+ mcp.push(...pMcp);
341
+ }
342
+ }
343
+ const seen = /* @__PURE__ */ new Set();
344
+ const dedupedSkills = skills.filter((s) => {
345
+ const k = `${s.scope}:${s.source ?? ""}:${s.name}`;
346
+ if (seen.has(k)) return false;
347
+ seen.add(k);
348
+ return true;
349
+ });
350
+ const data = {
351
+ skills: sortItems(dedupedSkills),
352
+ agents: sortItems(agents),
353
+ mcp: sortItems(mcp),
354
+ counts: {
355
+ skills: dedupedSkills.length,
356
+ agents: agents.length,
357
+ mcp: mcp.length,
358
+ plugins: pluginCount
359
+ },
360
+ scanned_at: new Date(now).toISOString()
361
+ };
362
+ cache = { key, at: now, data };
363
+ return data;
364
+ }
365
+
148
366
  // src/dashboard/delta.ts
149
- import { readFile as readFile3 } from "fs/promises";
367
+ import { readFile as readFile4 } from "fs/promises";
150
368
 
151
369
  // src/learn/store.ts
152
- import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
153
- import { dirname } from "path";
370
+ import { appendFile, mkdir, readFile as readFile2, writeFile } from "fs/promises";
371
+ import { dirname as dirname2 } from "path";
154
372
 
155
373
  // src/learn/usage.ts
156
374
  var LEARN_SCHEMA_VERSION = 1;
@@ -217,7 +435,7 @@ function recomputeFromLog(events) {
217
435
  // src/learn/store.ts
218
436
  async function readLearnStore(path) {
219
437
  try {
220
- const raw = await readFile(path, "utf8");
438
+ const raw = await readFile2(path, "utf8");
221
439
  const parsed = JSON.parse(raw);
222
440
  if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
223
441
  return emptyStore();
@@ -233,14 +451,14 @@ async function readLearnStore(path) {
233
451
  }
234
452
  async function writeLearnStore(path, store) {
235
453
  try {
236
- await mkdir(dirname(path), { recursive: true });
454
+ await mkdir(dirname2(path), { recursive: true });
237
455
  await writeFile(path, JSON.stringify(store, null, 2) + "\n", "utf8");
238
456
  } catch {
239
457
  }
240
458
  }
241
459
  async function readAccessLog(path) {
242
460
  try {
243
- const raw = await readFile(path, "utf8");
461
+ const raw = await readFile2(path, "utf8");
244
462
  const out = [];
245
463
  for (const line of raw.split("\n")) {
246
464
  const t = line.trim();
@@ -260,43 +478,43 @@ async function readAccessLog(path) {
260
478
  }
261
479
  async function appendAccess(path, ev) {
262
480
  try {
263
- await mkdir(dirname(path), { recursive: true });
481
+ await mkdir(dirname2(path), { recursive: true });
264
482
  await appendFile(path, JSON.stringify(ev) + "\n", "utf8");
265
483
  } catch {
266
484
  }
267
485
  }
268
486
 
269
487
  // src/shared/paths.ts
270
- import { join } from "path";
488
+ import { join as join2 } from "path";
271
489
  function resolvePaths(projectRoot) {
272
- const graphDir = join(projectRoot, ".synthra-graph");
273
- const contextDir = join(projectRoot, ".synthra");
274
- const claudeDir = join(projectRoot, ".claude");
490
+ const graphDir = join2(projectRoot, ".synthra-graph");
491
+ const contextDir = join2(projectRoot, ".synthra");
492
+ const claudeDir = join2(projectRoot, ".claude");
275
493
  return {
276
494
  projectRoot,
277
495
  graphDir,
278
496
  contextDir,
279
- infoGraph: join(graphDir, "info_graph.json"),
280
- symbolIndex: join(graphDir, "symbol_index.json"),
281
- sessionState: join(graphDir, "session.json"),
282
- activityLog: join(graphDir, "activity.jsonl"),
283
- tokenLog: join(graphDir, "token_log.jsonl"),
284
- gateLog: join(graphDir, "gate_log.jsonl"),
285
- toolLog: join(graphDir, "tool_log.jsonl"),
286
- accessLog: join(graphDir, "access_log.jsonl"),
287
- learnStore: join(graphDir, "learn_store.json"),
288
- parseCache: join(graphDir, "parse_cache.json"),
289
- mcpPort: join(graphDir, "mcp_port"),
290
- mcpServerLog: join(graphDir, "mcp_server.log"),
291
- mcpServerErrLog: join(graphDir, "mcp_server.err.log"),
292
- contextStore: join(contextDir, "context-store.json"),
293
- contextMd: join(contextDir, "CONTEXT.md"),
294
- branchesDir: join(contextDir, "branches"),
497
+ infoGraph: join2(graphDir, "info_graph.json"),
498
+ symbolIndex: join2(graphDir, "symbol_index.json"),
499
+ sessionState: join2(graphDir, "session.json"),
500
+ activityLog: join2(graphDir, "activity.jsonl"),
501
+ tokenLog: join2(graphDir, "token_log.jsonl"),
502
+ gateLog: join2(graphDir, "gate_log.jsonl"),
503
+ toolLog: join2(graphDir, "tool_log.jsonl"),
504
+ accessLog: join2(graphDir, "access_log.jsonl"),
505
+ learnStore: join2(graphDir, "learn_store.json"),
506
+ parseCache: join2(graphDir, "parse_cache.json"),
507
+ mcpPort: join2(graphDir, "mcp_port"),
508
+ mcpServerLog: join2(graphDir, "mcp_server.log"),
509
+ mcpServerErrLog: join2(graphDir, "mcp_server.err.log"),
510
+ contextStore: join2(contextDir, "context-store.json"),
511
+ contextMd: join2(contextDir, "CONTEXT.md"),
512
+ branchesDir: join2(contextDir, "branches"),
295
513
  claudeDir,
296
- claudeSettings: join(claudeDir, "settings.local.json"),
297
- claudeHooksDir: join(claudeDir, "hooks"),
298
- claudeMd: join(projectRoot, "CLAUDE.md"),
299
- gitignore: join(projectRoot, ".gitignore")
514
+ claudeSettings: join2(claudeDir, "settings.local.json"),
515
+ claudeHooksDir: join2(claudeDir, "hooks"),
516
+ claudeMd: join2(projectRoot, "CLAUDE.md"),
517
+ gitignore: join2(projectRoot, ".gitignore")
300
518
  };
301
519
  }
302
520
 
@@ -332,15 +550,15 @@ function estimateCostUsd(usage) {
332
550
  }
333
551
 
334
552
  // src/shared/project-registry.ts
335
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
336
- import { homedir } from "os";
337
- import { basename, dirname as dirname2, join as join2 } from "path";
338
- var REGISTRY_DIR = join2(homedir(), ".synthra");
339
- var REGISTRY_PATH = join2(REGISTRY_DIR, "projects.json");
553
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
554
+ import { homedir as homedir2 } from "os";
555
+ import { basename as basename2, dirname as dirname3, join as join3 } from "path";
556
+ var REGISTRY_DIR = join3(homedir2(), ".synthra");
557
+ var REGISTRY_PATH = join3(REGISTRY_DIR, "projects.json");
340
558
  var SCHEMA_VERSION = 1;
341
559
  async function readRegistry() {
342
560
  try {
343
- const raw = await readFile2(REGISTRY_PATH, "utf8");
561
+ const raw = await readFile3(REGISTRY_PATH, "utf8");
344
562
  const parsed = JSON.parse(raw);
345
563
  if (!Array.isArray(parsed.projects)) return { schema_version: SCHEMA_VERSION, projects: [] };
346
564
  return { schema_version: parsed.schema_version ?? SCHEMA_VERSION, projects: parsed.projects };
@@ -349,7 +567,7 @@ async function readRegistry() {
349
567
  }
350
568
  }
351
569
  async function writeRegistry(registry) {
352
- await mkdir2(dirname2(REGISTRY_PATH), { recursive: true });
570
+ await mkdir2(dirname3(REGISTRY_PATH), { recursive: true });
353
571
  await writeFile2(REGISTRY_PATH, JSON.stringify(registry, null, 2) + "\n", "utf8");
354
572
  }
355
573
  async function recordProject(projectRoot) {
@@ -358,11 +576,11 @@ async function recordProject(projectRoot) {
358
576
  const existing = registry.projects.find((p) => p.path === projectRoot);
359
577
  if (existing) {
360
578
  existing.last_seen = now;
361
- existing.name = basename(projectRoot);
579
+ existing.name = basename2(projectRoot);
362
580
  } else {
363
581
  registry.projects.push({
364
582
  path: projectRoot,
365
- name: basename(projectRoot),
583
+ name: basename2(projectRoot),
366
584
  first_seen: now,
367
585
  last_seen: now
368
586
  });
@@ -392,7 +610,7 @@ function topHotFiles(store, nowMs, limit = 8) {
392
610
  }
393
611
  async function readJsonl(path) {
394
612
  try {
395
- const text = await readFile3(path, "utf8");
613
+ const text = await readFile4(path, "utf8");
396
614
  return text.split(/\r?\n/).filter((l) => l.length > 0).map((l) => {
397
615
  try {
398
616
  return JSON.parse(l);
@@ -404,7 +622,7 @@ async function readJsonl(path) {
404
622
  return [];
405
623
  }
406
624
  }
407
- function basename2(p) {
625
+ function basename3(p) {
408
626
  const parts = p.split(/[\\/]/);
409
627
  return parts[parts.length - 1] || p;
410
628
  }
@@ -502,7 +720,7 @@ function dedupeTokens(entries) {
502
720
  async function computeDashboardData(activePaths, recentN = 500) {
503
721
  const registered = await listProjects();
504
722
  const activePath = activePaths.projectRoot;
505
- const activeName = basename2(activePath);
723
+ const activeName = basename3(activePath);
506
724
  const knownPaths = new Set(registered.map((p) => p.path));
507
725
  const allEntries = [
508
726
  ...registered.map((p) => ({ path: p.path, name: p.name, last_seen: p.last_seen }))
@@ -618,6 +836,7 @@ var public_default = `<!doctype html>
618
836
 
619
837
  <!-- ============ Top nav ============ -->
620
838
  <header class="topnav">
839
+ <button class="arsenal-toggle has-tooltip" id="arsenal-toggle" data-tooltip="Your arsenal \u2014 every skill, agent, and MCP server installed for this project, personally, or via plugins, with descriptions. So you never have to drop to the CLI to remember what's available." aria-label="Toggle arsenal">\u2694 <span>Arsenal</span></button>
621
840
  <div class="brand">
622
841
  <div class="brand-mark"></div>
623
842
  <div class="brand-name">Synth<em>ra</em></div>
@@ -644,6 +863,26 @@ var public_default = `<!doctype html>
644
863
  </div>
645
864
  </header>
646
865
 
866
+ <!-- ============ Arsenal drawer ============ -->
867
+ <div class="arsenal-scrim" id="arsenal-scrim"></div>
868
+ <aside class="arsenal-drawer" id="arsenal-drawer" aria-hidden="true">
869
+ <div class="arsenal-head">
870
+ <div class="arsenal-title">\u2694 Arsenal</div>
871
+ <button class="arsenal-icon" id="arsenal-refresh" title="Rescan" aria-label="Rescan">\u21BB</button>
872
+ <button class="arsenal-icon" id="arsenal-close" title="Close" aria-label="Close">\u2715</button>
873
+ </div>
874
+ <div class="arsenal-tabs">
875
+ <button class="arsenal-tab active" data-tab="skills">Skills <span class="at-count" id="at-skills">0</span></button>
876
+ <button class="arsenal-tab" data-tab="agents">Agents <span class="at-count" id="at-agents">0</span></button>
877
+ <button class="arsenal-tab" data-tab="mcp">MCP <span class="at-count" id="at-mcp">0</span></button>
878
+ </div>
879
+ <input class="arsenal-filter" id="arsenal-filter" type="text" placeholder="Filter by name or description\u2026" autocomplete="off" />
880
+ <div class="arsenal-list" id="arsenal-list">
881
+ <div class="empty">Open to load your arsenal\u2026</div>
882
+ </div>
883
+ <div class="arsenal-foot" id="arsenal-foot"></div>
884
+ </aside>
885
+
647
886
  <!-- ============ Main 3-column grid ============ -->
648
887
  <main class="grid-main">
649
888
 
@@ -1572,6 +1811,115 @@ var public_default = `<!doctype html>
1572
1811
  document.addEventListener('scroll', hideTooltip, true);
1573
1812
  document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideTooltip(); });
1574
1813
 
1814
+ // ----- Arsenal drawer -----
1815
+ let arsenalData = null; // cached payload after first load
1816
+ let arsenalTab = 'skills';
1817
+ let arsenalLoading = false;
1818
+
1819
+ function scopeBadge(item) {
1820
+ const s = item.scope;
1821
+ const label = s === 'plugin' ? (item.source || 'plugin') : s;
1822
+ const dis = (s === 'plugin' && item.enabled === false) ? ' off' : '';
1823
+ return '<span class="scope-badge ' + s + dis + '">' + escapeHtml(label) + (dis ? ' \xB7 off' : '') + '</span>';
1824
+ }
1825
+
1826
+ function escapeHtml(s) {
1827
+ return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => (
1828
+ { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]
1829
+ ));
1830
+ }
1831
+
1832
+ function renderArsenal() {
1833
+ const listEl = $('#arsenal-list');
1834
+ const footEl = $('#arsenal-foot');
1835
+ if (arsenalLoading) { listEl.innerHTML = '<div class="empty">Scanning\u2026</div>'; return; }
1836
+ if (!arsenalData) { listEl.innerHTML = '<div class="empty">Open to load your arsenal\u2026</div>'; return; }
1837
+
1838
+ $('#at-skills').textContent = arsenalData.counts.skills;
1839
+ $('#at-agents').textContent = arsenalData.counts.agents;
1840
+ $('#at-mcp').textContent = arsenalData.counts.mcp;
1841
+ footEl.textContent = arsenalData.counts.plugins + ' plugins \xB7 scanned ' +
1842
+ new Date(arsenalData.scanned_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1843
+
1844
+ const q = ($('#arsenal-filter').value || '').toLowerCase().trim();
1845
+ let items = arsenalData[arsenalTab] || [];
1846
+ if (q) items = items.filter((it) =>
1847
+ it.name.toLowerCase().includes(q) ||
1848
+ (it.description || '').toLowerCase().includes(q) ||
1849
+ (it.source || '').toLowerCase().includes(q));
1850
+
1851
+ listEl.innerHTML = '';
1852
+ if (!items.length) {
1853
+ listEl.innerHTML = '<div class="empty">' + (q ? 'No matches.' : 'Nothing installed in this category.') + '</div>';
1854
+ return;
1855
+ }
1856
+ const frag = document.createDocumentFragment();
1857
+ for (const it of items) {
1858
+ const row = document.createElement('div');
1859
+ row.className = 'arsenal-item';
1860
+ const meta = it.meta || {};
1861
+ const metaBits = [];
1862
+ if (meta.model) metaBits.push('model: ' + meta.model);
1863
+ if (meta.type) metaBits.push(meta.type);
1864
+ if (meta.url) metaBits.push(meta.url);
1865
+ if (meta.argument_hint) metaBits.push('args: ' + meta.argument_hint);
1866
+ if (meta.tools) metaBits.push('tools: ' + meta.tools);
1867
+ row.innerHTML =
1868
+ '<div class="ai-head">' +
1869
+ '<span class="ai-name">' + escapeHtml(it.name) + '</span>' +
1870
+ scopeBadge(it) +
1871
+ '</div>' +
1872
+ (it.description ? '<div class="ai-desc">' + escapeHtml(it.description) + '</div>' : '') +
1873
+ (metaBits.length ? '<div class="ai-meta">' + escapeHtml(metaBits.join(' \xB7 ')) + '</div>' : '');
1874
+ row.addEventListener('click', () => row.classList.toggle('open'));
1875
+ frag.appendChild(row);
1876
+ }
1877
+ listEl.appendChild(frag);
1878
+ }
1879
+
1880
+ async function loadArsenal(force) {
1881
+ if (arsenalData && !force) { renderArsenal(); return; }
1882
+ arsenalLoading = true; renderArsenal();
1883
+ try {
1884
+ const res = await fetch('/arsenal');
1885
+ if (!res.ok) throw new Error('HTTP ' + res.status);
1886
+ arsenalData = await res.json();
1887
+ } catch (e) {
1888
+ arsenalData = null;
1889
+ $('#arsenal-list').innerHTML = '<div class="empty">Failed to load arsenal.</div>';
1890
+ } finally {
1891
+ arsenalLoading = false;
1892
+ renderArsenal();
1893
+ }
1894
+ }
1895
+
1896
+ function openArsenal() {
1897
+ $('#arsenal-drawer').classList.add('open');
1898
+ $('#arsenal-drawer').setAttribute('aria-hidden', 'false');
1899
+ $('#arsenal-scrim').classList.add('on');
1900
+ loadArsenal(false);
1901
+ }
1902
+ function closeArsenal() {
1903
+ $('#arsenal-drawer').classList.remove('open');
1904
+ $('#arsenal-drawer').setAttribute('aria-hidden', 'true');
1905
+ $('#arsenal-scrim').classList.remove('on');
1906
+ }
1907
+
1908
+ $('#arsenal-toggle').addEventListener('click', () =>
1909
+ $('#arsenal-drawer').classList.contains('open') ? closeArsenal() : openArsenal());
1910
+ $('#arsenal-close').addEventListener('click', closeArsenal);
1911
+ $('#arsenal-scrim').addEventListener('click', closeArsenal);
1912
+ $('#arsenal-refresh').addEventListener('click', () => loadArsenal(true));
1913
+ $('#arsenal-filter').addEventListener('input', renderArsenal);
1914
+ document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeArsenal(); });
1915
+ for (const tab of document.querySelectorAll('.arsenal-tab')) {
1916
+ tab.addEventListener('click', () => {
1917
+ arsenalTab = tab.getAttribute('data-tab');
1918
+ for (const t of document.querySelectorAll('.arsenal-tab')) t.classList.toggle('active', t === tab);
1919
+ renderArsenal();
1920
+ });
1921
+ }
1922
+
1575
1923
  setHeroDate();
1576
1924
  tick();
1577
1925
  setInterval(tick, 10000);
@@ -1581,7 +1929,7 @@ var public_default = `<!doctype html>
1581
1929
  `;
1582
1930
 
1583
1931
  // src/dashboard/public/style.css
1584
- var style_default = '/* Synthra dashboard \xB7 v0.2 \xB7 Cool Marine\n Darkened surfaces; brand blue reserved for hero elements only.\n Layout: top nav + hero strip + 3-column main, fits 1280\xD7720. */\n\n:root {\n /* Core palette */\n --ink: #04081A;\n --navy: #0A1530;\n --navy-2: #122549;\n --deep-blue: #1B3A78;\n --blue: #2C5DB8;\n --blue-bright: #5C8FE6;\n --sky: #9BC2EF;\n --mist: #D7E6F7;\n --bone: #F4F7FC;\n\n /* Text */\n --text: #ECF2FB;\n --text-dim: #A9BBD6;\n --text-mute: #6D80A0;\n\n /* Rules / dividers */\n --rule: rgba(155, 194, 239, .14);\n --rule-2: rgba(155, 194, 239, .06);\n --rule-hover: rgba(155, 194, 239, .28);\n\n /* Surfaces (darker than v0.1.2) */\n --surface-1: rgba(18, 37, 73, .14);\n --surface-2: rgba(18, 37, 73, .22);\n --surface-3: rgba(4, 8, 26, .55);\n\n /* Signal accents (OKLCH shared chroma) */\n --signal-cyan: oklch(78% 0.14 220);\n --signal-amber: oklch(78% 0.14 75);\n --signal-rose: oklch(70% 0.14 20);\n --signal-green: oklch(75% 0.14 155);\n --signal-violet: oklch(72% 0.14 285);\n\n /* Model family colors */\n --c-fable: #2EE08F;\n --c-opus: #FF6338;\n --c-sonnet: #FFB938;\n --c-haiku: #7438FF;\n --c-unknown: #12CBF5;\n\n /* Money */\n --money: var(--signal-green);\n\n /* Type */\n --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-serif: "Instrument Serif", "Times New Roman", serif;\n --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;\n}\n\n/* ============================================================\n Reset + base\n ============================================================ */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n}\n\nhtml,\nbody {\n height: 100vh;\n overflow: hidden;\n}\n\nbody {\n background: var(--ink);\n color: var(--text);\n font-family: var(--font-sans);\n font-size: 13px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n display: grid;\n grid-template-rows: auto 1fr auto;\n position: relative;\n}\n\n/* Layered backdrop \u2014 quieter */\nbody::before,\nbody::after {\n content: "";\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 0;\n}\n\nbody::before {\n background-image: radial-gradient(circle, rgba(155, 194, 239, .06) 1px, transparent 1.2px);\n background-size: 22px 22px;\n}\n\nbody::after {\n background:\n radial-gradient(60% 40% at 50% 105%, rgba(44, 93, 184, .16) 0%, rgba(10, 21, 48, 0) 65%),\n radial-gradient(30% 25% at 50% 0%, rgba(92, 143, 230, .06) 0%, transparent 70%);\n}\n\nbody>* {\n position: relative;\n z-index: 1;\n}\n\nbutton {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: transparent;\n color: inherit;\n}\n\na {\n color: inherit;\n text-decoration: none;\n}\n\n/* ============================================================\n Top nav\n ============================================================ */\n.topnav {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n height: 52px;\n padding: 0 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(180deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.brand-mark {\n width: 22px;\n height: 22px;\n border-radius: 7px;\n background: radial-gradient(120% 120% at 30% 30%, #6FA6E8 0%, #2C5DB8 45%, #0A1530 100%);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22), 0 4px 12px -6px #2C5DB8;\n}\n\n.brand-name {\n font-size: 15px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--mist);\n}\n\n.brand-name em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: 0;\n}\n\n.brand-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-left: 6px;\n padding-left: 10px;\n border-left: 1px solid var(--rule);\n}\n\n.top-right {\n display: flex;\n align-items: center;\n gap: 12px;\n grid-column: 2;\n justify-self: center;\n}\n\n.topnav-right {\n grid-column: 3;\n justify-self: end;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.port-badge {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding: 6px 10px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n}\n\n.port-badge .mono {\n color: var(--text-dim);\n letter-spacing: 0.04em;\n text-transform: none;\n}\n\n.faq-btn {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .55);\n color: var(--text-dim);\n font-family: var(--font-mono);\n font-size: 13px;\n font-weight: 500;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, border-color 180ms, color 180ms, transform 180ms;\n}\n\n.faq-btn:hover {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n\n.status-pill {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-dim);\n transition: border-color 240ms ease;\n}\n\n.status-pill:has(.dot.live) {\n border-color: rgba(155, 194, 239, .45);\n color: var(--mist);\n animation: pill-glow 2.4s ease-in-out infinite;\n}\n\n.status-pill:has(.dot.dead) {\n border-color: rgba(220, 90, 90, .40);\n color: oklch(80% 0.10 20);\n}\n\n@keyframes pill-glow {\n\n 0%,\n 100% {\n box-shadow: 0 0 14px -4px rgba(155, 194, 239, .30), inset 0 0 12px -8px rgba(155, 194, 239, .30);\n }\n\n 50% {\n box-shadow: 0 0 26px -2px rgba(155, 194, 239, .55), inset 0 0 18px -6px rgba(155, 194, 239, .45);\n }\n}\n\n.dot {\n width: 7px;\n height: 7px;\n border-radius: 2px;\n background: var(--text-mute);\n transition: background 200ms;\n}\n\n.dot.live {\n background: var(--signal-cyan);\n animation: dot-pulse 1.8s ease-in-out infinite;\n}\n\n.dot.dead {\n background: var(--signal-rose);\n box-shadow: 0 0 0 3px rgba(220, 90, 90, .10);\n}\n\n@keyframes dot-pulse {\n\n 0%,\n 100% {\n box-shadow:\n 0 0 0 3px rgba(155, 194, 239, .10),\n 0 0 6px rgba(155, 194, 239, .50);\n }\n\n 50% {\n box-shadow:\n 0 0 0 6px rgba(155, 194, 239, .05),\n 0 0 14px rgba(155, 194, 239, .90);\n }\n}\n\n/* ============================================================\n Hero strip\n ============================================================ */\n.hero-strip {\n display: flex;\n align-items: center;\n gap: 24px;\n padding: 14px 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(90deg, rgba(27, 58, 120, .10) 0%, rgba(4, 8, 26, 0) 100%);\n position: relative;\n overflow: hidden;\n}\n\n.hero-spacer {\n flex: 1;\n}\n\n.date-block {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.d-day {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 38px;\n line-height: 1;\n letter-spacing: -0.04em;\n color: var(--mist);\n}\n\n.d-rest {\n display: flex;\n flex-direction: column;\n gap: 2px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-dim);\n}\n\n.d-rest .d-mute {\n color: var(--text-mute);\n}\n\n.active-block {\n display: flex;\n flex-direction: column;\n gap: 2px;\n text-align: right;\n max-width: 360px;\n overflow: hidden;\n}\n\n.ab-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.ab-value {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 360px;\n}\n\n/* ============================================================\n Main grid\n ============================================================ */\n.grid-main {\n display: grid;\n grid-template-columns: 260px 1fr 340px;\n gap: 16px;\n padding: 16px 24px;\n min-height: 0;\n z-index: 10;\n}\n\n.col-left,\n.col-center,\n.col-right {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n}\n\n/* Savings + Cost share one row at the top of the center column */\n.hero-row {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n align-items: stretch;\n flex-shrink: 0;\n}\n\n.hero-row > .card { flex-shrink: 1; }\n\n@media (max-width: 1100px) {\n .hero-row { grid-template-columns: 1fr; }\n}\n\n/* ============================================================\n Panels / cards \u2014 darker\n ============================================================ */\n.panel,\n.card {\n position: relative;\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n transition: border-color 180ms ease, background 180ms ease;\n}\n\n.card.has-tooltip {\n cursor: help;\n}\n\n.card.has-tooltip:hover {\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n\n.card-head {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 12px;\n}\n\n.card-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.card-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.card-meta {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* Legend panel */\n.panel {\n padding: 14px 14px 16px;\n gap: 14px;\n flex-shrink: 0;\n}\n\n.p-head {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.p-section {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.p-section+.p-section {\n padding-top: 12px;\n border-top: 1px solid var(--rule-2);\n}\n\n.ps-head {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 4px;\n}\n\n.check {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n letter-spacing: 0.02em;\n}\n\nbutton.check {\n border: 0;\n background: transparent;\n width: 100%;\n text-align: left;\n}\n\n.check-clickable {\n cursor: pointer;\n border-radius: 6px;\n padding: 5px 6px;\n transition: background 140ms, color 140ms, transform 140ms;\n}\n\n.check-clickable .pf-arrow {\n margin-left: auto;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 12px;\n transition: color 140ms, transform 140ms;\n}\n\n.check-clickable:hover {\n background: rgba(155, 194, 239, .07);\n color: var(--mist);\n}\n\n.check-clickable:hover .pf-arrow {\n color: var(--sky);\n transform: translateX(2px);\n}\n\n.dot-sq {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n background: var(--text-mute);\n flex-shrink: 0;\n}\n\n.dot-sq.fable {\n background: var(--c-fable);\n}\n\n.dot-sq.opus {\n background: var(--c-opus);\n}\n\n.dot-sq.sonnet {\n background: var(--c-sonnet);\n}\n\n.dot-sq.haiku {\n background: var(--c-haiku);\n}\n\n.dot-sq.unknown {\n background: var(--c-unknown);\n}\n\n.proj-filter {\n display: flex;\n flex-direction: column;\n gap: 1px;\n max-height: 90px;\n overflow-y: auto;\n}\n\n.pf-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 140px;\n}\n\n/* ============================================================\n Donut card (model usage)\n ============================================================ */\n.donut-card {\n flex: 1;\n gap: 10px;\n}\n\n.donut-wrap {\n position: relative;\n width: 140px;\n height: 140px;\n margin: 4px auto 0;\n}\n\n.donut {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.donut-track {\n fill: none;\n stroke: rgba(155, 194, 239, .07);\n stroke-width: 14;\n}\n\n.donut-seg {\n transition: stroke-dashoffset 400ms ease, stroke-dasharray 400ms ease;\n}\n\n.donut-center {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n pointer-events: none;\n}\n\n.donut-total {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.donut-total-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.donut-legend {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dl-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n}\n\n.dl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.dl-name {\n color: var(--text-dim);\n}\n\n.dl-pct {\n color: var(--mist);\n font-weight: 500;\n}\n\n/* ============================================================\n Center column \u2014 Metric strip (no card chrome, divider-separated)\n ============================================================ */\n.metric-strip {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n overflow: hidden;\n flex-shrink: 0;\n}\n\n.metric-item {\n padding: 14px 18px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n cursor: help;\n border-right: 1px solid var(--rule-2);\n transition: background 200ms ease;\n min-width: 0;\n}\n.metric-item:last-child { border-right: 0; }\n.metric-item:hover { background: var(--surface-2); }\n\n.m-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.m-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1;\n}\n\n.m-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: -0.005em;\n}\n\n/* ============================================================\n Savings card\n ============================================================ */\n.card.savings {\n flex-shrink: 0;\n gap: 12px;\n background:\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 50%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .18);\n}\n\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .32);\n}\n\n.savings-body {\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: center;\n gap: 18px;\n}\n\n.savings-figure {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.savings-money {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n}\n\n.savings-tokens {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.savings-bar {\n position: relative;\n height: 8px;\n border-radius: 999px;\n overflow: hidden;\n background: var(--surface-3);\n display: flex;\n}\n\n.savings-actual {\n height: 100%;\n background: rgba(215, 230, 247, .55);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.savings-saved {\n height: 100%;\n background: var(--signal-green);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);\n}\n\n.savings-legend {\n grid-column: 2;\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 24px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.08em;\n color: var(--text-mute);\n margin-top: 8px;\n}\n\n.sl-row {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.sl-row b {\n color: var(--mist);\n font-weight: 500;\n letter-spacing: 0.04em;\n}\n\n.sl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.sl-dot.actual {\n background: var(--mist);\n}\n\n.sl-dot.saved {\n background: var(--signal-green);\n}\n\n/* ============================================================\n Recent turns table\n ============================================================ */\n.turns-card {\n flex: 1;\n padding: 0;\n overflow: hidden;\n}\n\n.turns-card .card-head {\n padding: 14px 16px 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.turns-scroll {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.turns-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.turns-table thead th {\n position: sticky;\n top: 0;\n background: var(--ink);\n padding: 9px 16px;\n text-align: left;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n font-weight: 500;\n border-bottom: 1px solid var(--rule);\n z-index: 1;\n}\n\n.turns-table thead th.num {\n text-align: right;\n}\n\n.turns-table tbody td {\n padding: 8px 16px;\n border-bottom: 1px solid var(--rule-2);\n color: var(--text-dim);\n font-size: 12px;\n}\n\n.turns-table tbody td.num {\n text-align: right;\n font-family: var(--font-mono);\n}\n\n.turns-table tbody td.cost {\n color: var(--money);\n font-family: var(--font-mono);\n font-weight: 500;\n}\n\n.turns-table tbody td.ts {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n}\n\n.turns-table tbody td.proj {\n color: var(--mist);\n}\n\n.turns-table tbody tr:hover {\n background: rgba(155, 194, 239, .03);\n}\n\n.turns-table tbody tr:last-child td {\n border-bottom: 0;\n}\n\n/* Model pills */\n.model-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 2px 8px;\n border-radius: 999px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.04em;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .5);\n color: var(--mist);\n}\n\n.model-pill .sq {\n width: 6px;\n height: 6px;\n border-radius: 2px;\n background: var(--text-mute);\n}\n\n.model-pill.fable {\n color: #6BEFB4;\n border-color: rgba(46, 224, 143, .32);\n background: rgba(46, 224, 143, .07);\n}\n\n.model-pill.fable .sq {\n background: var(--c-fable);\n}\n\n.model-pill.opus {\n color: #FF8A66;\n border-color: rgba(255, 99, 56, .32);\n background: rgba(255, 99, 56, .07);\n}\n\n.model-pill.opus .sq {\n background: var(--c-opus);\n}\n\n.model-pill.sonnet {\n color: #FFC766;\n border-color: rgba(255, 185, 56, .32);\n background: rgba(255, 185, 56, .07);\n}\n\n.model-pill.sonnet .sq {\n background: var(--c-sonnet);\n}\n\n.model-pill.haiku {\n color: #A878FF;\n border-color: rgba(116, 56, 255, .42);\n background: rgba(116, 56, 255, .10);\n}\n\n.model-pill.haiku .sq {\n background: var(--c-haiku);\n}\n\n.model-pill.unknown {\n color: #5BDDF7;\n border-color: rgba(18, 203, 245, .32);\n background: rgba(18, 203, 245, .07);\n font-style: italic;\n}\n\n.model-pill.unknown .sq {\n background: var(--c-unknown);\n}\n\n/* ============================================================\n Right column \u2014 Cost hero\n ============================================================ */\n.cost-hero {\n position: relative;\n overflow: hidden;\n background:\n radial-gradient(120% 80% at 50% 110%, rgba(44, 93, 184, .18) 0%, rgba(4, 8, 26, .20) 60%),\n var(--surface-1);\n padding: 16px 18px 18px;\n gap: 10px;\n flex-shrink: 0;\n}\n\n.big-money {\n position: relative;\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 42px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n margin-top: 2px;\n}\n\n.big-money em {\n font-family: inherit;\n font-style: normal;\n font-weight: inherit;\n color: inherit;\n letter-spacing: inherit;\n opacity: 1;\n}\n\n.cost-sub {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.cs-row {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n font-family: var(--font-mono);\n font-size: 11px;\n}\n\n.cs-k {\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* File paths stay as-is (no uppercase/wide tracking) and may overflow \u2014 clip. */\n.cs-k.path {\n text-transform: none;\n letter-spacing: 0.01em;\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 14rem;\n}\n\n.cs-v {\n color: var(--mist);\n}\n\n/* ============================================================\n Moat card\n ============================================================ */\n.moat {\n flex: 1;\n gap: 8px;\n}\n\n/* Hot-files list: cap height + scroll so a long list never squeezes the Moat */\n#hot-files-list {\n max-height: 190px;\n overflow-y: auto;\n}\n\n.moat-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.03em;\n line-height: 1;\n color: var(--mist);\n margin-top: 2px;\n}\n\n.moat-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 18px;\n color: var(--sky);\n letter-spacing: 0;\n margin-left: 6px;\n}\n\n.gate-mini {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.gate-row {\n display: grid;\n grid-template-columns: auto auto 1fr;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-dim);\n padding: 3px 0;\n}\n\n.gate-row .g-ts {\n color: var(--text-mute);\n font-size: 9px;\n min-width: 38px;\n}\n\n.gate-row .g-decision {\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 999px;\n}\n\n.gate-row .g-decision.block {\n color: var(--signal-rose);\n background: rgba(220, 90, 90, .06);\n}\n\n.gate-row .g-decision.allow {\n color: var(--text-mute);\n background: rgba(155, 194, 239, .03);\n}\n\n.gate-row .g-q {\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ============================================================\n Tooltips\n ============================================================ */\n.has-tooltip {\n position: relative;\n}\n\n/* Global JS-positioned tooltip \u2014 viewport-clamped */\n.global-tooltip {\n position: fixed;\n top: 0;\n left: 0;\n background: linear-gradient(180deg, rgba(18, 37, 73, .98), rgba(10, 21, 48, .98));\n color: var(--mist);\n border: 1px solid var(--rule-hover);\n border-radius: 12px;\n padding: 14px 16px;\n font-family: var(--font-sans);\n font-size: 15px;\n font-weight: 400;\n text-transform: none;\n letter-spacing: 0;\n white-space: normal;\n width: 320px;\n max-width: calc(100vw - 24px);\n text-align: left;\n line-height: 1.55;\n box-shadow: 0 16px 36px rgba(0, 0, 0, .7);\n backdrop-filter: blur(10px);\n z-index: 99999;\n opacity: 0;\n pointer-events: none;\n transform: translateY(6px);\n transition: opacity 180ms ease, transform 180ms ease;\n}\n\n.global-tooltip.on {\n opacity: 1;\n transform: translateY(0);\n}\n\n/* ============================================================\n Footer\n ============================================================ */\n.foot {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 24px;\n border-top: 1px solid var(--rule);\n background: linear-gradient(0deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.foot em {\n font-family: var(--font-serif);\n font-style: italic;\n text-transform: none;\n letter-spacing: 0;\n color: var(--sky);\n font-size: 12px;\n}\n\n.foot .mono {\n color: var(--text-dim);\n text-transform: none;\n letter-spacing: 0.04em;\n}\n\n/* ============================================================\n Empty state\n ============================================================ */\n.empty {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.06em;\n color: var(--text-mute);\n text-align: center;\n padding: 16px 8px;\n font-style: italic;\n text-transform: none;\n}\n\n/* Scrollbar styling */\n.turns-scroll::-webkit-scrollbar,\n.proj-chart::-webkit-scrollbar,\n.gate-mini::-webkit-scrollbar,\n#hot-files-list::-webkit-scrollbar {\n width: 6px;\n}\n\n.turns-scroll::-webkit-scrollbar-thumb,\n.proj-chart::-webkit-scrollbar-thumb,\n.gate-mini::-webkit-scrollbar-thumb,\n#hot-files-list::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.turns-scroll::-webkit-scrollbar-track,\n.proj-chart::-webkit-scrollbar-track,\n.gate-mini::-webkit-scrollbar-track,\n#hot-files-list::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.hidden {\n display: none !important;\n}\n\n/* ============================================================\n Staggered cascade on first paint (one-time, MOTION 6)\n ============================================================ */\n@keyframes cascade-in {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .col-left > *,\n .col-center > *,\n .col-right > * {\n opacity: 0;\n animation: cascade-in 520ms cubic-bezier(0.16, 1, 0.3, 1) forwards;\n will-change: transform, opacity;\n }\n .col-left > *:nth-child(1) { animation-delay: 0ms; }\n .col-left > *:nth-child(2) { animation-delay: 120ms; }\n .col-center > *:nth-child(1) { animation-delay: 40ms; }\n .col-center > *:nth-child(2) { animation-delay: 140ms; }\n .col-center > *:nth-child(3) { animation-delay: 240ms; }\n .col-right > *:nth-child(1) { animation-delay: 80ms; }\n .col-right > *:nth-child(2) { animation-delay: 200ms; }\n .col-right > *:nth-child(3) { animation-delay: 320ms; }\n .col-right > *:nth-child(4) { animation-delay: 440ms; }\n\n /* Clear will-change after animation completes */\n .col-left > *,\n .col-center > *,\n .col-right > * {\n animation-fill-mode: forwards;\n }\n}\n\n/* ============================================================\n Source / basis annotations\n ============================================================ */\n.card-source {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.08em;\n text-transform: lowercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: auto;\n padding-top: 8px;\n border-top: 1px solid var(--rule-2);\n width: 100%;\n}\n\n.src-badge {\n font-family: var(--font-mono);\n font-size: 8px;\n font-weight: 500;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 4px;\n flex-shrink: 0;\n}\n\n.src-badge.verified {\n color: var(--signal-green);\n background: rgba(123, 255, 199, .08);\n border: 1px solid rgba(123, 255, 199, .25);\n}\n\n.src-badge.estimated,\n.src-badge.estimated.floor {\n color: var(--signal-amber);\n background: rgba(255, 185, 56, .10);\n border: 1px solid rgba(255, 185, 56, .30);\n}\n\n.src-badge.priced {\n color: var(--signal-cyan);\n background: rgba(155, 194, 239, .08);\n border: 1px solid rgba(155, 194, 239, .25);\n}\n\n/* Eyebrow that contains a badge */\n.card-eyebrow .src-badge {\n margin-left: 4px;\n}\n\n/* Savings audit row \u2014 live formula reveal */\n.savings-audit {\n margin-top: 10px;\n padding: 10px 12px;\n border: 1px dashed rgba(255, 185, 56, .25);\n border-radius: 8px;\n background: rgba(255, 185, 56, .04);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: 0.04em;\n color: var(--text-mute);\n text-align: center;\n}\n\n.savings-audit b {\n color: var(--text-dim);\n font-weight: 500;\n}\n\n.savings-audit .audit-result {\n color: var(--money);\n}\n\n/* ============================================================\n FAQ dialog\n ============================================================ */\n.dialog.dialog-faq {\n max-width: min(80vw, 1100px);\n width: 100%;\n max-height: 86vh;\n display: flex;\n flex-direction: column;\n padding: 28px 32px 24px;\n gap: 6px;\n}\n\n.dialog.dialog-faq .dialog-path {\n margin-bottom: 4px;\n word-break: normal;\n overflow-wrap: anywhere;\n}\n\n.faq-content {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n margin-top: 18px;\n padding-right: 8px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content::-webkit-scrollbar {\n width: 6px;\n}\n\n.faq-content::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.faq-content::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.faq-content details {\n border: 1px solid var(--rule);\n border-radius: 12px;\n background: var(--surface-1);\n overflow: hidden;\n transition: background 180ms, border-color 180ms;\n flex-shrink: 0;\n}\n\n.faq-content details:hover {\n border-color: rgba(155, 194, 239, .22);\n}\n\n.faq-content details[open] {\n background: var(--surface-2);\n border-color: var(--rule-hover);\n}\n\n.faq-content summary {\n cursor: pointer;\n padding: 14px 20px;\n font-family: var(--font-sans);\n font-size: 14px;\n font-weight: 500;\n color: var(--mist);\n list-style: none;\n display: flex;\n align-items: center;\n gap: 12px;\n user-select: none;\n}\n\n.faq-content summary::-webkit-details-marker {\n display: none;\n}\n\n.faq-content summary::before {\n content: "\u203A";\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 14px;\n transition: transform 220ms ease, color 220ms ease;\n}\n\n.faq-content details[open] summary::before {\n transform: rotate(90deg);\n color: var(--sky);\n}\n\n.faq-content .faq-body {\n padding: 0 22px 20px 46px;\n color: var(--text-dim);\n font-size: 13.5px;\n line-height: 1.7;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content .faq-body p {\n margin: 0;\n}\n\n.faq-content .faq-body ul {\n margin: 0;\n padding-left: 20px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.faq-content .faq-body li {\n margin: 0;\n}\n\n.faq-content .faq-body b,\n.faq-content .faq-body strong {\n color: var(--mist);\n font-weight: 500;\n}\n\n.faq-content .faq-body code {\n font-family: var(--font-mono);\n font-size: 12px;\n background: rgba(155, 194, 239, .08);\n padding: 2px 6px;\n border-radius: 4px;\n color: var(--mist);\n border: 1px solid rgba(155, 194, 239, .12);\n word-break: break-word;\n}\n\n.faq-content .faq-body a {\n color: var(--blue-bright);\n text-decoration: underline;\n text-decoration-color: rgba(92, 143, 230, .40);\n text-underline-offset: 3px;\n transition: color 140ms, text-decoration-color 140ms;\n}\n\n.faq-content .faq-body a:hover {\n color: var(--mist);\n text-decoration-color: var(--sky);\n}\n\n.faq-content .faq-body table {\n width: 100%;\n border-collapse: collapse;\n margin: 4px 0;\n font-size: 13px;\n table-layout: fixed;\n}\n\n.faq-content .faq-body thead td {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 6px;\n border-bottom: 1px solid var(--rule);\n font-weight: 500;\n}\n\n.faq-content .faq-body td {\n padding: 9px 10px;\n border-bottom: 1px solid var(--rule-2);\n vertical-align: top;\n word-break: break-word;\n}\n\n.faq-content .faq-body tr:last-child td {\n border-bottom: 0;\n}\n\n.faq-content .faq-body td:first-child {\n color: var(--text-dim);\n width: 38%;\n}\n\n.faq-content .faq-body td:first-child code {\n font-size: 11.5px;\n}\n\n.faq-content .faq-body .formula-box {\n font-family: var(--font-mono);\n font-size: 12.5px;\n background: rgba(255, 185, 56, .06);\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px dashed rgba(255, 185, 56, .30);\n color: var(--mist);\n letter-spacing: 0.02em;\n}\n\n.faq-content .faq-body .link-list {\n list-style: none;\n padding-left: 0;\n}\n\n.faq-content .faq-body .link-list li {\n padding-left: 18px;\n position: relative;\n}\n\n.faq-content .faq-body .link-list li::before {\n content: "\u203A";\n position: absolute;\n left: 0;\n color: var(--sky);\n font-family: var(--font-mono);\n}\n\n.faq-content .faq-body .warning {\n margin-top: 14px;\n padding: 12px 14px;\n background: rgba(255, 185, 56, .06);\n border: 1px solid rgba(255, 185, 56, .25);\n border-left: 3px solid var(--signal-amber);\n border-radius: 8px;\n font-size: 12.5px;\n color: var(--text-dim);\n}\n\n.faq-content .faq-body .warning .icon {\n color: var(--signal-amber);\n margin-right: 8px;\n font-weight: 500;\n}\n\n/* ============================================================\n Project dialog\n ============================================================ */\n.dialog-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(4, 8, 26, .78);\n backdrop-filter: blur(10px);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n animation: dlg-fade 180ms ease;\n}\n\n@keyframes dlg-fade {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n.dialog {\n position: relative;\n width: 100%;\n max-width: 520px;\n background:\n radial-gradient(120% 80% at 50% 0%, rgba(44, 93, 184, .22) 0%, rgba(4, 8, 26, .20) 60%),\n linear-gradient(180deg, rgba(18, 37, 73, .88) 0%, rgba(10, 21, 48, .96) 100%);\n border: 1px solid var(--rule-hover);\n border-radius: 18px;\n padding: 28px 32px 32px;\n box-shadow:\n 0 30px 80px -20px rgba(0, 0, 0, .7),\n inset 0 1px 0 rgba(255, 255, 255, .04);\n animation: dlg-rise 220ms cubic-bezier(.2, .7, .2, 1);\n}\n\n@keyframes dlg-rise {\n from {\n opacity: 0;\n transform: translateY(8px) scale(.98);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.dialog-close {\n position: absolute;\n top: 14px;\n right: 14px;\n width: 30px;\n height: 30px;\n border-radius: 50%;\n color: var(--text-mute);\n font-size: 22px;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, color 180ms;\n}\n\n.dialog-close:hover {\n background: rgba(155, 194, 239, .10);\n color: var(--mist);\n}\n\n.dialog-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 10px;\n}\n\n.dialog-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.dialog-name {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 28px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1.1;\n}\n\n.dialog-path {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n margin-top: 6px;\n word-break: break-all;\n}\n\n.dialog-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 18px 24px;\n margin-top: 22px;\n padding-top: 20px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dg-cell {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.dg-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.dg-v {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 22px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.dg-v.money {\n color: var(--money);\n}\n\n.dg-v-sm {\n font-size: 13px;\n font-family: var(--font-mono);\n font-weight: 400;\n color: var(--text-dim);\n letter-spacing: 0;\n}\n\n/* ============================================================\n v0.3 visual refresh\n - merged model-family legend into donut (count column)\n - Projects -> colored bar chart\n - elevated Savings card\n ============================================================ */\n\n/* Left column sizing: donut natural height, projects fills + scrolls */\n.donut-card { flex: 0 0 auto; }\n\n/* Donut legend now carries a count column */\n.dl-row {\n grid-template-columns: auto 1fr auto auto;\n gap: 8px;\n padding: 1px 0;\n}\n.dl-count {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n}\n.dl-pct {\n min-width: 30px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---- Projects bar chart ---- */\n.projects-card {\n flex: 1 1 auto;\n min-height: 0;\n gap: 10px;\n}\n.proj-chart {\n display: flex;\n flex-direction: column;\n gap: 9px;\n overflow-y: auto;\n min-height: 0;\n flex: 1;\n padding-right: 2px;\n}\n.proj-row {\n display: flex;\n flex-direction: column;\n gap: 7px;\n width: 100%;\n text-align: left;\n padding: 8px;\n border-radius: 9px;\n background: transparent;\n border: 0;\n cursor: pointer;\n transition: background 150ms ease;\n}\n.proj-row:hover { background: rgba(155, 194, 239, .055); }\n.pr-top {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: 9px;\n}\n.pr-dot {\n width: 9px;\n height: 9px;\n border-radius: 3px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 9px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n.pr-name {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text-dim);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n letter-spacing: 0.01em;\n transition: color 150ms ease;\n}\n.proj-row:hover .pr-name { color: var(--mist); }\n.pr-turns {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--mist);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n}\n.pr-arrow {\n font-family: var(--font-mono);\n font-size: 13px;\n color: var(--text-mute);\n transition: color 150ms ease, transform 150ms ease;\n}\n.proj-row:hover .pr-arrow {\n color: var(--pc, var(--sky));\n transform: translateX(2px);\n}\n.pr-bar {\n position: relative;\n height: 5px;\n border-radius: 999px;\n background: var(--surface-3);\n overflow: hidden;\n}\n.pr-fill {\n display: block;\n height: 100%;\n border-radius: 999px;\n background: linear-gradient(90deg,\n color-mix(in oklch, var(--pc, var(--sky)) 45%, transparent) 0%,\n var(--pc, var(--sky)) 100%);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);\n transition: width 640ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* ---- Elevated Savings card (priority focus) ---- */\n.card.savings {\n background:\n radial-gradient(120% 140% at 10% -10%, rgba(123, 255, 199, .14) 0%, rgba(4, 8, 26, .08) 44%),\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 52%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .24);\n box-shadow:\n inset 0 1px 0 rgba(123, 255, 199, .08),\n 0 20px 46px -30px rgba(123, 255, 199, .55);\n}\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .38);\n}\n.savings-money {\n font-size: 40px;\n text-shadow: 0 0 26px rgba(123, 255, 199, .22);\n}\n.savings-bar { height: 9px; }\n.savings-saved {\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, .18),\n 0 0 12px -2px var(--signal-green);\n}\n\n/* Project dialog: name gets a project-colored accent dot */\n.dialog-name.has-accent {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.dialog-name.has-accent::before {\n content: "";\n width: 12px;\n height: 12px;\n border-radius: 4px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 12px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n\n\n/* ============================================================\n v0.3.1 \u2014 date + active project folded into the top nav\n ============================================================ */\n\n/* Let the right cluster shrink so the active path can ellipsize */\n.topnav-right { min-width: 0; }\n\n/* Compact date beside the brand */\n.nav-date {\n display: inline-flex;\n align-items: baseline;\n gap: 6px;\n margin-left: 8px;\n padding-left: 12px;\n border-left: 1px solid var(--rule);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.nav-date .nd-day { color: var(--mist); font-weight: 500; }\n.nav-date .nd-weekday { color: var(--text-dim); }\n.nav-date .nd-month { color: var(--text-mute); }\n\n/* Active project, compact, tail-truncated */\n.nav-active {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n max-width: 300px;\n padding-right: 12px;\n margin-right: 2px;\n border-right: 1px solid var(--rule);\n cursor: help;\n}\n.na-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n flex-shrink: 0;\n}\n.na-value {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--mist);\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n /* keep the project folder (tail) visible, ellipsize the drive prefix */\n direction: rtl;\n text-align: left;\n}\n\n/* Tighten nav on narrow widths */\n@media (max-width: 1100px) {\n .nav-active { max-width: 200px; }\n .nav-date .nd-month { display: none; }\n}\n\n\n/* Column headers signal they are hover-explainable */\n.turns-table thead th.has-tooltip { cursor: help; }\n.turns-table thead th.has-tooltip:hover { color: var(--text-dim); }\n\n/* ---- Recent-turns pager ---- */\n.turns-pager {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n padding: 8px 2px 0;\n margin-top: 6px;\n border-top: 1px solid var(--rule-2);\n}\n.turns-pager.hidden { display: none; }\n.turns-pager button {\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n color: var(--text-dim);\n background: rgba(4, 8, 26, .55);\n border: 1px solid var(--rule);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n transition: background 150ms, border-color 150ms, color 150ms, transform 150ms;\n}\n.turns-pager button:hover:not(:disabled) {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n.turns-pager button:disabled {\n opacity: .35;\n cursor: default;\n}\n#turns-page-label {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n min-width: 84px;\n text-align: center;\n}\n';
1932
+ var style_default = '/* Synthra dashboard \xB7 v0.2 \xB7 Cool Marine\n Darkened surfaces; brand blue reserved for hero elements only.\n Layout: top nav + hero strip + 3-column main, fits 1280\xD7720. */\n\n:root {\n /* Core palette */\n --ink: #04081A;\n --navy: #0A1530;\n --navy-2: #122549;\n --deep-blue: #1B3A78;\n --blue: #2C5DB8;\n --blue-bright: #5C8FE6;\n --sky: #9BC2EF;\n --mist: #D7E6F7;\n --bone: #F4F7FC;\n\n /* Text */\n --text: #ECF2FB;\n --text-dim: #A9BBD6;\n --text-mute: #6D80A0;\n\n /* Rules / dividers */\n --rule: rgba(155, 194, 239, .14);\n --rule-2: rgba(155, 194, 239, .06);\n --rule-hover: rgba(155, 194, 239, .28);\n\n /* Surfaces (darker than v0.1.2) */\n --surface-1: rgba(18, 37, 73, .14);\n --surface-2: rgba(18, 37, 73, .22);\n --surface-3: rgba(4, 8, 26, .55);\n\n /* Signal accents (OKLCH shared chroma) */\n --signal-cyan: oklch(78% 0.14 220);\n --signal-amber: oklch(78% 0.14 75);\n --signal-rose: oklch(70% 0.14 20);\n --signal-green: oklch(75% 0.14 155);\n --signal-violet: oklch(72% 0.14 285);\n\n /* Model family colors */\n --c-fable: #2EE08F;\n --c-opus: #FF6338;\n --c-sonnet: #FFB938;\n --c-haiku: #7438FF;\n --c-unknown: #12CBF5;\n\n /* Money */\n --money: var(--signal-green);\n\n /* Type */\n --font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;\n --font-serif: "Instrument Serif", "Times New Roman", serif;\n --font-mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;\n}\n\n/* ============================================================\n Reset + base\n ============================================================ */\n*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n}\n\nhtml,\nbody {\n height: 100vh;\n overflow: hidden;\n}\n\nbody {\n background: var(--ink);\n color: var(--text);\n font-family: var(--font-sans);\n font-size: 13px;\n line-height: 1.5;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n display: grid;\n grid-template-rows: auto 1fr auto;\n position: relative;\n}\n\n/* Layered backdrop \u2014 quieter */\nbody::before,\nbody::after {\n content: "";\n position: fixed;\n inset: 0;\n pointer-events: none;\n z-index: 0;\n}\n\nbody::before {\n background-image: radial-gradient(circle, rgba(155, 194, 239, .06) 1px, transparent 1.2px);\n background-size: 22px 22px;\n}\n\nbody::after {\n background:\n radial-gradient(60% 40% at 50% 105%, rgba(44, 93, 184, .16) 0%, rgba(10, 21, 48, 0) 65%),\n radial-gradient(30% 25% at 50% 0%, rgba(92, 143, 230, .06) 0%, transparent 70%);\n}\n\nbody>* {\n position: relative;\n z-index: 1;\n}\n\nbutton {\n font: inherit;\n cursor: pointer;\n border: 0;\n background: transparent;\n color: inherit;\n}\n\na {\n color: inherit;\n text-decoration: none;\n}\n\n/* ============================================================\n Top nav\n ============================================================ */\n.topnav {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n height: 52px;\n padding: 0 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(180deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.brand-mark {\n width: 22px;\n height: 22px;\n border-radius: 7px;\n background: radial-gradient(120% 120% at 30% 30%, #6FA6E8 0%, #2C5DB8 45%, #0A1530 100%);\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .22), 0 4px 12px -6px #2C5DB8;\n}\n\n.brand-name {\n font-size: 15px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--mist);\n}\n\n.brand-name em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: 0;\n}\n\n.brand-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-left: 6px;\n padding-left: 10px;\n border-left: 1px solid var(--rule);\n}\n\n.top-right {\n display: flex;\n align-items: center;\n gap: 12px;\n grid-column: 2;\n justify-self: center;\n}\n\n.topnav-right {\n grid-column: 3;\n justify-self: end;\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.port-badge {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding: 6px 10px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n}\n\n.port-badge .mono {\n color: var(--text-dim);\n letter-spacing: 0.04em;\n text-transform: none;\n}\n\n.faq-btn {\n width: 30px;\n height: 30px;\n border-radius: 50%;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .55);\n color: var(--text-dim);\n font-family: var(--font-mono);\n font-size: 13px;\n font-weight: 500;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, border-color 180ms, color 180ms, transform 180ms;\n}\n\n.faq-btn:hover {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n\n.status-pill {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 6px 12px;\n border: 1px solid var(--rule);\n border-radius: 999px;\n background: rgba(4, 8, 26, .55);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-dim);\n transition: border-color 240ms ease;\n}\n\n.status-pill:has(.dot.live) {\n border-color: rgba(155, 194, 239, .45);\n color: var(--mist);\n animation: pill-glow 2.4s ease-in-out infinite;\n}\n\n.status-pill:has(.dot.dead) {\n border-color: rgba(220, 90, 90, .40);\n color: oklch(80% 0.10 20);\n}\n\n@keyframes pill-glow {\n\n 0%,\n 100% {\n box-shadow: 0 0 14px -4px rgba(155, 194, 239, .30), inset 0 0 12px -8px rgba(155, 194, 239, .30);\n }\n\n 50% {\n box-shadow: 0 0 26px -2px rgba(155, 194, 239, .55), inset 0 0 18px -6px rgba(155, 194, 239, .45);\n }\n}\n\n.dot {\n width: 7px;\n height: 7px;\n border-radius: 2px;\n background: var(--text-mute);\n transition: background 200ms;\n}\n\n.dot.live {\n background: var(--signal-cyan);\n animation: dot-pulse 1.8s ease-in-out infinite;\n}\n\n.dot.dead {\n background: var(--signal-rose);\n box-shadow: 0 0 0 3px rgba(220, 90, 90, .10);\n}\n\n@keyframes dot-pulse {\n\n 0%,\n 100% {\n box-shadow:\n 0 0 0 3px rgba(155, 194, 239, .10),\n 0 0 6px rgba(155, 194, 239, .50);\n }\n\n 50% {\n box-shadow:\n 0 0 0 6px rgba(155, 194, 239, .05),\n 0 0 14px rgba(155, 194, 239, .90);\n }\n}\n\n/* ============================================================\n Hero strip\n ============================================================ */\n.hero-strip {\n display: flex;\n align-items: center;\n gap: 24px;\n padding: 14px 24px;\n border-bottom: 1px solid var(--rule);\n background: linear-gradient(90deg, rgba(27, 58, 120, .10) 0%, rgba(4, 8, 26, 0) 100%);\n position: relative;\n overflow: hidden;\n}\n\n.hero-spacer {\n flex: 1;\n}\n\n.date-block {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n\n.d-day {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 38px;\n line-height: 1;\n letter-spacing: -0.04em;\n color: var(--mist);\n}\n\n.d-rest {\n display: flex;\n flex-direction: column;\n gap: 2px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-dim);\n}\n\n.d-rest .d-mute {\n color: var(--text-mute);\n}\n\n.active-block {\n display: flex;\n flex-direction: column;\n gap: 2px;\n text-align: right;\n max-width: 360px;\n overflow: hidden;\n}\n\n.ab-label {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.ab-value {\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 360px;\n}\n\n/* ============================================================\n Main grid\n ============================================================ */\n.grid-main {\n display: grid;\n grid-template-columns: 260px 1fr 340px;\n gap: 16px;\n padding: 16px 24px;\n min-height: 0;\n z-index: 10;\n}\n\n.col-left,\n.col-center,\n.col-right {\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n}\n\n/* Savings + Cost share one row at the top of the center column */\n.hero-row {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 12px;\n align-items: stretch;\n flex-shrink: 0;\n}\n\n.hero-row > .card { flex-shrink: 1; }\n\n@media (max-width: 1100px) {\n .hero-row { grid-template-columns: 1fr; }\n}\n\n/* ============================================================\n Panels / cards \u2014 darker\n ============================================================ */\n.panel,\n.card {\n position: relative;\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n min-height: 0;\n transition: border-color 180ms ease, background 180ms ease;\n}\n\n.card.has-tooltip {\n cursor: help;\n}\n\n.card.has-tooltip:hover {\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n\n.card-head {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: 12px;\n}\n\n.card-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.card-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.card-meta {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* Legend panel */\n.panel {\n padding: 14px 14px 16px;\n gap: 14px;\n flex-shrink: 0;\n}\n\n.p-head {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.p-section {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.p-section+.p-section {\n padding-top: 12px;\n border-top: 1px solid var(--rule-2);\n}\n\n.ps-head {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 4px;\n}\n\n.check {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n padding: 3px 6px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n letter-spacing: 0.02em;\n}\n\nbutton.check {\n border: 0;\n background: transparent;\n width: 100%;\n text-align: left;\n}\n\n.check-clickable {\n cursor: pointer;\n border-radius: 6px;\n padding: 5px 6px;\n transition: background 140ms, color 140ms, transform 140ms;\n}\n\n.check-clickable .pf-arrow {\n margin-left: auto;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 12px;\n transition: color 140ms, transform 140ms;\n}\n\n.check-clickable:hover {\n background: rgba(155, 194, 239, .07);\n color: var(--mist);\n}\n\n.check-clickable:hover .pf-arrow {\n color: var(--sky);\n transform: translateX(2px);\n}\n\n.dot-sq {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n background: var(--text-mute);\n flex-shrink: 0;\n}\n\n.dot-sq.fable {\n background: var(--c-fable);\n}\n\n.dot-sq.opus {\n background: var(--c-opus);\n}\n\n.dot-sq.sonnet {\n background: var(--c-sonnet);\n}\n\n.dot-sq.haiku {\n background: var(--c-haiku);\n}\n\n.dot-sq.unknown {\n background: var(--c-unknown);\n}\n\n.proj-filter {\n display: flex;\n flex-direction: column;\n gap: 1px;\n max-height: 90px;\n overflow-y: auto;\n}\n\n.pf-name {\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 140px;\n}\n\n/* ============================================================\n Donut card (model usage)\n ============================================================ */\n.donut-card {\n flex: 1;\n gap: 10px;\n}\n\n.donut-wrap {\n position: relative;\n width: 140px;\n height: 140px;\n margin: 4px auto 0;\n}\n\n.donut {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.donut-track {\n fill: none;\n stroke: rgba(155, 194, 239, .07);\n stroke-width: 14;\n}\n\n.donut-seg {\n transition: stroke-dashoffset 400ms ease, stroke-dasharray 400ms ease;\n}\n\n.donut-center {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n pointer-events: none;\n}\n\n.donut-total {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.donut-total-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.donut-legend {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dl-row {\n display: grid;\n grid-template-columns: auto 1fr auto;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-dim);\n}\n\n.dl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.dl-name {\n color: var(--text-dim);\n}\n\n.dl-pct {\n color: var(--mist);\n font-weight: 500;\n}\n\n/* ============================================================\n Center column \u2014 Metric strip (no card chrome, divider-separated)\n ============================================================ */\n.metric-strip {\n display: grid;\n grid-template-columns: repeat(5, 1fr);\n border: 1px solid var(--rule);\n border-radius: 14px;\n background: var(--surface-1);\n overflow: hidden;\n flex-shrink: 0;\n}\n\n.metric-item {\n padding: 14px 18px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n cursor: help;\n border-right: 1px solid var(--rule-2);\n transition: background 200ms ease;\n min-width: 0;\n}\n.metric-item:last-child { border-right: 0; }\n.metric-item:hover { background: var(--surface-2); }\n\n.m-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.m-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 26px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1;\n}\n\n.m-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n color: var(--sky);\n letter-spacing: -0.005em;\n}\n\n/* ============================================================\n Savings card\n ============================================================ */\n.card.savings {\n flex-shrink: 0;\n gap: 12px;\n background:\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 50%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .18);\n}\n\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .32);\n}\n\n.savings-body {\n display: grid;\n grid-template-columns: auto 1fr;\n align-items: center;\n gap: 18px;\n}\n\n.savings-figure {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.savings-money {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n}\n\n.savings-tokens {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-top: 4px;\n}\n\n.savings-bar {\n position: relative;\n height: 8px;\n border-radius: 999px;\n overflow: hidden;\n background: var(--surface-3);\n display: flex;\n}\n\n.savings-actual {\n height: 100%;\n background: rgba(215, 230, 247, .55);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n.savings-saved {\n height: 100%;\n background: var(--signal-green);\n transition: width 500ms cubic-bezier(0.16, 1, 0.3, 1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .12);\n}\n\n.savings-legend {\n grid-column: 2;\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 24px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.08em;\n color: var(--text-mute);\n margin-top: 8px;\n}\n\n.sl-row {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n}\n\n.sl-row b {\n color: var(--mist);\n font-weight: 500;\n letter-spacing: 0.04em;\n}\n\n.sl-dot {\n width: 8px;\n height: 8px;\n border-radius: 2px;\n}\n\n.sl-dot.actual {\n background: var(--mist);\n}\n\n.sl-dot.saved {\n background: var(--signal-green);\n}\n\n/* ============================================================\n Recent turns table\n ============================================================ */\n.turns-card {\n flex: 1;\n padding: 0;\n overflow: hidden;\n}\n\n.turns-card .card-head {\n padding: 14px 16px 10px;\n border-bottom: 1px solid var(--rule-2);\n}\n\n.turns-scroll {\n flex: 1;\n overflow-y: auto;\n min-height: 0;\n}\n\n.turns-table {\n width: 100%;\n border-collapse: collapse;\n}\n\n.turns-table thead th {\n position: sticky;\n top: 0;\n background: var(--ink);\n padding: 9px 16px;\n text-align: left;\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n font-weight: 500;\n border-bottom: 1px solid var(--rule);\n z-index: 1;\n}\n\n.turns-table thead th.num {\n text-align: right;\n}\n\n.turns-table tbody td {\n padding: 8px 16px;\n border-bottom: 1px solid var(--rule-2);\n color: var(--text-dim);\n font-size: 12px;\n}\n\n.turns-table tbody td.num {\n text-align: right;\n font-family: var(--font-mono);\n}\n\n.turns-table tbody td.cost {\n color: var(--money);\n font-family: var(--font-mono);\n font-weight: 500;\n}\n\n.turns-table tbody td.ts {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n}\n\n.turns-table tbody td.proj {\n color: var(--mist);\n}\n\n.turns-table tbody tr:hover {\n background: rgba(155, 194, 239, .03);\n}\n\n.turns-table tbody tr:last-child td {\n border-bottom: 0;\n}\n\n/* Model pills */\n.model-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 2px 8px;\n border-radius: 999px;\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.04em;\n border: 1px solid var(--rule);\n background: rgba(4, 8, 26, .5);\n color: var(--mist);\n}\n\n.model-pill .sq {\n width: 6px;\n height: 6px;\n border-radius: 2px;\n background: var(--text-mute);\n}\n\n.model-pill.fable {\n color: #6BEFB4;\n border-color: rgba(46, 224, 143, .32);\n background: rgba(46, 224, 143, .07);\n}\n\n.model-pill.fable .sq {\n background: var(--c-fable);\n}\n\n.model-pill.opus {\n color: #FF8A66;\n border-color: rgba(255, 99, 56, .32);\n background: rgba(255, 99, 56, .07);\n}\n\n.model-pill.opus .sq {\n background: var(--c-opus);\n}\n\n.model-pill.sonnet {\n color: #FFC766;\n border-color: rgba(255, 185, 56, .32);\n background: rgba(255, 185, 56, .07);\n}\n\n.model-pill.sonnet .sq {\n background: var(--c-sonnet);\n}\n\n.model-pill.haiku {\n color: #A878FF;\n border-color: rgba(116, 56, 255, .42);\n background: rgba(116, 56, 255, .10);\n}\n\n.model-pill.haiku .sq {\n background: var(--c-haiku);\n}\n\n.model-pill.unknown {\n color: #5BDDF7;\n border-color: rgba(18, 203, 245, .32);\n background: rgba(18, 203, 245, .07);\n font-style: italic;\n}\n\n.model-pill.unknown .sq {\n background: var(--c-unknown);\n}\n\n/* ============================================================\n Right column \u2014 Cost hero\n ============================================================ */\n.cost-hero {\n position: relative;\n overflow: hidden;\n background:\n radial-gradient(120% 80% at 50% 110%, rgba(44, 93, 184, .18) 0%, rgba(4, 8, 26, .20) 60%),\n var(--surface-1);\n padding: 16px 18px 18px;\n gap: 10px;\n flex-shrink: 0;\n}\n\n.big-money {\n position: relative;\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 42px;\n letter-spacing: -0.035em;\n line-height: 1;\n color: var(--money);\n margin-top: 2px;\n}\n\n.big-money em {\n font-family: inherit;\n font-style: normal;\n font-weight: inherit;\n color: inherit;\n letter-spacing: inherit;\n opacity: 1;\n}\n\n.cost-sub {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 6px;\n margin-top: 4px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n}\n\n.cs-row {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n font-family: var(--font-mono);\n font-size: 11px;\n}\n\n.cs-k {\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n/* File paths stay as-is (no uppercase/wide tracking) and may overflow \u2014 clip. */\n.cs-k.path {\n text-transform: none;\n letter-spacing: 0.01em;\n font-size: 11px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 14rem;\n}\n\n.cs-v {\n color: var(--mist);\n}\n\n/* ============================================================\n Moat card\n ============================================================ */\n.moat {\n flex: 1;\n gap: 8px;\n}\n\n/* Hot-files list: cap height + scroll so a long list never squeezes the Moat */\n#hot-files-list {\n max-height: 190px;\n overflow-y: auto;\n}\n\n.moat-value {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 34px;\n letter-spacing: -0.03em;\n line-height: 1;\n color: var(--mist);\n margin-top: 2px;\n}\n\n.moat-value em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 18px;\n color: var(--sky);\n letter-spacing: 0;\n margin-left: 6px;\n}\n\n.gate-mini {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin-top: 6px;\n padding-top: 10px;\n border-top: 1px solid var(--rule-2);\n overflow-y: auto;\n flex: 1;\n min-height: 0;\n}\n\n.gate-row {\n display: grid;\n grid-template-columns: auto auto 1fr;\n align-items: center;\n gap: 8px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-dim);\n padding: 3px 0;\n}\n\n.gate-row .g-ts {\n color: var(--text-mute);\n font-size: 9px;\n min-width: 38px;\n}\n\n.gate-row .g-decision {\n font-size: 9px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 999px;\n}\n\n.gate-row .g-decision.block {\n color: var(--signal-rose);\n background: rgba(220, 90, 90, .06);\n}\n\n.gate-row .g-decision.allow {\n color: var(--text-mute);\n background: rgba(155, 194, 239, .03);\n}\n\n.gate-row .g-q {\n color: var(--mist);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ============================================================\n Tooltips\n ============================================================ */\n.has-tooltip {\n position: relative;\n}\n\n/* Global JS-positioned tooltip \u2014 viewport-clamped */\n.global-tooltip {\n position: fixed;\n top: 0;\n left: 0;\n background: linear-gradient(180deg, rgba(18, 37, 73, .98), rgba(10, 21, 48, .98));\n color: var(--mist);\n border: 1px solid var(--rule-hover);\n border-radius: 12px;\n padding: 14px 16px;\n font-family: var(--font-sans);\n font-size: 15px;\n font-weight: 400;\n text-transform: none;\n letter-spacing: 0;\n white-space: normal;\n width: 320px;\n max-width: calc(100vw - 24px);\n text-align: left;\n line-height: 1.55;\n box-shadow: 0 16px 36px rgba(0, 0, 0, .7);\n backdrop-filter: blur(10px);\n z-index: 99999;\n opacity: 0;\n pointer-events: none;\n transform: translateY(6px);\n transition: opacity 180ms ease, transform 180ms ease;\n}\n\n.global-tooltip.on {\n opacity: 1;\n transform: translateY(0);\n}\n\n/* ============================================================\n Footer\n ============================================================ */\n.foot {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 8px 24px;\n border-top: 1px solid var(--rule);\n background: linear-gradient(0deg, rgba(4, 8, 26, .7), rgba(4, 8, 26, .4));\n backdrop-filter: blur(10px);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.foot em {\n font-family: var(--font-serif);\n font-style: italic;\n text-transform: none;\n letter-spacing: 0;\n color: var(--sky);\n font-size: 12px;\n}\n\n.foot .mono {\n color: var(--text-dim);\n text-transform: none;\n letter-spacing: 0.04em;\n}\n\n/* ============================================================\n Empty state\n ============================================================ */\n.empty {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.06em;\n color: var(--text-mute);\n text-align: center;\n padding: 16px 8px;\n font-style: italic;\n text-transform: none;\n}\n\n/* Scrollbar styling */\n.turns-scroll::-webkit-scrollbar,\n.proj-chart::-webkit-scrollbar,\n.gate-mini::-webkit-scrollbar,\n#hot-files-list::-webkit-scrollbar {\n width: 6px;\n}\n\n.turns-scroll::-webkit-scrollbar-thumb,\n.proj-chart::-webkit-scrollbar-thumb,\n.gate-mini::-webkit-scrollbar-thumb,\n#hot-files-list::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.turns-scroll::-webkit-scrollbar-track,\n.proj-chart::-webkit-scrollbar-track,\n.gate-mini::-webkit-scrollbar-track,\n#hot-files-list::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.hidden {\n display: none !important;\n}\n\n/* ============================================================\n Staggered cascade on first paint (one-time, MOTION 6)\n ============================================================ */\n@keyframes cascade-in {\n from { opacity: 0; transform: translateY(10px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .col-left > *,\n .col-center > *,\n .col-right > * {\n opacity: 0;\n animation: cascade-in 520ms cubic-bezier(0.16, 1, 0.3, 1) forwards;\n will-change: transform, opacity;\n }\n .col-left > *:nth-child(1) { animation-delay: 0ms; }\n .col-left > *:nth-child(2) { animation-delay: 120ms; }\n .col-center > *:nth-child(1) { animation-delay: 40ms; }\n .col-center > *:nth-child(2) { animation-delay: 140ms; }\n .col-center > *:nth-child(3) { animation-delay: 240ms; }\n .col-right > *:nth-child(1) { animation-delay: 80ms; }\n .col-right > *:nth-child(2) { animation-delay: 200ms; }\n .col-right > *:nth-child(3) { animation-delay: 320ms; }\n .col-right > *:nth-child(4) { animation-delay: 440ms; }\n\n /* Clear will-change after animation completes */\n .col-left > *,\n .col-center > *,\n .col-right > * {\n animation-fill-mode: forwards;\n }\n}\n\n/* ============================================================\n Source / basis annotations\n ============================================================ */\n.card-source {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.08em;\n text-transform: lowercase;\n color: var(--text-mute);\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-top: auto;\n padding-top: 8px;\n border-top: 1px solid var(--rule-2);\n width: 100%;\n}\n\n.src-badge {\n font-family: var(--font-mono);\n font-size: 8px;\n font-weight: 500;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 4px;\n flex-shrink: 0;\n}\n\n.src-badge.verified {\n color: var(--signal-green);\n background: rgba(123, 255, 199, .08);\n border: 1px solid rgba(123, 255, 199, .25);\n}\n\n.src-badge.estimated,\n.src-badge.estimated.floor {\n color: var(--signal-amber);\n background: rgba(255, 185, 56, .10);\n border: 1px solid rgba(255, 185, 56, .30);\n}\n\n.src-badge.priced {\n color: var(--signal-cyan);\n background: rgba(155, 194, 239, .08);\n border: 1px solid rgba(155, 194, 239, .25);\n}\n\n/* Eyebrow that contains a badge */\n.card-eyebrow .src-badge {\n margin-left: 4px;\n}\n\n/* Savings audit row \u2014 live formula reveal */\n.savings-audit {\n margin-top: 10px;\n padding: 10px 12px;\n border: 1px dashed rgba(255, 185, 56, .25);\n border-radius: 8px;\n background: rgba(255, 185, 56, .04);\n font-family: var(--font-mono);\n font-size: 10.5px;\n letter-spacing: 0.04em;\n color: var(--text-mute);\n text-align: center;\n}\n\n.savings-audit b {\n color: var(--text-dim);\n font-weight: 500;\n}\n\n.savings-audit .audit-result {\n color: var(--money);\n}\n\n/* ============================================================\n FAQ dialog\n ============================================================ */\n.dialog.dialog-faq {\n max-width: min(80vw, 1100px);\n width: 100%;\n max-height: 86vh;\n display: flex;\n flex-direction: column;\n padding: 28px 32px 24px;\n gap: 6px;\n}\n\n.dialog.dialog-faq .dialog-path {\n margin-bottom: 4px;\n word-break: normal;\n overflow-wrap: anywhere;\n}\n\n.faq-content {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n margin-top: 18px;\n padding-right: 8px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content::-webkit-scrollbar {\n width: 6px;\n}\n\n.faq-content::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n\n.faq-content::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.faq-content details {\n border: 1px solid var(--rule);\n border-radius: 12px;\n background: var(--surface-1);\n overflow: hidden;\n transition: background 180ms, border-color 180ms;\n flex-shrink: 0;\n}\n\n.faq-content details:hover {\n border-color: rgba(155, 194, 239, .22);\n}\n\n.faq-content details[open] {\n background: var(--surface-2);\n border-color: var(--rule-hover);\n}\n\n.faq-content summary {\n cursor: pointer;\n padding: 14px 20px;\n font-family: var(--font-sans);\n font-size: 14px;\n font-weight: 500;\n color: var(--mist);\n list-style: none;\n display: flex;\n align-items: center;\n gap: 12px;\n user-select: none;\n}\n\n.faq-content summary::-webkit-details-marker {\n display: none;\n}\n\n.faq-content summary::before {\n content: "\u203A";\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 16px;\n height: 16px;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 14px;\n transition: transform 220ms ease, color 220ms ease;\n}\n\n.faq-content details[open] summary::before {\n transform: rotate(90deg);\n color: var(--sky);\n}\n\n.faq-content .faq-body {\n padding: 0 22px 20px 46px;\n color: var(--text-dim);\n font-size: 13.5px;\n line-height: 1.7;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.faq-content .faq-body p {\n margin: 0;\n}\n\n.faq-content .faq-body ul {\n margin: 0;\n padding-left: 20px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.faq-content .faq-body li {\n margin: 0;\n}\n\n.faq-content .faq-body b,\n.faq-content .faq-body strong {\n color: var(--mist);\n font-weight: 500;\n}\n\n.faq-content .faq-body code {\n font-family: var(--font-mono);\n font-size: 12px;\n background: rgba(155, 194, 239, .08);\n padding: 2px 6px;\n border-radius: 4px;\n color: var(--mist);\n border: 1px solid rgba(155, 194, 239, .12);\n word-break: break-word;\n}\n\n.faq-content .faq-body a {\n color: var(--blue-bright);\n text-decoration: underline;\n text-decoration-color: rgba(92, 143, 230, .40);\n text-underline-offset: 3px;\n transition: color 140ms, text-decoration-color 140ms;\n}\n\n.faq-content .faq-body a:hover {\n color: var(--mist);\n text-decoration-color: var(--sky);\n}\n\n.faq-content .faq-body table {\n width: 100%;\n border-collapse: collapse;\n margin: 4px 0;\n font-size: 13px;\n table-layout: fixed;\n}\n\n.faq-content .faq-body thead td {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n color: var(--text-mute);\n padding-bottom: 6px;\n border-bottom: 1px solid var(--rule);\n font-weight: 500;\n}\n\n.faq-content .faq-body td {\n padding: 9px 10px;\n border-bottom: 1px solid var(--rule-2);\n vertical-align: top;\n word-break: break-word;\n}\n\n.faq-content .faq-body tr:last-child td {\n border-bottom: 0;\n}\n\n.faq-content .faq-body td:first-child {\n color: var(--text-dim);\n width: 38%;\n}\n\n.faq-content .faq-body td:first-child code {\n font-size: 11.5px;\n}\n\n.faq-content .faq-body .formula-box {\n font-family: var(--font-mono);\n font-size: 12.5px;\n background: rgba(255, 185, 56, .06);\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px dashed rgba(255, 185, 56, .30);\n color: var(--mist);\n letter-spacing: 0.02em;\n}\n\n.faq-content .faq-body .link-list {\n list-style: none;\n padding-left: 0;\n}\n\n.faq-content .faq-body .link-list li {\n padding-left: 18px;\n position: relative;\n}\n\n.faq-content .faq-body .link-list li::before {\n content: "\u203A";\n position: absolute;\n left: 0;\n color: var(--sky);\n font-family: var(--font-mono);\n}\n\n.faq-content .faq-body .warning {\n margin-top: 14px;\n padding: 12px 14px;\n background: rgba(255, 185, 56, .06);\n border: 1px solid rgba(255, 185, 56, .25);\n border-left: 3px solid var(--signal-amber);\n border-radius: 8px;\n font-size: 12.5px;\n color: var(--text-dim);\n}\n\n.faq-content .faq-body .warning .icon {\n color: var(--signal-amber);\n margin-right: 8px;\n font-weight: 500;\n}\n\n/* ============================================================\n Project dialog\n ============================================================ */\n.dialog-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(4, 8, 26, .78);\n backdrop-filter: blur(10px);\n z-index: 10000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n animation: dlg-fade 180ms ease;\n}\n\n@keyframes dlg-fade {\n from {\n opacity: 0;\n }\n\n to {\n opacity: 1;\n }\n}\n\n.dialog {\n position: relative;\n width: 100%;\n max-width: 520px;\n background:\n radial-gradient(120% 80% at 50% 0%, rgba(44, 93, 184, .22) 0%, rgba(4, 8, 26, .20) 60%),\n linear-gradient(180deg, rgba(18, 37, 73, .88) 0%, rgba(10, 21, 48, .96) 100%);\n border: 1px solid var(--rule-hover);\n border-radius: 18px;\n padding: 28px 32px 32px;\n box-shadow:\n 0 30px 80px -20px rgba(0, 0, 0, .7),\n inset 0 1px 0 rgba(255, 255, 255, .04);\n animation: dlg-rise 220ms cubic-bezier(.2, .7, .2, 1);\n}\n\n@keyframes dlg-rise {\n from {\n opacity: 0;\n transform: translateY(8px) scale(.98);\n }\n\n to {\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n}\n\n.dialog-close {\n position: absolute;\n top: 14px;\n right: 14px;\n width: 30px;\n height: 30px;\n border-radius: 50%;\n color: var(--text-mute);\n font-size: 22px;\n line-height: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n transition: background 180ms, color 180ms;\n}\n\n.dialog-close:hover {\n background: rgba(155, 194, 239, .10);\n color: var(--mist);\n}\n\n.dialog-eyebrow {\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--text-mute);\n margin-bottom: 10px;\n}\n\n.dialog-eyebrow em {\n font-family: var(--font-serif);\n font-style: italic;\n font-weight: 400;\n font-size: 12px;\n color: var(--sky);\n letter-spacing: 0;\n text-transform: none;\n}\n\n.dialog-name {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 28px;\n letter-spacing: -0.025em;\n color: var(--mist);\n line-height: 1.1;\n}\n\n.dialog-path {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n margin-top: 6px;\n word-break: break-all;\n}\n\n.dialog-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: 18px 24px;\n margin-top: 22px;\n padding-top: 20px;\n border-top: 1px solid var(--rule-2);\n}\n\n.dg-cell {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.dg-k {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n}\n\n.dg-v {\n font-family: var(--font-sans);\n font-weight: 500;\n font-size: 22px;\n letter-spacing: -0.02em;\n color: var(--mist);\n line-height: 1;\n}\n\n.dg-v.money {\n color: var(--money);\n}\n\n.dg-v-sm {\n font-size: 13px;\n font-family: var(--font-mono);\n font-weight: 400;\n color: var(--text-dim);\n letter-spacing: 0;\n}\n\n/* ============================================================\n v0.3 visual refresh\n - merged model-family legend into donut (count column)\n - Projects -> colored bar chart\n - elevated Savings card\n ============================================================ */\n\n/* Left column sizing: donut natural height, projects fills + scrolls */\n.donut-card { flex: 0 0 auto; }\n\n/* Donut legend now carries a count column */\n.dl-row {\n grid-template-columns: auto 1fr auto auto;\n gap: 8px;\n padding: 1px 0;\n}\n.dl-count {\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n}\n.dl-pct {\n min-width: 30px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n/* ---- Projects bar chart ---- */\n.projects-card {\n flex: 1 1 auto;\n min-height: 0;\n gap: 10px;\n}\n.proj-chart {\n display: flex;\n flex-direction: column;\n gap: 9px;\n overflow-y: auto;\n min-height: 0;\n flex: 1;\n padding-right: 2px;\n}\n.proj-row {\n display: flex;\n flex-direction: column;\n gap: 7px;\n width: 100%;\n text-align: left;\n padding: 8px;\n border-radius: 9px;\n background: transparent;\n border: 0;\n cursor: pointer;\n transition: background 150ms ease;\n}\n.proj-row:hover { background: rgba(155, 194, 239, .055); }\n.pr-top {\n display: grid;\n grid-template-columns: auto 1fr auto auto;\n align-items: center;\n gap: 9px;\n}\n.pr-dot {\n width: 9px;\n height: 9px;\n border-radius: 3px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 9px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n.pr-name {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--text-dim);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n letter-spacing: 0.01em;\n transition: color 150ms ease;\n}\n.proj-row:hover .pr-name { color: var(--mist); }\n.pr-turns {\n font-family: var(--font-mono);\n font-size: 11.5px;\n color: var(--mist);\n font-weight: 500;\n font-variant-numeric: tabular-nums;\n letter-spacing: 0.02em;\n}\n.pr-arrow {\n font-family: var(--font-mono);\n font-size: 13px;\n color: var(--text-mute);\n transition: color 150ms ease, transform 150ms ease;\n}\n.proj-row:hover .pr-arrow {\n color: var(--pc, var(--sky));\n transform: translateX(2px);\n}\n.pr-bar {\n position: relative;\n height: 5px;\n border-radius: 999px;\n background: var(--surface-3);\n overflow: hidden;\n}\n.pr-fill {\n display: block;\n height: 100%;\n border-radius: 999px;\n background: linear-gradient(90deg,\n color-mix(in oklch, var(--pc, var(--sky)) 45%, transparent) 0%,\n var(--pc, var(--sky)) 100%);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);\n transition: width 640ms cubic-bezier(0.16, 1, 0.3, 1);\n}\n\n/* ---- Elevated Savings card (priority focus) ---- */\n.card.savings {\n background:\n radial-gradient(120% 140% at 10% -10%, rgba(123, 255, 199, .14) 0%, rgba(4, 8, 26, .08) 44%),\n linear-gradient(180deg, rgba(123, 255, 199, .05) 0%, rgba(4, 8, 26, .20) 52%),\n var(--surface-1);\n border-color: rgba(123, 255, 199, .24);\n box-shadow:\n inset 0 1px 0 rgba(123, 255, 199, .08),\n 0 20px 46px -30px rgba(123, 255, 199, .55);\n}\n.card.savings:hover {\n border-color: rgba(123, 255, 199, .38);\n}\n.savings-money {\n font-size: 40px;\n text-shadow: 0 0 26px rgba(123, 255, 199, .22);\n}\n.savings-bar { height: 9px; }\n.savings-saved {\n box-shadow:\n inset 0 1px 0 rgba(255, 255, 255, .18),\n 0 0 12px -2px var(--signal-green);\n}\n\n/* Project dialog: name gets a project-colored accent dot */\n.dialog-name.has-accent {\n display: flex;\n align-items: center;\n gap: 12px;\n}\n.dialog-name.has-accent::before {\n content: "";\n width: 12px;\n height: 12px;\n border-radius: 4px;\n background: var(--pc, var(--sky));\n box-shadow: 0 0 12px -1px var(--pc, var(--sky));\n flex-shrink: 0;\n}\n\n\n/* ============================================================\n v0.3.1 \u2014 date + active project folded into the top nav\n ============================================================ */\n\n/* Let the right cluster shrink so the active path can ellipsize */\n.topnav-right { min-width: 0; }\n\n/* Compact date beside the brand */\n.nav-date {\n display: inline-flex;\n align-items: baseline;\n gap: 6px;\n margin-left: 8px;\n padding-left: 12px;\n border-left: 1px solid var(--rule);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.10em;\n text-transform: uppercase;\n white-space: nowrap;\n}\n.nav-date .nd-day { color: var(--mist); font-weight: 500; }\n.nav-date .nd-weekday { color: var(--text-dim); }\n.nav-date .nd-month { color: var(--text-mute); }\n\n/* Active project, compact, tail-truncated */\n.nav-active {\n display: flex;\n align-items: baseline;\n gap: 8px;\n min-width: 0;\n max-width: 300px;\n padding-right: 12px;\n margin-right: 2px;\n border-right: 1px solid var(--rule);\n cursor: help;\n}\n.na-label {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n color: var(--text-mute);\n flex-shrink: 0;\n}\n.na-value {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--mist);\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n /* keep the project folder (tail) visible, ellipsize the drive prefix */\n direction: rtl;\n text-align: left;\n}\n\n/* Tighten nav on narrow widths */\n@media (max-width: 1100px) {\n .nav-active { max-width: 200px; }\n .nav-date .nd-month { display: none; }\n}\n\n\n/* Column headers signal they are hover-explainable */\n.turns-table thead th.has-tooltip { cursor: help; }\n.turns-table thead th.has-tooltip:hover { color: var(--text-dim); }\n\n/* ---- Recent-turns pager ---- */\n.turns-pager {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 10px;\n padding: 8px 2px 0;\n margin-top: 6px;\n border-top: 1px solid var(--rule-2);\n}\n.turns-pager.hidden { display: none; }\n.turns-pager button {\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.04em;\n color: var(--text-dim);\n background: rgba(4, 8, 26, .55);\n border: 1px solid var(--rule);\n border-radius: 7px;\n padding: 4px 10px;\n cursor: pointer;\n transition: background 150ms, border-color 150ms, color 150ms, transform 150ms;\n}\n.turns-pager button:hover:not(:disabled) {\n background: rgba(155, 194, 239, .10);\n border-color: var(--rule-hover);\n color: var(--mist);\n transform: translateY(-1px);\n}\n.turns-pager button:disabled {\n opacity: .35;\n cursor: default;\n}\n#turns-page-label {\n font-family: var(--font-mono);\n font-size: 11px;\n color: var(--text-mute);\n letter-spacing: 0.04em;\n font-variant-numeric: tabular-nums;\n min-width: 84px;\n text-align: center;\n}\n\n/* ============================================================\n Arsenal drawer (skills \xB7 agents \xB7 MCP)\n ============================================================ */\n.arsenal-toggle {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-right: 16px;\n padding: 6px 12px;\n border: 1px solid var(--rule);\n border-radius: 9px;\n background: var(--surface-1);\n color: var(--mist);\n font-family: var(--font-mono);\n font-size: 11px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n transition: border-color 160ms ease, background 160ms ease;\n}\n.arsenal-toggle:hover {\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n\n.arsenal-scrim {\n position: fixed;\n inset: 0;\n background: rgba(4, 8, 26, .55);\n opacity: 0;\n pointer-events: none;\n transition: opacity 180ms ease;\n z-index: 60;\n}\n.arsenal-scrim.on {\n opacity: 1;\n pointer-events: auto;\n}\n\n.arsenal-drawer {\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n width: 360px;\n max-width: 88vw;\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 16px 14px;\n background: var(--surface-3);\n backdrop-filter: blur(8px);\n border-right: 1px solid var(--rule-hover);\n transform: translateX(-102%);\n transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);\n z-index: 70;\n}\n.arsenal-drawer.open {\n transform: translateX(0);\n}\n\n.arsenal-head {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n.arsenal-title {\n font-family: var(--font-serif);\n font-size: 20px;\n color: var(--text);\n flex: 1;\n}\n.arsenal-icon {\n width: 28px;\n height: 28px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n background: var(--surface-1);\n color: var(--text-dim);\n cursor: pointer;\n font-size: 13px;\n transition: border-color 160ms ease, color 160ms ease;\n}\n.arsenal-icon:hover {\n border-color: var(--rule-hover);\n color: var(--mist);\n}\n\n.arsenal-tabs {\n display: flex;\n gap: 6px;\n}\n.arsenal-tab {\n flex: 1;\n padding: 7px 6px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n background: transparent;\n color: var(--text-mute);\n font-family: var(--font-mono);\n font-size: 10px;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n cursor: pointer;\n transition: border-color 160ms ease, color 160ms ease, background 160ms ease;\n}\n.arsenal-tab:hover {\n color: var(--text-dim);\n}\n.arsenal-tab.active {\n color: var(--text);\n border-color: var(--rule-hover);\n background: var(--surface-2);\n}\n.at-count {\n color: var(--text-mute);\n font-variant-numeric: tabular-nums;\n}\n\n.arsenal-filter {\n padding: 8px 10px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n background: var(--surface-1);\n color: var(--mist);\n font-family: var(--font-sans);\n font-size: 12px;\n outline: none;\n}\n.arsenal-filter:focus {\n border-color: var(--rule-hover);\n}\n.arsenal-filter::placeholder {\n color: var(--text-mute);\n}\n\n.arsenal-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding-right: 4px;\n}\n\n.arsenal-item {\n border: 1px solid var(--rule-2);\n border-radius: 9px;\n background: var(--surface-1);\n padding: 9px 11px;\n cursor: pointer;\n transition: border-color 140ms ease, background 140ms ease;\n}\n.arsenal-item:hover {\n border-color: var(--rule);\n background: var(--surface-2);\n}\n.ai-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.ai-name {\n flex: 1;\n font-family: var(--font-mono);\n font-size: 12px;\n color: var(--mist);\n word-break: break-word;\n}\n.ai-desc {\n margin-top: 6px;\n font-size: 11.5px;\n line-height: 1.5;\n color: var(--text-dim);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n.ai-meta {\n margin-top: 6px;\n font-family: var(--font-mono);\n font-size: 10px;\n color: var(--text-mute);\n word-break: break-word;\n display: none;\n}\n.arsenal-item.open .ai-desc {\n -webkit-line-clamp: unset;\n line-clamp: unset;\n}\n.arsenal-item.open .ai-meta {\n display: block;\n}\n\n.scope-badge {\n flex-shrink: 0;\n font-family: var(--font-mono);\n font-size: 8px;\n font-weight: 500;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n padding: 2px 6px;\n border-radius: 4px;\n max-width: 140px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.scope-badge.project {\n color: var(--c-fable);\n background: rgba(46, 224, 143, .08);\n border: 1px solid rgba(46, 224, 143, .26);\n}\n.scope-badge.personal {\n color: var(--c-sonnet);\n background: rgba(255, 185, 56, .08);\n border: 1px solid rgba(255, 185, 56, .26);\n}\n.scope-badge.plugin {\n color: var(--sky);\n background: rgba(155, 194, 239, .06);\n border: 1px solid var(--rule);\n}\n.scope-badge.off {\n opacity: 0.5;\n}\n\n.arsenal-foot {\n font-family: var(--font-mono);\n font-size: 9px;\n letter-spacing: 0.08em;\n color: var(--text-mute);\n text-align: center;\n padding-top: 4px;\n border-top: 1px solid var(--rule-2);\n}\n\n.arsenal-list::-webkit-scrollbar {\n width: 6px;\n}\n.arsenal-list::-webkit-scrollbar-thumb {\n background: var(--rule);\n border-radius: 999px;\n}\n.arsenal-list::-webkit-scrollbar-track {\n background: transparent;\n}\n';
1585
1933
 
1586
1934
  // src/dashboard/public/favicon.svg
1587
1935
  var favicon_default = '<svg width="107" height="107" viewBox="0 0 107 107" fill="none" xmlns="http://www.w3.org/2000/svg">\n<rect x="0.5" y="0.5" width="106" height="106" rx="7.5" fill="url(#paint0_radial_21_11)"/>\n<rect x="0.5" y="0.5" width="106" height="106" rx="7.5" stroke="url(#paint1_linear_21_11)"/>\n<path d="M26.408 72.558C25.5813 72.558 24.6513 72.4753 23.618 72.31C22.626 72.1447 21.6753 71.938 20.766 71.69C19.898 71.442 19.216 71.1733 18.72 70.884C18.4307 70.7187 18.2033 70.5533 18.038 70.388C17.914 70.1813 17.852 69.8507 17.852 69.396L17.542 59.662C17.542 58.8353 17.8107 58.422 18.348 58.422C18.8027 58.422 19.1127 58.794 19.278 59.538L19.836 61.894C21.2413 67.8047 23.866 70.76 27.71 70.76C29.7767 70.76 31.4093 70.078 32.608 68.714C33.848 67.3087 34.468 65.3453 34.468 62.824C34.468 60.3853 33.8273 58.112 32.546 56.004C31.306 53.896 29.3013 51.8087 26.532 49.742C23.4733 47.5513 21.2413 45.4433 19.836 43.418C18.4307 41.3513 17.728 39.1607 17.728 36.846C17.728 33.7873 18.782 31.3487 20.89 29.53C23.0393 27.67 25.7673 26.74 29.074 26.74C30.314 26.74 31.5127 26.8847 32.67 27.174C33.8273 27.4633 34.778 27.8767 35.522 28.414C35.77 28.5793 35.956 28.7653 36.08 28.972C36.2453 29.1787 36.328 29.4473 36.328 29.778L36.514 39.14C36.514 39.8427 36.2453 40.194 35.708 40.194C35.336 40.194 35.0673 39.9047 34.902 39.326L34.468 37.776C33.5587 34.5933 32.608 32.2787 31.616 30.832C30.6653 29.344 29.2807 28.6 27.462 28.6C25.6847 28.6 24.2587 29.1787 23.184 30.336C22.1507 31.4933 21.634 33.126 21.634 35.234C21.634 37.0113 22.2127 38.706 23.37 40.318C24.5273 41.93 26.5527 43.8933 29.446 46.208C32.546 48.7293 34.7987 51.168 36.204 53.524C37.6507 55.8387 38.374 58.3187 38.374 60.964C38.374 63.2787 37.8573 65.304 36.824 67.04C35.7907 68.776 34.3647 70.14 32.546 71.132C30.7687 72.0827 28.7227 72.558 26.408 72.558ZM44.1831 84.71C43.0671 84.71 42.1784 84.3173 41.5171 83.532C40.8558 82.788 40.5251 81.92 40.5251 80.928C40.5251 80.1427 40.7524 79.4813 41.2071 78.944C41.6618 78.4067 42.2818 78.138 43.0671 78.138C43.7284 78.138 44.2038 78.2827 44.4931 78.572C44.7824 78.8613 44.9891 79.192 45.1131 79.564C45.2371 79.936 45.3611 80.2667 45.4851 80.556C45.6091 80.8453 45.8571 80.99 46.2291 80.99C46.6838 80.99 47.0971 80.6593 47.4691 79.998C47.8411 79.378 48.3578 78.0347 49.0191 75.968C49.4324 74.728 49.6804 73.6533 49.7631 72.744C49.8871 71.8347 49.8664 70.8633 49.7011 69.83C49.5771 68.7967 49.2671 67.536 48.7711 66.048L42.0131 44.534C41.7238 43.542 41.4344 42.9013 41.1451 42.612C40.8971 42.3227 40.4838 42.116 39.9051 41.992L38.9751 41.806C38.4378 41.682 38.1691 41.4133 38.1691 41C38.1691 40.5867 38.4584 40.38 39.0371 40.38H49.2671C49.8458 40.38 50.1351 40.5867 50.1351 41C50.1351 41.4547 49.8664 41.7233 49.3291 41.806L48.3371 41.93C47.3451 42.054 46.7044 42.302 46.4151 42.674C46.1671 43.0047 46.1878 43.6247 46.4771 44.534L51.9331 62.7C52.0571 63.1133 52.2431 63.32 52.4911 63.32C52.7804 63.32 52.9871 63.1133 53.1111 62.7L58.3811 45.65C58.7118 44.534 58.7944 43.7073 58.6291 43.17C58.5051 42.5913 57.9264 42.2193 56.8931 42.054L55.5911 41.806C55.0538 41.682 54.7851 41.4133 54.7851 41C54.7851 40.5867 55.0744 40.38 55.6531 40.38H63.5271C64.1058 40.38 64.3951 40.6073 64.3951 41.062C64.3951 41.4753 64.1471 41.7647 63.6511 41.93L63.0931 42.116C62.4731 42.3227 61.9564 42.6947 61.5431 43.232C61.1298 43.7693 60.6958 44.6993 60.2411 46.022L51.0031 74.852C50.0938 77.7453 49.2671 79.8947 48.5231 81.3C47.8204 82.7053 47.1384 83.6147 46.4771 84.028C45.8158 84.4827 45.0511 84.71 44.1831 84.71ZM64.9342 72C64.3142 72 64.0042 71.7727 64.0042 71.318C64.0042 70.946 64.2729 70.698 64.8102 70.574L65.5542 70.45C66.4222 70.2847 66.9802 70.0367 67.2282 69.706C67.5176 69.334 67.6622 68.714 67.6622 67.846V47.014C67.6622 46.27 67.5382 45.774 67.2902 45.526C67.0836 45.2367 66.6909 45.0507 66.1122 44.968L64.8102 44.782C64.2729 44.7407 64.0042 44.5133 64.0042 44.1C64.0042 43.7693 64.3349 43.542 64.9962 43.418C66.2776 43.2113 67.2489 42.8807 67.9102 42.426C68.6129 41.9713 69.3362 41.4133 70.0802 40.752C70.4522 40.38 70.7622 40.194 71.0102 40.194C71.3822 40.194 71.5682 40.442 71.5682 40.938V43.728C71.5682 44.1 71.6922 44.348 71.9402 44.472C72.2296 44.5547 72.5396 44.4307 72.8702 44.1C74.5236 42.5293 75.9702 41.4547 77.2102 40.876C78.4916 40.2973 79.8349 40.008 81.2402 40.008C83.1002 40.008 84.5882 40.6693 85.7042 41.992C86.8202 43.2733 87.3782 45.1747 87.3782 47.696V67.846C87.3782 68.714 87.5022 69.334 87.7502 69.706C88.0396 70.0367 88.6182 70.264 89.4862 70.388L90.8502 70.574C91.3049 70.6567 91.5322 70.9047 91.5322 71.318C91.5322 71.7727 91.2842 72 90.7882 72H80.3722C79.7936 72 79.5042 71.7727 79.5042 71.318C79.5042 70.9047 79.7316 70.6567 80.1862 70.574L81.0542 70.45C81.9222 70.326 82.4802 70.078 82.7282 69.706C83.0176 69.334 83.1622 68.714 83.1622 67.846V48.378C83.1622 46.3527 82.7902 44.9267 82.0462 44.1C81.3022 43.2733 80.2482 42.86 78.8842 42.86C77.5616 42.86 76.3629 43.2527 75.2882 44.038C74.2549 44.782 73.4282 45.7947 72.8082 47.076C72.1882 48.316 71.8782 49.7007 71.8782 51.23V67.846C71.8782 68.714 72.0022 69.334 72.2502 69.706C72.5396 70.078 73.1182 70.3053 73.9862 70.388L75.6602 70.574C76.1149 70.6567 76.3422 70.884 76.3422 71.256C76.3422 71.752 76.0322 72 75.4122 72H64.9342Z" fill="white"/>\n<defs>\n<radialGradient id="paint0_radial_21_11" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(14 16) rotate(43.5498) scale(167.638 156.2)">\n<stop stop-color="#2C5DB8"/>\n<stop offset="1" stop-color="#04081A"/>\n</radialGradient>\n<linearGradient id="paint1_linear_21_11" x1="1.77511" y1="1.50277e-06" x2="105.225" y2="107" gradientUnits="userSpaceOnUse">\n<stop stop-color="#5C8FE6"/>\n<stop offset="1" stop-color="#335080"/>\n</linearGradient>\n</defs>\n</svg>\n';
@@ -1610,6 +1958,7 @@ async function startDashboard(paths, preferredPort = 8901) {
1610
1958
  return c.body(favicon_default);
1611
1959
  });
1612
1960
  app.get("/health", (c) => c.json({ ok: true }));
1961
+ app.get("/arsenal", async (c) => c.json(await computeArsenal(paths.projectRoot)));
1613
1962
  app.get("/data", async (c) => {
1614
1963
  const data = await computeDashboardData(paths, RECENT_N);
1615
1964
  return c.json(data);
@@ -1627,8 +1976,8 @@ async function startDashboard(paths, preferredPort = 8901) {
1627
1976
  }
1628
1977
 
1629
1978
  // src/hooks/installer.ts
1630
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1631
- import { dirname as dirname3, join as join3 } from "path";
1979
+ import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
1980
+ import { dirname as dirname4, join as join4 } from "path";
1632
1981
 
1633
1982
  // src/hooks/scripts/pre-compact.ps1
1634
1983
  var pre_compact_default = '# PreCompact hook \u2014 Windows PowerShell.\n# Re-injects the primer after Claude auto-compacts. Same logic as prime.ps1.\n\n$ErrorActionPreference = "SilentlyContinue"\n\n$portFile = Join-Path $PWD ".synthra-graph\\mcp_port"\nif (-not (Test-Path $portFile)) { exit 0 }\n$port = (Get-Content -Path $portFile -Raw).Trim()\nif (-not $port) { exit 0 }\n\ntry {\n $resp = Invoke-RestMethod -Uri "http://127.0.0.1:$port/prime" -Method GET -TimeoutSec 3\n if ($resp.primer) { Write-Output $resp.primer }\n} catch {\n # silent\n}\nexit 0\n';
@@ -1932,7 +2281,7 @@ function chosenScriptExt() {
1932
2281
  }
1933
2282
  async function readSettings(path) {
1934
2283
  try {
1935
- const raw = await readFile4(path, "utf8");
2284
+ const raw = await readFile5(path, "utf8");
1936
2285
  return JSON.parse(raw);
1937
2286
  } catch {
1938
2287
  return {};
@@ -1954,7 +2303,7 @@ function stripOurHooks(config) {
1954
2303
  function mergeOurHooks(config, paths) {
1955
2304
  const hooks = config.hooks = config.hooks ?? {};
1956
2305
  for (const s of SCRIPTS) {
1957
- const scriptPath = join3(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
2306
+ const scriptPath = join4(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
1958
2307
  const entry = {
1959
2308
  ...s.matcher ? { matcher: s.matcher } : {},
1960
2309
  hooks: [
@@ -1974,11 +2323,11 @@ async function installHooks(paths) {
1974
2323
  await mkdir3(paths.claudeHooksDir, { recursive: true });
1975
2324
  const scriptsWritten = [];
1976
2325
  for (const s of SCRIPTS) {
1977
- const target = join3(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
2326
+ const target = join4(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
1978
2327
  await writeFile3(target, chosenScriptBody(s), "utf8");
1979
2328
  scriptsWritten.push(target);
1980
2329
  }
1981
- await mkdir3(dirname3(paths.claudeSettings), { recursive: true });
2330
+ await mkdir3(dirname4(paths.claudeSettings), { recursive: true });
1982
2331
  const existing = await readSettings(paths.claudeSettings);
1983
2332
  const stripped = stripOurHooks(existing);
1984
2333
  const merged = mergeOurHooks(stripped, paths);
@@ -1994,7 +2343,7 @@ import { writeFile as writeFile11 } from "fs/promises";
1994
2343
 
1995
2344
  // src/activity/activity-log.ts
1996
2345
  import { appendFile as appendFile2, mkdir as mkdir4 } from "fs/promises";
1997
- import { dirname as dirname4 } from "path";
2346
+ import { dirname as dirname5 } from "path";
1998
2347
  var DEFAULT_RING_SIZE = 100;
1999
2348
  var ActivityStore = class {
2000
2349
  ring = [];
@@ -2031,7 +2380,7 @@ var ActivityStore = class {
2031
2380
  }
2032
2381
  async persist(event) {
2033
2382
  try {
2034
- await mkdir4(dirname4(this.persistPath), { recursive: true });
2383
+ await mkdir4(dirname5(this.persistPath), { recursive: true });
2035
2384
  await appendFile2(this.persistPath, JSON.stringify(event) + "\n", "utf8");
2036
2385
  } catch {
2037
2386
  }
@@ -2040,8 +2389,8 @@ var ActivityStore = class {
2040
2389
 
2041
2390
  // src/activity/file-watcher.ts
2042
2391
  import chokidar from "chokidar";
2043
- import { readFile as readFile5 } from "fs/promises";
2044
- import { join as join4, relative, sep } from "path";
2392
+ import { readFile as readFile6 } from "fs/promises";
2393
+ import { join as join5, relative, sep } from "path";
2045
2394
  import ignore from "ignore";
2046
2395
  var ALWAYS_IGNORE = [
2047
2396
  ".git",
@@ -2063,7 +2412,7 @@ var ALWAYS_IGNORE = [
2063
2412
  ];
2064
2413
  async function readIgnoreFile(path) {
2065
2414
  try {
2066
- const text = await readFile5(path, "utf8");
2415
+ const text = await readFile6(path, "utf8");
2067
2416
  return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
2068
2417
  } catch {
2069
2418
  return [];
@@ -2072,8 +2421,8 @@ async function readIgnoreFile(path) {
2072
2421
  async function buildMatcher(root) {
2073
2422
  const ig = ignore();
2074
2423
  ig.add(ALWAYS_IGNORE.map((d) => `${d}/`));
2075
- ig.add(await readIgnoreFile(join4(root, ".gitignore")));
2076
- ig.add(await readIgnoreFile(join4(root, ".synthraignore")));
2424
+ ig.add(await readIgnoreFile(join5(root, ".gitignore")));
2425
+ ig.add(await readIgnoreFile(join5(root, ".synthraignore")));
2077
2426
  return ig;
2078
2427
  }
2079
2428
  function toPosixRel(root, abs) {
@@ -2128,14 +2477,14 @@ function createFileWatcher(root, onEvent) {
2128
2477
  // src/activity/git-watcher.ts
2129
2478
  import { execFile } from "child_process";
2130
2479
  import { watch } from "fs";
2131
- import { readFile as readFile6 } from "fs/promises";
2132
- import { join as join5 } from "path";
2480
+ import { readFile as readFile7 } from "fs/promises";
2481
+ import { join as join6 } from "path";
2133
2482
  import { promisify } from "util";
2134
2483
  var execFileAsync = promisify(execFile);
2135
2484
  var POLL_MS = 2e3;
2136
2485
  async function readHeadBranch(projectRoot) {
2137
2486
  try {
2138
- const head = await readFile6(join5(projectRoot, ".git", "HEAD"), "utf8");
2487
+ const head = await readFile7(join6(projectRoot, ".git", "HEAD"), "utf8");
2139
2488
  const m = head.trim().match(/^ref:\s+refs\/heads\/(.+)$/);
2140
2489
  return m?.[1] ?? null;
2141
2490
  } catch {
@@ -2202,7 +2551,7 @@ function createGitWatcher(root, onEvent) {
2202
2551
  lastBranch = await readHeadBranch(root);
2203
2552
  lastStatus = await readStatusPorcelain(root);
2204
2553
  try {
2205
- headWatcher = watch(join5(root, ".git", "HEAD"), () => {
2554
+ headWatcher = watch(join6(root, ".git", "HEAD"), () => {
2206
2555
  void checkHead();
2207
2556
  });
2208
2557
  headWatcher.on("error", () => {
@@ -2234,7 +2583,7 @@ function parseStatusFiles(porcelain) {
2234
2583
  import { resolve } from "path";
2235
2584
 
2236
2585
  // src/scanner/extract.ts
2237
- import { dirname as dirname5, join as join6, posix } from "path";
2586
+ import { dirname as dirname6, join as join7, posix } from "path";
2238
2587
 
2239
2588
  // src/graph/types.ts
2240
2589
  var SCHEMA_VERSION2 = 2;
@@ -2648,11 +2997,11 @@ function buildCallEdges(symbolsByFile, callsByFile) {
2648
2997
  }
2649
2998
 
2650
2999
  // src/scanner/parse-cache.ts
2651
- import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
2652
- import { dirname as dirname6 } from "path";
3000
+ import { mkdir as mkdir5, readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
3001
+ import { dirname as dirname7 } from "path";
2653
3002
 
2654
3003
  // src/scanner/parser.ts
2655
- import { readFile as readFile7 } from "fs/promises";
3004
+ import { readFile as readFile8 } from "fs/promises";
2656
3005
  import { createRequire } from "module";
2657
3006
  import { Language, Parser } from "web-tree-sitter";
2658
3007
 
@@ -3513,7 +3862,7 @@ function emptyParseCache() {
3513
3862
  }
3514
3863
  async function readParseCache(path) {
3515
3864
  try {
3516
- const raw = await readFile8(path, "utf8");
3865
+ const raw = await readFile9(path, "utf8");
3517
3866
  const parsed = JSON.parse(raw);
3518
3867
  if (parsed.schema_version !== PARSE_CACHE_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
3519
3868
  return emptyParseCache();
@@ -3523,16 +3872,16 @@ async function readParseCache(path) {
3523
3872
  return emptyParseCache();
3524
3873
  }
3525
3874
  }
3526
- async function writeParseCache(path, cache) {
3875
+ async function writeParseCache(path, cache2) {
3527
3876
  try {
3528
- await mkdir5(dirname6(path), { recursive: true });
3529
- await writeFile4(path, `${JSON.stringify(cache)}
3877
+ await mkdir5(dirname7(path), { recursive: true });
3878
+ await writeFile4(path, `${JSON.stringify(cache2)}
3530
3879
  `, "utf8");
3531
3880
  } catch {
3532
3881
  }
3533
3882
  }
3534
3883
  async function incrementalParse(parsable, prev, opts = {}) {
3535
- const cache = emptyParseCache();
3884
+ const cache2 = emptyParseCache();
3536
3885
  const parsed = [];
3537
3886
  let reused = 0;
3538
3887
  let reparsed = 0;
@@ -3540,7 +3889,7 @@ async function incrementalParse(parsable, prev, opts = {}) {
3540
3889
  for (const f of parsable) {
3541
3890
  let source;
3542
3891
  try {
3543
- source = await readFile8(f.absPath, "utf8");
3892
+ source = await readFile9(f.absPath, "utf8");
3544
3893
  } catch {
3545
3894
  continue;
3546
3895
  }
@@ -3554,25 +3903,25 @@ async function incrementalParse(parsable, prev, opts = {}) {
3554
3903
  imports: cached.imports,
3555
3904
  calls: cached.calls
3556
3905
  });
3557
- cache.files[f.relPath] = cached;
3906
+ cache2.files[f.relPath] = cached;
3558
3907
  reused += 1;
3559
3908
  continue;
3560
3909
  }
3561
3910
  try {
3562
3911
  const p = await parseSource(f, source);
3563
3912
  parsed.push(p);
3564
- cache.files[f.relPath] = { hash, symbols: p.symbols, imports: p.imports, calls: p.calls };
3913
+ cache2.files[f.relPath] = { hash, symbols: p.symbols, imports: p.imports, calls: p.calls };
3565
3914
  reparsed += 1;
3566
3915
  } catch {
3567
3916
  parseErrors += 1;
3568
3917
  }
3569
3918
  }
3570
- return { parsed, cache, reused, reparsed, parseErrors };
3919
+ return { parsed, cache: cache2, reused, reparsed, parseErrors };
3571
3920
  }
3572
3921
 
3573
3922
  // src/scanner/walker.ts
3574
- import { readFile as readFile9, readdir, stat } from "fs/promises";
3575
- import { extname, join as join7, relative as relative2, sep as sep2 } from "path";
3923
+ import { readFile as readFile10, readdir as readdir2, stat } from "fs/promises";
3924
+ import { extname, join as join8, relative as relative2, sep as sep2 } from "path";
3576
3925
  import ignore2 from "ignore";
3577
3926
  var DEFAULT_IGNORE = [
3578
3927
  ".git/",
@@ -3652,7 +4001,7 @@ var BINARY_EXTS = /* @__PURE__ */ new Set([
3652
4001
  ]);
3653
4002
  async function readIgnoreFile2(path) {
3654
4003
  try {
3655
- const text = await readFile9(path, "utf8");
4004
+ const text = await readFile10(path, "utf8");
3656
4005
  return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
3657
4006
  } catch {
3658
4007
  return [];
@@ -3661,8 +4010,8 @@ async function readIgnoreFile2(path) {
3661
4010
  async function buildMatcher2(root, extra) {
3662
4011
  const ig = ignore2();
3663
4012
  ig.add(DEFAULT_IGNORE);
3664
- ig.add(await readIgnoreFile2(join7(root, ".gitignore")));
3665
- ig.add(await readIgnoreFile2(join7(root, ".synthraignore")));
4013
+ ig.add(await readIgnoreFile2(join8(root, ".gitignore")));
4014
+ ig.add(await readIgnoreFile2(join8(root, ".synthraignore")));
3666
4015
  if (extra.length) ig.add(extra);
3667
4016
  return ig;
3668
4017
  }
@@ -3675,12 +4024,12 @@ async function* walk(root, options = {}) {
3675
4024
  async function* recurse(dir) {
3676
4025
  let entries;
3677
4026
  try {
3678
- entries = await readdir(dir, { withFileTypes: true });
4027
+ entries = await readdir2(dir, { withFileTypes: true });
3679
4028
  } catch {
3680
4029
  return;
3681
4030
  }
3682
4031
  for (const entry of entries) {
3683
- const abs = join7(dir, entry.name);
4032
+ const abs = join8(dir, entry.name);
3684
4033
  const rel = relative2(root, abs);
3685
4034
  if (!rel) continue;
3686
4035
  const relPosix = toPosix2(rel);
@@ -3707,38 +4056,38 @@ async function* walk(root, options = {}) {
3707
4056
  }
3708
4057
 
3709
4058
  // src/graph/store.ts
3710
- import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
3711
- import { dirname as dirname7 } from "path";
4059
+ import { mkdir as mkdir6, readFile as readFile11, writeFile as writeFile5 } from "fs/promises";
4060
+ import { dirname as dirname8 } from "path";
3712
4061
  async function writeJson(path, data, pretty) {
3713
- await mkdir6(dirname7(path), { recursive: true });
4062
+ await mkdir6(dirname8(path), { recursive: true });
3714
4063
  const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
3715
4064
  await writeFile5(path, text + "\n", "utf8");
3716
4065
  }
3717
- async function readJson(path) {
3718
- const text = await readFile10(path, "utf8");
4066
+ async function readJson2(path) {
4067
+ const text = await readFile11(path, "utf8");
3719
4068
  return JSON.parse(text);
3720
4069
  }
3721
4070
  async function writeGraph(path, graph) {
3722
4071
  await writeJson(path, graph, false);
3723
4072
  }
3724
4073
  async function readGraph(path) {
3725
- return readJson(path);
4074
+ return readJson2(path);
3726
4075
  }
3727
4076
  async function writeSymbolIndex(path, index) {
3728
4077
  await writeJson(path, index, true);
3729
4078
  }
3730
4079
  async function readSymbolIndex(path) {
3731
- const parsed = await readJson(path);
4080
+ const parsed = await readJson2(path);
3732
4081
  return Object.assign(/* @__PURE__ */ Object.create(null), parsed);
3733
4082
  }
3734
4083
 
3735
4084
  // src/cli/bootstrap.ts
3736
- import { mkdir as mkdir7, readFile as readFile12, stat as stat2, writeFile as writeFile7 } from "fs/promises";
3737
- import { basename as basename4 } from "path";
4085
+ import { mkdir as mkdir7, readFile as readFile13, stat as stat2, writeFile as writeFile7 } from "fs/promises";
4086
+ import { basename as basename5 } from "path";
3738
4087
 
3739
4088
  // src/hooks/claude-md.ts
3740
- import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
3741
- import { basename as basename3, dirname as dirname8 } from "path";
4089
+ import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
4090
+ import { basename as basename4, dirname as dirname9 } from "path";
3742
4091
  var POLICY_VERSION = 7;
3743
4092
  var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
3744
4093
  var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
@@ -3905,13 +4254,13 @@ function onboardingSkeleton(projectName) {
3905
4254
  async function patchClaudeMd(path, projectName) {
3906
4255
  let existing;
3907
4256
  try {
3908
- existing = await readFile11(path, "utf8");
4257
+ existing = await readFile12(path, "utf8");
3909
4258
  } catch {
3910
4259
  existing = null;
3911
4260
  }
3912
4261
  const block = policyBlock();
3913
4262
  if (existing === null) {
3914
- const name = projectName || basename3(dirname8(path)) || "this project";
4263
+ const name = projectName || basename4(dirname9(path)) || "this project";
3915
4264
  await writeFile6(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
3916
4265
  return { created: true, updated: false, skipped: false };
3917
4266
  }
@@ -3952,7 +4301,7 @@ async function ensureDir(path) {
3952
4301
  async function patchGitignore(path) {
3953
4302
  let existing = "";
3954
4303
  try {
3955
- existing = await readFile12(path, "utf8");
4304
+ existing = await readFile13(path, "utf8");
3956
4305
  } catch {
3957
4306
  }
3958
4307
  const trimmed = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
@@ -3969,7 +4318,7 @@ async function bootstrap(paths) {
3969
4318
  const contextCreated = await ensureDir(paths.contextDir);
3970
4319
  const gitignoreUpdated = await patchGitignore(paths.gitignore);
3971
4320
  const claudeMdExistedBefore = await exists(paths.claudeMd);
3972
- const patch = await patchClaudeMd(paths.claudeMd, basename4(paths.projectRoot));
4321
+ const patch = await patchClaudeMd(paths.claudeMd, basename5(paths.projectRoot));
3973
4322
  return {
3974
4323
  graphCreated,
3975
4324
  contextCreated,
@@ -4036,7 +4385,7 @@ async function scanProject(projectRootRaw, opts = {}) {
4036
4385
  if (verbose) log.info(` walked ${walked.length} files`);
4037
4386
  const parsable = walked.filter((f) => PARSABLE_EXTS.has(f.ext));
4038
4387
  const prevCache = await readParseCache(paths.parseCache);
4039
- const { parsed, cache, reused, reparsed, parseErrors } = await incrementalParse(
4388
+ const { parsed, cache: cache2, reused, reparsed, parseErrors } = await incrementalParse(
4040
4389
  parsable,
4041
4390
  prevCache,
4042
4391
  { full: opts.full }
@@ -4050,7 +4399,7 @@ async function scanProject(projectRootRaw, opts = {}) {
4050
4399
  const symbolIndex = buildSymbolIndex(graph);
4051
4400
  await writeGraph(paths.infoGraph, graph);
4052
4401
  await writeSymbolIndex(paths.symbolIndex, symbolIndex);
4053
- await writeParseCache(paths.parseCache, cache);
4402
+ await writeParseCache(paths.parseCache, cache2);
4054
4403
  if (verbose) {
4055
4404
  log.info(
4056
4405
  ` wrote ${paths.infoGraph} \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges`
@@ -4130,7 +4479,7 @@ var LearnRuntime = class _LearnRuntime {
4130
4479
 
4131
4480
  // src/server/mcp.ts
4132
4481
  import { appendFile as appendFile3, mkdir as mkdir10 } from "fs/promises";
4133
- import { dirname as dirname11 } from "path";
4482
+ import { dirname as dirname12 } from "path";
4134
4483
 
4135
4484
  // src/graph/rank.ts
4136
4485
  var KW_BASE_WEIGHT = 2;
@@ -4383,14 +4732,14 @@ async function retrieve(graph, query, options = {}) {
4383
4732
 
4384
4733
  // src/memory/branches.ts
4385
4734
  import { execFile as execFile2 } from "child_process";
4386
- import { readFile as readFile13 } from "fs/promises";
4387
- import { join as join8 } from "path";
4735
+ import { readFile as readFile14 } from "fs/promises";
4736
+ import { join as join9 } from "path";
4388
4737
  import { promisify as promisify2 } from "util";
4389
4738
  var execFileAsync2 = promisify2(execFile2);
4390
4739
  async function currentBranch(projectRoot) {
4391
4740
  try {
4392
- const headPath = join8(projectRoot, ".git", "HEAD");
4393
- const head = await readFile13(headPath, "utf8");
4741
+ const headPath = join9(projectRoot, ".git", "HEAD");
4742
+ const head = await readFile14(headPath, "utf8");
4394
4743
  const trimmed = head.trim();
4395
4744
  const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
4396
4745
  if (match?.[1]) return match[1];
@@ -4426,22 +4775,22 @@ function sanitizeBranchName(name) {
4426
4775
  function resolveBranchPaths(contextDir, branch, isDefault) {
4427
4776
  if (isDefault) {
4428
4777
  return {
4429
- contextStore: join8(contextDir, "context-store.json"),
4430
- contextMd: join8(contextDir, "CONTEXT.md"),
4778
+ contextStore: join9(contextDir, "context-store.json"),
4779
+ contextMd: join9(contextDir, "CONTEXT.md"),
4431
4780
  branchDir: null
4432
4781
  };
4433
4782
  }
4434
- const branchDir = join8(contextDir, "branches", sanitizeBranchName(branch));
4783
+ const branchDir = join9(contextDir, "branches", sanitizeBranchName(branch));
4435
4784
  return {
4436
- contextStore: join8(branchDir, "context-store.json"),
4437
- contextMd: join8(branchDir, "CONTEXT.md"),
4785
+ contextStore: join9(branchDir, "context-store.json"),
4786
+ contextMd: join9(branchDir, "CONTEXT.md"),
4438
4787
  branchDir
4439
4788
  };
4440
4789
  }
4441
4790
 
4442
4791
  // src/memory/context-md.ts
4443
- import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
4444
- import { dirname as dirname9 } from "path";
4792
+ import { mkdir as mkdir8, readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
4793
+ import { dirname as dirname10 } from "path";
4445
4794
  var MAX_BULLETS = 3;
4446
4795
  function deriveContextMd(entries, branch) {
4447
4796
  const tasks = entries.filter((e) => e.type === "task").reverse();
@@ -4484,17 +4833,17 @@ function formatContextMd(ctx) {
4484
4833
  return lines.join("\n");
4485
4834
  }
4486
4835
  async function writeContextMd(path, ctx) {
4487
- await mkdir8(dirname9(path), { recursive: true });
4836
+ await mkdir8(dirname10(path), { recursive: true });
4488
4837
  await writeFile8(path, formatContextMd(ctx), "utf8");
4489
4838
  }
4490
4839
 
4491
4840
  // src/memory/context-store.ts
4492
- import { mkdir as mkdir9, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
4493
- import { dirname as dirname10 } from "path";
4841
+ import { mkdir as mkdir9, readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
4842
+ import { dirname as dirname11 } from "path";
4494
4843
  var SCHEMA_VERSION3 = 1;
4495
4844
  async function readEntries(path) {
4496
4845
  try {
4497
- const raw = await readFile15(path, "utf8");
4846
+ const raw = await readFile16(path, "utf8");
4498
4847
  const parsed = JSON.parse(raw);
4499
4848
  return Array.isArray(parsed.entries) ? parsed.entries : [];
4500
4849
  } catch {
@@ -4502,7 +4851,7 @@ async function readEntries(path) {
4502
4851
  }
4503
4852
  }
4504
4853
  async function writeEntries(path, entries) {
4505
- await mkdir9(dirname10(path), { recursive: true });
4854
+ await mkdir9(dirname11(path), { recursive: true });
4506
4855
  const store = { schema_version: SCHEMA_VERSION3, entries };
4507
4856
  await writeFile9(path, JSON.stringify(store, null, 2) + "\n", "utf8");
4508
4857
  }
@@ -5141,10 +5490,10 @@ function buildDepsFooter(symbol, graph, maxChars = loadConfig().readDepsMaxChars
5141
5490
  let cUsed = used + sep3 + head.length;
5142
5491
  for (const c of callers.slice(0, DEPS_MAX_CALLERS)) {
5143
5492
  const part = `${c.name} \u2192 ${c.file}`;
5144
- const join12 = shown.length > 0 ? 3 : 0;
5145
- if (cUsed + join12 + part.length > maxChars) break;
5493
+ const join13 = shown.length > 0 ? 3 : 0;
5494
+ if (cUsed + join13 + part.length > maxChars) break;
5146
5495
  shown.push(part);
5147
- cUsed += join12 + part.length;
5496
+ cUsed += join13 + part.length;
5148
5497
  }
5149
5498
  if (lines.length > 0) lines.push("");
5150
5499
  if (shown.length > 0) {
@@ -5288,7 +5637,7 @@ async function contextRecall(args, ctx) {
5288
5637
  }
5289
5638
  async function logToolCall(ctx, tool) {
5290
5639
  try {
5291
- await mkdir10(dirname11(ctx.paths.toolLog), { recursive: true });
5640
+ await mkdir10(dirname12(ctx.paths.toolLog), { recursive: true });
5292
5641
  await appendFile3(
5293
5642
  ctx.paths.toolLog,
5294
5643
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
@@ -5387,12 +5736,12 @@ async function getCommitsSince(projectRoot, sinceIso) {
5387
5736
  }
5388
5737
 
5389
5738
  // src/memory/session.ts
5390
- import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
5391
- import { dirname as dirname12 } from "path";
5739
+ import { mkdir as mkdir11, readFile as readFile17, writeFile as writeFile10 } from "fs/promises";
5740
+ import { dirname as dirname13 } from "path";
5392
5741
  var SESSION_SCHEMA_VERSION = 1;
5393
5742
  async function readSession(path) {
5394
5743
  try {
5395
- const raw = await readFile16(path, "utf8");
5744
+ const raw = await readFile17(path, "utf8");
5396
5745
  const parsed = JSON.parse(raw);
5397
5746
  if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
5398
5747
  return parsed;
@@ -5401,7 +5750,7 @@ async function readSession(path) {
5401
5750
  }
5402
5751
  }
5403
5752
  async function writeSession(path, state) {
5404
- await mkdir11(dirname12(path), { recursive: true });
5753
+ await mkdir11(dirname13(path), { recursive: true });
5405
5754
  await writeFile10(path, JSON.stringify(state, null, 2) + "\n", "utf8");
5406
5755
  }
5407
5756
 
@@ -5448,7 +5797,7 @@ async function handleContextUpdate(req, ctx) {
5448
5797
 
5449
5798
  // src/server/routes/gate.ts
5450
5799
  import { appendFile as appendFile4, mkdir as mkdir12 } from "fs/promises";
5451
- import { dirname as dirname13 } from "path";
5800
+ import { dirname as dirname14 } from "path";
5452
5801
  var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
5453
5802
  var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
5454
5803
  function extractQuery(toolName, input) {
@@ -5510,7 +5859,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
5510
5859
  var LOG_REASON_MAX_CHARS = 240;
5511
5860
  async function logDecision(ctx, toolName, query, decision, reason, hintChars) {
5512
5861
  try {
5513
- await mkdir12(dirname13(ctx.paths.gateLog), { recursive: true });
5862
+ await mkdir12(dirname14(ctx.paths.gateLog), { recursive: true });
5514
5863
  const entry = {
5515
5864
  ts: (/* @__PURE__ */ new Date()).toISOString(),
5516
5865
  tool: toolName,
@@ -5638,14 +5987,14 @@ async function handleGate(req, ctx) {
5638
5987
 
5639
5988
  // src/server/routes/log.ts
5640
5989
  import { appendFile as appendFile5, mkdir as mkdir13 } from "fs/promises";
5641
- import { dirname as dirname14 } from "path";
5990
+ import { dirname as dirname15 } from "path";
5642
5991
  async function handleLog(entry, ctx) {
5643
5992
  if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
5644
5993
  throw new Error("log: input_tokens and output_tokens (number) are required");
5645
5994
  }
5646
5995
  const written_at = (/* @__PURE__ */ new Date()).toISOString();
5647
5996
  const record = { ...entry, written_at };
5648
- await mkdir13(dirname14(ctx.paths.tokenLog), { recursive: true });
5997
+ await mkdir13(dirname15(ctx.paths.tokenLog), { recursive: true });
5649
5998
  await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
5650
5999
  return { ok: true, written_at };
5651
6000
  }
@@ -5878,18 +6227,18 @@ async function startServer(paths, options = {}) {
5878
6227
  }
5879
6228
 
5880
6229
  // src/cli/session-discovery.ts
5881
- import { readdir as readdir2, stat as stat3 } from "fs/promises";
5882
- import { homedir as homedir2 } from "os";
5883
- import { join as join9 } from "path";
6230
+ import { readdir as readdir3, stat as stat3 } from "fs/promises";
6231
+ import { homedir as homedir3 } from "os";
6232
+ import { join as join10 } from "path";
5884
6233
  function encodeProjectPath(projectRoot) {
5885
6234
  return projectRoot.replace(/[\\/:]/g, "-");
5886
6235
  }
5887
6236
  async function findLatestSession(projectRoot) {
5888
6237
  const encoded = encodeProjectPath(projectRoot);
5889
- const dir = join9(homedir2(), ".claude", "projects", encoded);
6238
+ const dir = join10(homedir3(), ".claude", "projects", encoded);
5890
6239
  let entries;
5891
6240
  try {
5892
- entries = await readdir2(dir);
6241
+ entries = await readdir3(dir);
5893
6242
  } catch {
5894
6243
  return null;
5895
6244
  }
@@ -5897,7 +6246,7 @@ async function findLatestSession(projectRoot) {
5897
6246
  if (jsonlFiles.length === 0) return null;
5898
6247
  let latest = null;
5899
6248
  for (const file of jsonlFiles) {
5900
- const path = join9(dir, file);
6249
+ const path = join10(dir, file);
5901
6250
  try {
5902
6251
  const s = await stat3(path);
5903
6252
  if (!latest || s.mtime > latest.modifiedAt) {
@@ -5952,8 +6301,8 @@ async function dashboardCommand(rawPath) {
5952
6301
  }
5953
6302
 
5954
6303
  // src/cli/doctor-command.ts
5955
- import { readFile as readFile17, stat as stat4 } from "fs/promises";
5956
- import { join as join10, resolve as resolve3 } from "path";
6304
+ import { readFile as readFile18, stat as stat4 } from "fs/promises";
6305
+ import { join as join11, resolve as resolve3 } from "path";
5957
6306
  import spawn from "cross-spawn";
5958
6307
  var ICON = { ok: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
5959
6308
  function binWorks(bin, args) {
@@ -6021,7 +6370,7 @@ async function runDoctorChecks(projectRoot) {
6021
6370
  });
6022
6371
  } else {
6023
6372
  try {
6024
- const graph = JSON.parse(await readFile17(paths.infoGraph, "utf8"));
6373
+ const graph = JSON.parse(await readFile18(paths.infoGraph, "utf8"));
6025
6374
  const parts = [`${graph.symbol_count} symbols`, `${graph.file_count} files`];
6026
6375
  let status = "ok";
6027
6376
  const ageMs = Date.now() - Date.parse(graph.generated_at);
@@ -6045,7 +6394,7 @@ async function runDoctorChecks(projectRoot) {
6045
6394
  }
6046
6395
  }
6047
6396
  checks.push(
6048
- await exists2(join10(projectRoot, ".mcp.json")) ? {
6397
+ await exists2(join11(projectRoot, ".mcp.json")) ? {
6049
6398
  status: "ok",
6050
6399
  label: "MCP registration",
6051
6400
  detail: ".mcp.json present (IDE can see graph_* tools)"
@@ -6062,7 +6411,7 @@ async function runDoctorChecks(projectRoot) {
6062
6411
  detail: "no CLAUDE.md \u2014 run `syn .` to scaffold + inject the policy block."
6063
6412
  });
6064
6413
  } else {
6065
- const md = await readFile17(paths.claudeMd, "utf8");
6414
+ const md = await readFile18(paths.claudeMd, "utf8");
6066
6415
  if (md.includes(`synthra-policy v${POLICY_VERSION} BEGIN`)) {
6067
6416
  checks.push({
6068
6417
  status: "ok",
@@ -6085,7 +6434,7 @@ async function runDoctorChecks(projectRoot) {
6085
6434
  detail: "no .claude/settings.local.json \u2014 run `syn .` to install hooks."
6086
6435
  });
6087
6436
  } else {
6088
- const s = await readFile17(paths.claudeSettings, "utf8");
6437
+ const s = await readFile18(paths.claudeSettings, "utf8");
6089
6438
  checks.push(
6090
6439
  s.includes("synthra-hook=true") ? { status: "ok", label: "Hooks", detail: "registered in .claude/settings.local.json" } : {
6091
6440
  status: "warn",
@@ -6115,14 +6464,14 @@ async function doctorCommand(rawPath) {
6115
6464
  }
6116
6465
 
6117
6466
  // src/cli/self-update.ts
6118
- import { mkdir as mkdir14, readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
6119
- import { homedir as homedir3 } from "os";
6120
- import { join as join11 } from "path";
6467
+ import { mkdir as mkdir14, readFile as readFile19, writeFile as writeFile12 } from "fs/promises";
6468
+ import { homedir as homedir4 } from "os";
6469
+ import { join as join12 } from "path";
6121
6470
  import { createInterface } from "readline/promises";
6122
6471
  import spawn2 from "cross-spawn";
6123
6472
  var PKG_NAME = "@jefuriiij/synthra";
6124
- var SYNTHRA_DIR = join11(homedir3(), ".synthra");
6125
- var LAST_SEEN_PATH = join11(SYNTHRA_DIR, "last-seen-version.json");
6473
+ var SYNTHRA_DIR = join12(homedir4(), ".synthra");
6474
+ var LAST_SEEN_PATH = join12(SYNTHRA_DIR, "last-seen-version.json");
6126
6475
  var REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PKG_NAME)}/latest`;
6127
6476
  var FETCH_TIMEOUT_MS = 2e3;
6128
6477
  var currentVersionCache = null;
@@ -6173,7 +6522,7 @@ async function checkForUpdate() {
6173
6522
  }
6174
6523
  async function readLastSeen() {
6175
6524
  try {
6176
- const raw = await readFile18(LAST_SEEN_PATH, "utf8");
6525
+ const raw = await readFile19(LAST_SEEN_PATH, "utf8");
6177
6526
  const parsed = JSON.parse(raw);
6178
6527
  return parsed.version ?? null;
6179
6528
  } catch {
@@ -6216,7 +6565,7 @@ async function readInstalledChangelog() {
6216
6565
  const root = await npmGlobalRoot();
6217
6566
  if (!root) return null;
6218
6567
  try {
6219
- return await readFile18(join11(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
6568
+ return await readFile19(join12(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
6220
6569
  } catch {
6221
6570
  return null;
6222
6571
  }