@jefuriiij/synthra 0.2.1 → 0.3.1

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.2.1",
21
+ version: "0.3.1",
22
22
  publishConfig: {
23
23
  access: "public"
24
24
  },
@@ -146,7 +146,125 @@ function isFree(port) {
146
146
  }
147
147
 
148
148
  // src/dashboard/delta.ts
149
- import { readFile as readFile2 } from "fs/promises";
149
+ import { readFile as readFile3 } from "fs/promises";
150
+
151
+ // src/learn/store.ts
152
+ import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
153
+ import { dirname } from "path";
154
+
155
+ // src/learn/usage.ts
156
+ var LEARN_SCHEMA_VERSION = 1;
157
+ var DAY_MS = 24 * 60 * 60 * 1e3;
158
+ function halfLifeMs() {
159
+ const env = Number(process.env.SYN_LEARN_HALFLIFE_DAYS);
160
+ const days = Number.isFinite(env) && env > 0 ? env : 7;
161
+ return days * DAY_MS;
162
+ }
163
+ function weightFor(source) {
164
+ switch (source) {
165
+ case "register_edit":
166
+ return 2;
167
+ case "read":
168
+ return 1;
169
+ default:
170
+ return 0;
171
+ }
172
+ }
173
+ function emptyStore() {
174
+ return {
175
+ schema_version: LEARN_SCHEMA_VERSION,
176
+ asOf: (/* @__PURE__ */ new Date(0)).toISOString(),
177
+ files: {}
178
+ };
179
+ }
180
+ function decayFactor(fromTs, toMs, hl) {
181
+ const fromMs = Date.parse(fromTs);
182
+ if (!Number.isFinite(fromMs)) return 1;
183
+ const dt = toMs - fromMs;
184
+ if (dt <= 0) return 1;
185
+ return Math.exp(-(Math.LN2 / hl) * dt);
186
+ }
187
+ function foldEvent(store, ev) {
188
+ const w = weightFor(ev.source);
189
+ if (w <= 0 || !ev.path) return store;
190
+ const tMs = Date.parse(ev.ts);
191
+ if (!Number.isFinite(tMs)) return store;
192
+ const hl = halfLifeMs();
193
+ const prev = store.files[ev.path];
194
+ if (prev) {
195
+ const decayed = prev.decayed * decayFactor(prev.lastTs, tMs, hl) + w;
196
+ store.files[ev.path] = { count: prev.count + 1, decayed, lastTs: ev.ts };
197
+ } else {
198
+ store.files[ev.path] = { count: 1, decayed: w, lastTs: ev.ts };
199
+ }
200
+ return store;
201
+ }
202
+ function effectiveScores(store, nowMs) {
203
+ const hl = halfLifeMs();
204
+ const out = /* @__PURE__ */ new Map();
205
+ for (const [path, stat6] of Object.entries(store.files)) {
206
+ const eff = stat6.decayed * decayFactor(stat6.lastTs, nowMs, hl);
207
+ if (eff > 0.01) out.set(path, eff);
208
+ }
209
+ return out;
210
+ }
211
+ function recomputeFromLog(events) {
212
+ const store = emptyStore();
213
+ for (const ev of events) foldEvent(store, ev);
214
+ return store;
215
+ }
216
+
217
+ // src/learn/store.ts
218
+ async function readLearnStore(path) {
219
+ try {
220
+ const raw = await readFile(path, "utf8");
221
+ const parsed = JSON.parse(raw);
222
+ if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
223
+ return emptyStore();
224
+ }
225
+ return {
226
+ schema_version: LEARN_SCHEMA_VERSION,
227
+ asOf: typeof parsed.asOf === "string" ? parsed.asOf : emptyStore().asOf,
228
+ files: parsed.files
229
+ };
230
+ } catch {
231
+ return emptyStore();
232
+ }
233
+ }
234
+ async function writeLearnStore(path, store) {
235
+ try {
236
+ await mkdir(dirname(path), { recursive: true });
237
+ await writeFile(path, JSON.stringify(store, null, 2) + "\n", "utf8");
238
+ } catch {
239
+ }
240
+ }
241
+ async function readAccessLog(path) {
242
+ try {
243
+ const raw = await readFile(path, "utf8");
244
+ const out = [];
245
+ for (const line of raw.split("\n")) {
246
+ const t = line.trim();
247
+ if (!t) continue;
248
+ try {
249
+ const ev = JSON.parse(t);
250
+ if (ev && typeof ev.ts === "string" && typeof ev.path === "string" && typeof ev.source === "string") {
251
+ out.push(ev);
252
+ }
253
+ } catch {
254
+ }
255
+ }
256
+ return out;
257
+ } catch {
258
+ return [];
259
+ }
260
+ }
261
+ async function appendAccess(path, ev) {
262
+ try {
263
+ await mkdir(dirname(path), { recursive: true });
264
+ await appendFile(path, JSON.stringify(ev) + "\n", "utf8");
265
+ } catch {
266
+ }
267
+ }
150
268
 
151
269
  // src/shared/paths.ts
152
270
  import { join } from "path";
@@ -167,6 +285,7 @@ function resolvePaths(projectRoot) {
167
285
  toolLog: join(graphDir, "tool_log.jsonl"),
168
286
  accessLog: join(graphDir, "access_log.jsonl"),
169
287
  learnStore: join(graphDir, "learn_store.json"),
288
+ parseCache: join(graphDir, "parse_cache.json"),
170
289
  mcpPort: join(graphDir, "mcp_port"),
171
290
  mcpServerLog: join(graphDir, "mcp_server.log"),
172
291
  mcpServerErrLog: join(graphDir, "mcp_server.err.log"),
@@ -209,15 +328,15 @@ function estimateCostUsd(usage) {
209
328
  }
210
329
 
211
330
  // src/shared/project-registry.ts
212
- import { mkdir, readFile, writeFile } from "fs/promises";
331
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
213
332
  import { homedir } from "os";
214
- import { basename, dirname, join as join2 } from "path";
333
+ import { basename, dirname as dirname2, join as join2 } from "path";
215
334
  var REGISTRY_DIR = join2(homedir(), ".synthra");
216
335
  var REGISTRY_PATH = join2(REGISTRY_DIR, "projects.json");
217
336
  var SCHEMA_VERSION = 1;
218
337
  async function readRegistry() {
219
338
  try {
220
- const raw = await readFile(REGISTRY_PATH, "utf8");
339
+ const raw = await readFile2(REGISTRY_PATH, "utf8");
221
340
  const parsed = JSON.parse(raw);
222
341
  if (!Array.isArray(parsed.projects)) return { schema_version: SCHEMA_VERSION, projects: [] };
223
342
  return { schema_version: parsed.schema_version ?? SCHEMA_VERSION, projects: parsed.projects };
@@ -226,8 +345,8 @@ async function readRegistry() {
226
345
  }
227
346
  }
228
347
  async function writeRegistry(registry) {
229
- await mkdir(dirname(REGISTRY_PATH), { recursive: true });
230
- await writeFile(REGISTRY_PATH, JSON.stringify(registry, null, 2) + "\n", "utf8");
348
+ await mkdir2(dirname2(REGISTRY_PATH), { recursive: true });
349
+ await writeFile2(REGISTRY_PATH, JSON.stringify(registry, null, 2) + "\n", "utf8");
231
350
  }
232
351
  async function recordProject(projectRoot) {
233
352
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -264,9 +383,12 @@ function countToolCalls(entries) {
264
383
  }
265
384
  return out;
266
385
  }
386
+ function topHotFiles(store, nowMs, limit = 8) {
387
+ return [...effectiveScores(store, nowMs).entries()].map(([path, score2]) => ({ path, score: Math.round(score2 * 10) / 10 })).sort((a, b) => b.score - a.score).slice(0, limit);
388
+ }
267
389
  async function readJsonl(path) {
268
390
  try {
269
- const text = await readFile2(path, "utf8");
391
+ const text = await readFile3(path, "utf8");
270
392
  return text.split(/\r?\n/).filter((l) => l.length > 0).map((l) => {
271
393
  try {
272
394
  return JSON.parse(l);
@@ -299,6 +421,7 @@ function summarize(p) {
299
421
  }
300
422
  const blocked = p.gates.filter((g) => g.decision === "block").length;
301
423
  const saved = blocked * AVG_TOKENS_PER_BLOCKED_GREP;
424
+ const now = Date.now();
302
425
  return {
303
426
  path: p.path,
304
427
  name: p.name,
@@ -314,6 +437,8 @@ function summarize(p) {
314
437
  estimated_cost_usd: Math.round(costUsd * 100) / 100,
315
438
  total_tool_calls: p.tools.length,
316
439
  tool_calls: countToolCalls(p.tools),
440
+ hot_files: topHotFiles(p.learn, now),
441
+ hot_files_total: effectiveScores(p.learn, now).size,
317
442
  models
318
443
  };
319
444
  }
@@ -323,13 +448,14 @@ function dedupeEnabled() {
323
448
  }
324
449
  async function loadProjectFiles(path, name, lastSeen) {
325
450
  const paths = resolvePaths(path);
326
- const [rawTokens, gates, tools] = await Promise.all([
451
+ const [rawTokens, gates, tools, learn] = await Promise.all([
327
452
  readJsonl(paths.tokenLog),
328
453
  readJsonl(paths.gateLog),
329
- readJsonl(paths.toolLog)
454
+ readJsonl(paths.toolLog),
455
+ readLearnStore(paths.learnStore)
330
456
  ]);
331
457
  const tokens = dedupeEnabled() ? dedupeTokens(rawTokens) : rawTokens;
332
- return { path, name, last_seen: lastSeen, tokens, gates, tools };
458
+ return { path, name, last_seen: lastSeen, tokens, gates, tools, learn };
333
459
  }
334
460
  function dedupeTokens(entries) {
335
461
  const score2 = (model) => {
@@ -392,7 +518,8 @@ async function computeDashboardData(activePaths, recentN = 500) {
392
518
  last_seen: null,
393
519
  tokens: [],
394
520
  gates: [],
395
- tools: []
521
+ tools: [],
522
+ learn: emptyStore()
396
523
  };
397
524
  const activeStats = summarize(activeFiles);
398
525
  let g_in = 0, g_out = 0, g_cr = 0, g_cc = 0, g_gate = 0, g_block = 0, g_cost = 0, g_turns = 0, g_tools = 0;
@@ -477,6 +604,7 @@ var public_default = `<!doctype html>
477
604
  <meta charset="UTF-8" />
478
605
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
479
606
  <title>Synthra \xB7 Dashboard</title>
607
+ <link rel="icon" href="./favicon.svg" type="image/svg+xml" />
480
608
  <link rel="preconnect" href="https://fonts.googleapis.com">
481
609
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
482
610
  <link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
@@ -571,6 +699,9 @@ var public_default = `<!doctype html>
571
699
  </div>
572
700
  </div>
573
701
 
702
+ <!-- Savings + Cost, side by side -->
703
+ <div class="hero-row">
704
+
574
705
  <!-- Savings hero -->
575
706
  <div class="card savings has-tooltip" data-tooltip="What Synthra has saved you, as a deliberately conservative floor estimate. Each time the gate blocks an exploratory Grep/Glob, we credit 500 tokens \xD7 $3 per million-token input rate. Real savings are usually higher because the formula ignores cache thrash and follow-up Reads that the block also prevents. The audit line below shows the exact math live.">
576
707
  <div class="card-head">
@@ -598,6 +729,26 @@ var public_default = `<!doctype html>
598
729
  </div>
599
730
  </div>
600
731
 
732
+ <!-- Cost hero (moved beside Savings) -->
733
+ <div class="card cost-hero has-tooltip" data-tooltip="Your all-time Claude spend across every project Synthra has tracked on this machine. Token counts come from Claude's transcript JSONL files; dollar amounts are computed by multiplying those counts by Anthropic's published per-model rates. See the FAQ for full rate tables.">
734
+ <div class="card-head">
735
+ <div class="card-eyebrow">Total spend \xB7 <em>all time</em></div>
736
+ </div>
737
+ <div class="big-money" id="big-cost">$0.<em>00</em></div>
738
+ <div class="cost-sub">
739
+ <div class="cs-row">
740
+ <span class="cs-k">Tokens (in+out)</span>
741
+ <span class="cs-v" id="cs-tokens">0</span>
742
+ </div>
743
+ <div class="cs-row">
744
+ <span class="cs-k">Avg / turn</span>
745
+ <span class="cs-v" id="cs-avg">$0.00</span>
746
+ </div>
747
+ </div>
748
+ </div>
749
+
750
+ </div><!-- /hero-row -->
751
+
601
752
  <!-- Recent turns -->
602
753
  <div class="card turns-card has-tooltip" data-tooltip="Every conversational turn Synthra has observed across all your projects, newest first. Each row shows when, which project, which model, and how the cost broke down between fresh input, generated output, and cache.">
603
754
  <div class="card-head">
@@ -633,24 +784,6 @@ var public_default = `<!doctype html>
633
784
  <!-- ===== Right ===== -->
634
785
  <aside class="col-right">
635
786
 
636
- <!-- Cost hero -->
637
- <div class="card cost-hero has-tooltip" data-tooltip="Your all-time Claude spend across every project Synthra has tracked on this machine. Token counts come from Claude's transcript JSONL files; dollar amounts are computed by multiplying those counts by Anthropic's published per-model rates. See the FAQ for full rate tables.">
638
- <div class="card-head">
639
- <div class="card-eyebrow">Total spend \xB7 <em>all time</em></div>
640
- </div>
641
- <div class="big-money" id="big-cost">$0.<em>00</em></div>
642
- <div class="cost-sub">
643
- <div class="cs-row">
644
- <span class="cs-k">Tokens (in+out)</span>
645
- <span class="cs-v" id="cs-tokens">0</span>
646
- </div>
647
- <div class="cs-row">
648
- <span class="cs-k">Avg / turn</span>
649
- <span class="cs-v" id="cs-avg">$0.00</span>
650
- </div>
651
- </div>
652
- </div>
653
-
654
787
  <!-- Graph tool usage -->
655
788
  <div class="card tools-card has-tooltip" data-tooltip="How often Claude actually used Synthra's graph tools (graph_continue / graph_read / \u2026) across all projects. A positive usage signal \u2014 unlike the Moat's block count, it captures every time Claude reached for the graph instead of running a Grep.">
656
789
  <div class="card-head">
@@ -671,6 +804,16 @@ var public_default = `<!doctype html>
671
804
  <div class="gate-mini" id="gate-mini"></div>
672
805
  </div>
673
806
 
807
+ <!-- Hot files (usage-learning) -->
808
+ <div class="card tools-card has-tooltip" data-tooltip="Files Synthra has learned you work in most, weighted by recent use \u2014 it ranks these higher in retrieval. Scoped to the active project; decays over ~7 days, so the list reflects what's hot now.">
809
+ <div class="card-head">
810
+ <div class="card-eyebrow">Hot <em>files</em></div>
811
+ <div class="card-meta" id="hot-files-project">active project</div>
812
+ </div>
813
+ <div class="moat-value"><span id="hot-files-total">0</span> <em>tracked</em></div>
814
+ <div class="cost-sub" id="hot-files-list"></div>
815
+ </div>
816
+
674
817
  </aside>
675
818
  </main>
676
819
 
@@ -1042,6 +1185,40 @@ var public_default = `<!doctype html>
1042
1185
  el.appendChild(frag);
1043
1186
  }
1044
1187
 
1188
+ function shortenPath(p) {
1189
+ const parts = String(p).split('/');
1190
+ return parts.length <= 2 ? p : '\u2026/' + parts.slice(-2).join('/');
1191
+ }
1192
+
1193
+ function renderHotFiles(active) {
1194
+ const stats = (active && active.stats) || {};
1195
+ const files = stats.hot_files || [];
1196
+ $('#hot-files-total').innerHTML = fmt(stats.hot_files_total || 0);
1197
+ const proj = $('#hot-files-project');
1198
+ if (proj) proj.textContent = (active && active.project_name) || 'active project';
1199
+ const el = $('#hot-files-list');
1200
+ el.innerHTML = '';
1201
+ if (!files.length) {
1202
+ el.innerHTML = '<div class="empty">No usage learned yet \u2014 Synthra learns as you read/edit files.</div>';
1203
+ return;
1204
+ }
1205
+ const frag = document.createDocumentFragment();
1206
+ for (const f of files) {
1207
+ const row = document.createElement('div');
1208
+ row.className = 'cs-row';
1209
+ row.title = f.path;
1210
+ const k = document.createElement('span');
1211
+ k.className = 'cs-k path';
1212
+ k.textContent = shortenPath(f.path);
1213
+ const v = document.createElement('span');
1214
+ v.className = 'cs-v';
1215
+ v.textContent = String(f.score);
1216
+ row.append(k, v);
1217
+ frag.appendChild(row);
1218
+ }
1219
+ el.appendChild(frag);
1220
+ }
1221
+
1045
1222
  function renderTurns(turns) {
1046
1223
  const tbody = $('#turns-body');
1047
1224
  const empty = $('#turns-empty');
@@ -1308,6 +1485,7 @@ var public_default = `<!doctype html>
1308
1485
  renderCostHero(data.global);
1309
1486
  renderMoat(data.global);
1310
1487
  renderToolUsage(data.global);
1488
+ renderHotFiles(data.active);
1311
1489
  renderTurns(turns);
1312
1490
  renderGateMini(gates);
1313
1491
 
@@ -1396,7 +1574,10 @@ var public_default = `<!doctype html>
1396
1574
  `;
1397
1575
 
1398
1576
  // src/dashboard/public/style.css
1399
- 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-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/* ============================================================\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.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.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.cs-v {\n color: var(--mist);\n}\n\n/* ============================================================\n Moat card\n ============================================================ */\n.moat {\n flex: 1;\n gap: 8px;\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 width: 6px;\n}\n\n.turns-scroll::-webkit-scrollbar-thumb,\n.proj-chart::-webkit-scrollbar-thumb,\n.gate-mini::-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 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\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';
1577
+ 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-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.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.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';
1578
+
1579
+ // src/dashboard/public/favicon.svg
1580
+ 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';
1400
1581
 
1401
1582
  // src/dashboard/server.ts
1402
1583
  var FALLBACK_RANGE = 9;
@@ -1416,6 +1597,11 @@ async function startDashboard(paths, preferredPort = 8901) {
1416
1597
  c.header("Cache-Control", "no-cache");
1417
1598
  return c.body(style_default);
1418
1599
  });
1600
+ app.get("/favicon.svg", (c) => {
1601
+ c.header("Content-Type", "image/svg+xml; charset=utf-8");
1602
+ c.header("Cache-Control", "public, max-age=86400");
1603
+ return c.body(favicon_default);
1604
+ });
1419
1605
  app.get("/health", (c) => c.json({ ok: true }));
1420
1606
  app.get("/data", async (c) => {
1421
1607
  const data = await computeDashboardData(paths, RECENT_N);
@@ -1434,8 +1620,8 @@ async function startDashboard(paths, preferredPort = 8901) {
1434
1620
  }
1435
1621
 
1436
1622
  // src/hooks/installer.ts
1437
- import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1438
- import { dirname as dirname2, join as join3 } from "path";
1623
+ import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1624
+ import { dirname as dirname3, join as join3 } from "path";
1439
1625
 
1440
1626
  // src/hooks/scripts/pre-compact.ps1
1441
1627
  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';
@@ -1739,7 +1925,7 @@ function chosenScriptExt() {
1739
1925
  }
1740
1926
  async function readSettings(path) {
1741
1927
  try {
1742
- const raw = await readFile3(path, "utf8");
1928
+ const raw = await readFile4(path, "utf8");
1743
1929
  return JSON.parse(raw);
1744
1930
  } catch {
1745
1931
  return {};
@@ -1778,18 +1964,18 @@ function mergeOurHooks(config, paths) {
1778
1964
  return config;
1779
1965
  }
1780
1966
  async function installHooks(paths) {
1781
- await mkdir2(paths.claudeHooksDir, { recursive: true });
1967
+ await mkdir3(paths.claudeHooksDir, { recursive: true });
1782
1968
  const scriptsWritten = [];
1783
1969
  for (const s of SCRIPTS) {
1784
1970
  const target = join3(paths.claudeHooksDir, `${s.baseName}${chosenScriptExt()}`);
1785
- await writeFile2(target, chosenScriptBody(s), "utf8");
1971
+ await writeFile3(target, chosenScriptBody(s), "utf8");
1786
1972
  scriptsWritten.push(target);
1787
1973
  }
1788
- await mkdir2(dirname2(paths.claudeSettings), { recursive: true });
1974
+ await mkdir3(dirname3(paths.claudeSettings), { recursive: true });
1789
1975
  const existing = await readSettings(paths.claudeSettings);
1790
1976
  const stripped = stripOurHooks(existing);
1791
1977
  const merged = mergeOurHooks(stripped, paths);
1792
- await writeFile2(paths.claudeSettings, JSON.stringify(merged, null, 2) + "\n", "utf8");
1978
+ await writeFile3(paths.claudeSettings, JSON.stringify(merged, null, 2) + "\n", "utf8");
1793
1979
  log.debug(`installed ${scriptsWritten.length} hook script(s) into ${paths.claudeHooksDir}`);
1794
1980
  return { scriptsWritten, settingsUpdated: true };
1795
1981
  }
@@ -1797,11 +1983,11 @@ async function installHooks(paths) {
1797
1983
  // src/server/http.ts
1798
1984
  import { serve as serve2 } from "@hono/node-server";
1799
1985
  import { Hono as Hono2 } from "hono";
1800
- import { writeFile as writeFile10 } from "fs/promises";
1986
+ import { writeFile as writeFile11 } from "fs/promises";
1801
1987
 
1802
1988
  // src/activity/activity-log.ts
1803
- import { appendFile, mkdir as mkdir3 } from "fs/promises";
1804
- import { dirname as dirname3 } from "path";
1989
+ import { appendFile as appendFile2, mkdir as mkdir4 } from "fs/promises";
1990
+ import { dirname as dirname4 } from "path";
1805
1991
  var DEFAULT_RING_SIZE = 100;
1806
1992
  var ActivityStore = class {
1807
1993
  ring = [];
@@ -1838,8 +2024,8 @@ var ActivityStore = class {
1838
2024
  }
1839
2025
  async persist(event) {
1840
2026
  try {
1841
- await mkdir3(dirname3(this.persistPath), { recursive: true });
1842
- await appendFile(this.persistPath, JSON.stringify(event) + "\n", "utf8");
2027
+ await mkdir4(dirname4(this.persistPath), { recursive: true });
2028
+ await appendFile2(this.persistPath, JSON.stringify(event) + "\n", "utf8");
1843
2029
  } catch {
1844
2030
  }
1845
2031
  }
@@ -1847,7 +2033,7 @@ var ActivityStore = class {
1847
2033
 
1848
2034
  // src/activity/file-watcher.ts
1849
2035
  import chokidar from "chokidar";
1850
- import { readFile as readFile4 } from "fs/promises";
2036
+ import { readFile as readFile5 } from "fs/promises";
1851
2037
  import { join as join4, relative, sep } from "path";
1852
2038
  import ignore from "ignore";
1853
2039
  var ALWAYS_IGNORE = [
@@ -1870,7 +2056,7 @@ var ALWAYS_IGNORE = [
1870
2056
  ];
1871
2057
  async function readIgnoreFile(path) {
1872
2058
  try {
1873
- const text = await readFile4(path, "utf8");
2059
+ const text = await readFile5(path, "utf8");
1874
2060
  return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
1875
2061
  } catch {
1876
2062
  return [];
@@ -1935,14 +2121,14 @@ function createFileWatcher(root, onEvent) {
1935
2121
  // src/activity/git-watcher.ts
1936
2122
  import { execFile } from "child_process";
1937
2123
  import { watch } from "fs";
1938
- import { readFile as readFile5 } from "fs/promises";
2124
+ import { readFile as readFile6 } from "fs/promises";
1939
2125
  import { join as join5 } from "path";
1940
2126
  import { promisify } from "util";
1941
2127
  var execFileAsync = promisify(execFile);
1942
2128
  var POLL_MS = 2e3;
1943
2129
  async function readHeadBranch(projectRoot) {
1944
2130
  try {
1945
- const head = await readFile5(join5(projectRoot, ".git", "HEAD"), "utf8");
2131
+ const head = await readFile6(join5(projectRoot, ".git", "HEAD"), "utf8");
1946
2132
  const m = head.trim().match(/^ref:\s+refs\/heads\/(.+)$/);
1947
2133
  return m?.[1] ?? null;
1948
2134
  } catch {
@@ -2041,10 +2227,10 @@ function parseStatusFiles(porcelain) {
2041
2227
  import { resolve } from "path";
2042
2228
 
2043
2229
  // src/scanner/extract.ts
2044
- import { dirname as dirname4, join as join6, posix } from "path";
2230
+ import { dirname as dirname5, join as join6, posix } from "path";
2045
2231
 
2046
2232
  // src/graph/types.ts
2047
- var SCHEMA_VERSION2 = 1;
2233
+ var SCHEMA_VERSION2 = 2;
2048
2234
 
2049
2235
  // src/scanner/hash.ts
2050
2236
  import { createHash } from "crypto";
@@ -2362,14 +2548,20 @@ async function buildGraph(root, parsed) {
2362
2548
  for (const p of parsed) filesByPath.set(p.file.relPath, true);
2363
2549
  const nodes = [];
2364
2550
  const edges = [];
2551
+ const symbolsByFile = /* @__PURE__ */ new Map();
2552
+ const callsByFile = /* @__PURE__ */ new Map();
2365
2553
  for (const p of parsed) {
2366
2554
  const fileNode = toFileNode(p);
2367
2555
  nodes.push(fileNode);
2556
+ const fileSymNodes = [];
2368
2557
  for (const sym of p.symbols) {
2369
2558
  const symNode = toSymbolNode(p, sym);
2370
2559
  nodes.push(symNode);
2560
+ fileSymNodes.push(symNode);
2371
2561
  edges.push({ from: fileNode.id, to: symNode.id, kind: "defines" });
2372
2562
  }
2563
+ symbolsByFile.set(p.file.relPath, fileSymNodes);
2564
+ callsByFile.set(p.file.relPath, p.calls);
2373
2565
  const importEdges = /* @__PURE__ */ new Set();
2374
2566
  for (const spec of p.imports) {
2375
2567
  const target = resolveImport(p.file.relPath, spec, filesByPath);
@@ -2384,6 +2576,7 @@ async function buildGraph(root, parsed) {
2384
2576
  edges.push({ from: fileNode.id, to: fileId(testTargetPath), kind: "tests" });
2385
2577
  }
2386
2578
  }
2579
+ edges.push(...buildCallEdges(symbolsByFile, callsByFile));
2387
2580
  const symbolCount = nodes.filter((n) => n.kind === "symbol").length;
2388
2581
  const fileCount = nodes.length - symbolCount;
2389
2582
  return {
@@ -2407,9 +2600,52 @@ function buildSymbolIndex(graph) {
2407
2600
  }
2408
2601
  return out;
2409
2602
  }
2603
+ function tightestContainer(syms, line) {
2604
+ let best = null;
2605
+ for (const s of syms) {
2606
+ if (line < s.start_line || line > s.end_line) continue;
2607
+ if (!best || s.end_line - s.start_line < best.end_line - best.start_line) best = s;
2608
+ }
2609
+ return best;
2610
+ }
2611
+ function buildCallEdges(symbolsByFile, callsByFile) {
2612
+ const byName = /* @__PURE__ */ new Map();
2613
+ for (const syms of symbolsByFile.values()) {
2614
+ for (const s of syms) {
2615
+ const list = byName.get(s.name);
2616
+ if (list) list.push(s);
2617
+ else byName.set(s.name, [s]);
2618
+ }
2619
+ }
2620
+ const edges = [];
2621
+ const seen = /* @__PURE__ */ new Set();
2622
+ for (const [relPath, sites] of callsByFile) {
2623
+ const fileSyms = symbolsByFile.get(relPath) ?? [];
2624
+ for (const site of sites) {
2625
+ const caller = tightestContainer(fileSyms, site.line);
2626
+ if (!caller) continue;
2627
+ let callee = fileSyms.find((s) => s.name === site.callee);
2628
+ if (!callee) {
2629
+ const cands = byName.get(site.callee) ?? [];
2630
+ if (cands.length !== 1) continue;
2631
+ callee = cands[0];
2632
+ }
2633
+ if (!callee || callee.id === caller.id) continue;
2634
+ const key = `${caller.id}->${callee.id}`;
2635
+ if (seen.has(key)) continue;
2636
+ seen.add(key);
2637
+ edges.push({ from: caller.id, to: callee.id, kind: "calls" });
2638
+ }
2639
+ }
2640
+ return edges;
2641
+ }
2642
+
2643
+ // src/scanner/parse-cache.ts
2644
+ import { mkdir as mkdir5, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
2645
+ import { dirname as dirname6 } from "path";
2410
2646
 
2411
2647
  // src/scanner/parser.ts
2412
- import { readFile as readFile6 } from "fs/promises";
2648
+ import { readFile as readFile7 } from "fs/promises";
2413
2649
  import { createRequire } from "module";
2414
2650
  import { Language, Parser } from "web-tree-sitter";
2415
2651
 
@@ -2425,10 +2661,11 @@ function cleanImport(s) {
2425
2661
  async function runGenericParser(config, f, source) {
2426
2662
  let symbols = [];
2427
2663
  let imports = [];
2664
+ const calls = [];
2428
2665
  try {
2429
2666
  const { parser, language } = await createParser(config.grammar);
2430
2667
  const tree = parser.parse(source);
2431
- if (!tree) return { file: f, source, symbols, imports, calls: [] };
2668
+ if (!tree) return { file: f, source, symbols, imports, calls };
2432
2669
  const query = new Query(language, config.query);
2433
2670
  const matches = query.matches(tree.rootNode);
2434
2671
  for (const match of matches) {
@@ -2453,6 +2690,14 @@ async function runGenericParser(config, f, source) {
2453
2690
  });
2454
2691
  continue;
2455
2692
  }
2693
+ if (config.callCapture && config.callCalleeCapture) {
2694
+ const callNode = byName.get(config.callCapture);
2695
+ const calleeNode = byName.get(config.callCalleeCapture);
2696
+ if (callNode && calleeNode) {
2697
+ calls.push({ callee: calleeNode.text, line: callNode.startPosition.row + 1 });
2698
+ continue;
2699
+ }
2700
+ }
2456
2701
  if (config.importCapture) {
2457
2702
  const imp = byName.get(config.importCapture);
2458
2703
  if (imp) imports.push(cleanImport(imp.text));
@@ -2468,7 +2713,7 @@ async function runGenericParser(config, f, source) {
2468
2713
  imports = Array.from(new Set(imports)).filter((s) => s.length > 0);
2469
2714
  } catch {
2470
2715
  }
2471
- return { file: f, source, symbols, imports, calls: [] };
2716
+ return { file: f, source, symbols, imports, calls };
2472
2717
  }
2473
2718
 
2474
2719
  // src/scanner/parsers/c.ts
@@ -2479,6 +2724,7 @@ var QUERY = `
2479
2724
  (type_definition declarator: (type_identifier) @type.name) @type
2480
2725
  (preproc_include path: (string_literal) @import)
2481
2726
  (preproc_include path: (system_lib_string) @import)
2727
+ (call_expression function: (identifier) @call.name) @call
2482
2728
  `;
2483
2729
  async function parseC(f, source) {
2484
2730
  return runGenericParser(
@@ -2491,7 +2737,9 @@ async function parseC(f, source) {
2491
2737
  { declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
2492
2738
  { declCapture: "type", nameCapture: "type.name", kind: "type" }
2493
2739
  ],
2494
- importCapture: "import"
2740
+ importCapture: "import",
2741
+ callCapture: "call",
2742
+ callCalleeCapture: "call.name"
2495
2743
  },
2496
2744
  f,
2497
2745
  source
@@ -2508,6 +2756,9 @@ var QUERY2 = `
2508
2756
  (namespace_definition name: (namespace_identifier) @namespace.name) @namespace
2509
2757
  (preproc_include path: (string_literal) @import)
2510
2758
  (preproc_include path: (system_lib_string) @import)
2759
+ (call_expression function: (identifier) @call.name) @call
2760
+ (call_expression function: (field_expression field: (field_identifier) @call.name)) @call
2761
+ (call_expression function: (qualified_identifier name: (identifier) @call.name)) @call
2511
2762
  `;
2512
2763
  async function parseCpp(f, source) {
2513
2764
  return runGenericParser(
@@ -2522,7 +2773,9 @@ async function parseCpp(f, source) {
2522
2773
  { declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
2523
2774
  { declCapture: "namespace", nameCapture: "namespace.name", kind: "class" }
2524
2775
  ],
2525
- importCapture: "import"
2776
+ importCapture: "import",
2777
+ callCapture: "call",
2778
+ callCalleeCapture: "call.name"
2526
2779
  },
2527
2780
  f,
2528
2781
  source
@@ -2538,6 +2791,8 @@ var QUERY3 = `
2538
2791
  (method_declaration name: (identifier) @method.name) @method
2539
2792
  (namespace_declaration name: (_) @namespace.name) @namespace
2540
2793
  (using_directive (_) @import)
2794
+ (invocation_expression function: (identifier) @call.name) @call
2795
+ (invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call
2541
2796
  `;
2542
2797
  async function parseCSharp(f, source) {
2543
2798
  return runGenericParser(
@@ -2552,7 +2807,9 @@ async function parseCSharp(f, source) {
2552
2807
  { declCapture: "method", nameCapture: "method.name", kind: "method" },
2553
2808
  { declCapture: "namespace", nameCapture: "namespace.name", kind: "class" }
2554
2809
  ],
2555
- importCapture: "import"
2810
+ importCapture: "import",
2811
+ callCapture: "call",
2812
+ callCalleeCapture: "call.name"
2556
2813
  },
2557
2814
  f,
2558
2815
  source
@@ -2657,6 +2914,8 @@ var QUERY5 = `
2657
2914
  (method_declaration name: (field_identifier) @method.name) @method
2658
2915
  (type_spec name: (type_identifier) @type.name) @type
2659
2916
  (import_spec path: (interpreted_string_literal) @import)
2917
+ (call_expression function: (identifier) @call.name) @call
2918
+ (call_expression function: (selector_expression field: (field_identifier) @call.name)) @call
2660
2919
  `;
2661
2920
  async function parseGo(f, source) {
2662
2921
  return runGenericParser(
@@ -2668,7 +2927,9 @@ async function parseGo(f, source) {
2668
2927
  { declCapture: "method", nameCapture: "method.name", kind: "method" },
2669
2928
  { declCapture: "type", nameCapture: "type.name", kind: "type" }
2670
2929
  ],
2671
- importCapture: "import"
2930
+ importCapture: "import",
2931
+ callCapture: "call",
2932
+ callCalleeCapture: "call.name"
2672
2933
  },
2673
2934
  f,
2674
2935
  source
@@ -2733,6 +2994,7 @@ var QUERY6 = `
2733
2994
  (method_declaration name: (identifier) @method.name) @method
2734
2995
  (enum_declaration name: (identifier) @enum.name) @enum
2735
2996
  (import_declaration (scoped_identifier) @import)
2997
+ (method_invocation name: (identifier) @call.name) @call
2736
2998
  `;
2737
2999
  async function parseJava(f, source) {
2738
3000
  return runGenericParser(
@@ -2745,7 +3007,9 @@ async function parseJava(f, source) {
2745
3007
  { declCapture: "method", nameCapture: "method.name", kind: "method" },
2746
3008
  { declCapture: "enum", nameCapture: "enum.name", kind: "enum" }
2747
3009
  ],
2748
- importCapture: "import"
3010
+ importCapture: "import",
3011
+ callCapture: "call",
3012
+ callCalleeCapture: "call.name"
2749
3013
  },
2750
3014
  f,
2751
3015
  source
@@ -2758,6 +3022,7 @@ var QUERY7 = `
2758
3022
  (class_declaration (type_identifier) @class.name) @class
2759
3023
  (object_declaration (type_identifier) @object.name) @object
2760
3024
  (import_header (identifier) @import)
3025
+ (call_expression (simple_identifier) @call.name) @call
2761
3026
  `;
2762
3027
  async function parseKotlin(f, source) {
2763
3028
  return runGenericParser(
@@ -2769,7 +3034,9 @@ async function parseKotlin(f, source) {
2769
3034
  { declCapture: "class", nameCapture: "class.name", kind: "class" },
2770
3035
  { declCapture: "object", nameCapture: "object.name", kind: "class" }
2771
3036
  ],
2772
- importCapture: "import"
3037
+ importCapture: "import",
3038
+ callCapture: "call",
3039
+ callCalleeCapture: "call.name"
2773
3040
  },
2774
3041
  f,
2775
3042
  source
@@ -2783,6 +3050,9 @@ var QUERY8 = `
2783
3050
  (interface_declaration name: (name) @interface.name) @interface
2784
3051
  (trait_declaration name: (name) @trait.name) @trait
2785
3052
  (method_declaration name: (name) @method.name) @method
3053
+ (function_call_expression function: (name) @call.name) @call
3054
+ (member_call_expression name: (name) @call.name) @call
3055
+ (scoped_call_expression name: (name) @call.name) @call
2786
3056
  `;
2787
3057
  async function parsePhp(f, source) {
2788
3058
  return runGenericParser(
@@ -2795,7 +3065,9 @@ async function parsePhp(f, source) {
2795
3065
  { declCapture: "interface", nameCapture: "interface.name", kind: "interface" },
2796
3066
  { declCapture: "trait", nameCapture: "trait.name", kind: "class" },
2797
3067
  { declCapture: "method", nameCapture: "method.name", kind: "method" }
2798
- ]
3068
+ ],
3069
+ callCapture: "call",
3070
+ callCalleeCapture: "call.name"
2799
3071
  },
2800
3072
  f,
2801
3073
  source
@@ -2810,6 +3082,8 @@ var QUERY9 = `
2810
3082
  (import_statement name: (dotted_name) @import.module)
2811
3083
  (import_from_statement module_name: (dotted_name) @import.from)
2812
3084
  (import_from_statement module_name: (relative_import) @import.from)
3085
+ (call function: (identifier) @call.name) @call
3086
+ (call function: (attribute attribute: (identifier) @call.name)) @call
2813
3087
  `;
2814
3088
  function firstLine3(text, max = 200) {
2815
3089
  const line = text.split(/\r?\n/, 1)[0] ?? "";
@@ -2818,10 +3092,11 @@ function firstLine3(text, max = 200) {
2818
3092
  async function parsePython(f, source) {
2819
3093
  let symbols = [];
2820
3094
  let imports = [];
3095
+ const calls = [];
2821
3096
  try {
2822
3097
  const { parser, language } = await createParser("python");
2823
3098
  const tree = parser.parse(source);
2824
- if (!tree) return { file: f, source, symbols, imports, calls: [] };
3099
+ if (!tree) return { file: f, source, symbols, imports, calls };
2825
3100
  const query = new Query3(language, QUERY9);
2826
3101
  const matches = query.matches(tree.rootNode);
2827
3102
  for (const match of matches) {
@@ -2854,7 +3129,15 @@ async function parsePython(f, source) {
2854
3129
  continue;
2855
3130
  }
2856
3131
  const importNode = byName.get("import.module") ?? byName.get("import.from");
2857
- if (importNode) imports.push(importNode.text);
3132
+ if (importNode) {
3133
+ imports.push(importNode.text);
3134
+ continue;
3135
+ }
3136
+ const callName = byName.get("call.name");
3137
+ const callNode = byName.get("call");
3138
+ if (callName && callNode) {
3139
+ calls.push({ callee: callName.text, line: callNode.startPosition.row + 1 });
3140
+ }
2858
3141
  }
2859
3142
  const seen = /* @__PURE__ */ new Set();
2860
3143
  symbols = symbols.filter((s) => {
@@ -2866,7 +3149,7 @@ async function parsePython(f, source) {
2866
3149
  imports = Array.from(new Set(imports));
2867
3150
  } catch {
2868
3151
  }
2869
- return { file: f, source, symbols, imports, calls: [] };
3152
+ return { file: f, source, symbols, imports, calls };
2870
3153
  }
2871
3154
 
2872
3155
  // src/scanner/parsers/ruby.ts
@@ -2875,6 +3158,7 @@ var QUERY10 = `
2875
3158
  (singleton_method name: (identifier) @method.name) @method
2876
3159
  (class name: (constant) @class.name) @class
2877
3160
  (module name: (constant) @module.name) @module
3161
+ (call method: (identifier) @call.name) @call
2878
3162
  `;
2879
3163
  async function parseRuby(f, source) {
2880
3164
  return runGenericParser(
@@ -2886,7 +3170,9 @@ async function parseRuby(f, source) {
2886
3170
  { declCapture: "method", nameCapture: "method.name", kind: "method" },
2887
3171
  { declCapture: "class", nameCapture: "class.name", kind: "class" },
2888
3172
  { declCapture: "module", nameCapture: "module.name", kind: "class" }
2889
- ]
3173
+ ],
3174
+ callCapture: "call",
3175
+ callCalleeCapture: "call.name"
2890
3176
  },
2891
3177
  f,
2892
3178
  source
@@ -2900,6 +3186,9 @@ var QUERY11 = `
2900
3186
  (enum_item name: (type_identifier) @enum.name) @enum
2901
3187
  (trait_item name: (type_identifier) @trait.name) @trait
2902
3188
  (impl_item type: (type_identifier) @impl.name) @impl
3189
+ (call_expression function: (identifier) @call.name) @call
3190
+ (call_expression function: (scoped_identifier name: (identifier) @call.name)) @call
3191
+ (call_expression function: (field_expression field: (field_identifier) @call.name)) @call
2903
3192
  `;
2904
3193
  async function parseRust(f, source) {
2905
3194
  return runGenericParser(
@@ -2912,7 +3201,9 @@ async function parseRust(f, source) {
2912
3201
  { declCapture: "enum", nameCapture: "enum.name", kind: "enum" },
2913
3202
  { declCapture: "trait", nameCapture: "trait.name", kind: "interface" },
2914
3203
  { declCapture: "impl", nameCapture: "impl.name", kind: "class" }
2915
- ]
3204
+ ],
3205
+ callCapture: "call",
3206
+ callCalleeCapture: "call.name"
2916
3207
  },
2917
3208
  f,
2918
3209
  source
@@ -2931,6 +3222,8 @@ var TS_QUERY = `
2931
3222
  (lexical_declaration (variable_declarator name: (identifier) @const-fn.name value: [(arrow_function) (function_expression)])) @const-fn
2932
3223
  (assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
2933
3224
  (import_statement source: (string) @import)
3225
+ (call_expression function: (identifier) @call.name) @call
3226
+ (call_expression function: (member_expression property: (property_identifier) @call.name)) @call
2934
3227
  `;
2935
3228
  var JS_QUERY = `
2936
3229
  (function_declaration name: (identifier) @function.name) @function
@@ -2940,6 +3233,8 @@ var JS_QUERY = `
2940
3233
  (assignment_expression left: (member_expression property: (property_identifier) @member-fn.name) right: [(arrow_function) (function_expression)]) @member-fn
2941
3234
  (import_statement source: (string) @import)
2942
3235
  (call_expression function: (identifier) @_require_fn arguments: (arguments . (string) @require_source))
3236
+ (call_expression function: (identifier) @call.name) @call
3237
+ (call_expression function: (member_expression property: (property_identifier) @call.name)) @call
2943
3238
  `;
2944
3239
  function grammarFor(ext) {
2945
3240
  if (ext === ".tsx" || ext === ".jsx") return "tsx";
@@ -2968,10 +3263,11 @@ async function parseTypeScript(f, source) {
2968
3263
  const grammar = grammarFor(f.ext);
2969
3264
  let symbols = [];
2970
3265
  let imports = [];
3266
+ const calls = [];
2971
3267
  try {
2972
3268
  const { parser, language } = await createParser(grammar);
2973
3269
  const tree = parser.parse(source);
2974
- if (!tree) return { file: f, source, symbols, imports, calls: [] };
3270
+ if (!tree) return { file: f, source, symbols, imports, calls };
2975
3271
  const query = new Query4(language, queryFor(grammar));
2976
3272
  const matches = query.matches(tree.rootNode);
2977
3273
  for (const match of matches) {
@@ -2997,6 +3293,12 @@ async function parseTypeScript(f, source) {
2997
3293
  const requireSource = byName.get("require_source");
2998
3294
  if (requireFn && requireSource && requireFn.text === "require") {
2999
3295
  imports.push(unquote(requireSource.text));
3296
+ continue;
3297
+ }
3298
+ const callName = byName.get("call.name");
3299
+ const callNode = byName.get("call");
3300
+ if (callName && callNode) {
3301
+ calls.push({ callee: callName.text, line: callNode.startPosition.row + 1 });
3000
3302
  }
3001
3303
  }
3002
3304
  const seen = /* @__PURE__ */ new Set();
@@ -3009,7 +3311,7 @@ async function parseTypeScript(f, source) {
3009
3311
  imports = Array.from(new Set(imports));
3010
3312
  } catch {
3011
3313
  }
3012
- return { file: f, source, symbols, imports, calls: [] };
3314
+ return { file: f, source, symbols, imports, calls };
3013
3315
  }
3014
3316
 
3015
3317
  // src/scanner/parsers/svelte.ts
@@ -3043,6 +3345,7 @@ async function parseSvelte(f, source) {
3043
3345
  });
3044
3346
  }
3045
3347
  for (const imp of parsed.imports) out.imports.push(imp);
3348
+ for (const call of parsed.calls) out.calls.push({ ...call, line: call.line + offset });
3046
3349
  }
3047
3350
  out.symbols.push({
3048
3351
  name: f.relPath.split("/").pop()?.replace(/\.svelte$/i, "") ?? f.relPath,
@@ -3086,6 +3389,7 @@ async function parseVue(f, source) {
3086
3389
  });
3087
3390
  }
3088
3391
  for (const imp of parsed.imports) out.imports.push(imp);
3392
+ for (const call of parsed.calls) out.calls.push({ ...call, line: call.line + offset });
3089
3393
  }
3090
3394
  out.symbols.push({
3091
3395
  name: f.relPath.split("/").pop()?.replace(/\.vue$/i, "") ?? f.relPath,
@@ -3142,13 +3446,7 @@ async function createParser(name) {
3142
3446
  function emptyParsed(file, source) {
3143
3447
  return { file, source, symbols: [], imports: [], calls: [] };
3144
3448
  }
3145
- async function parseFile(f) {
3146
- let source;
3147
- try {
3148
- source = await readFile6(f.absPath, "utf8");
3149
- } catch {
3150
- return emptyParsed(f, "");
3151
- }
3449
+ async function parseSource(f, source) {
3152
3450
  switch (f.ext) {
3153
3451
  case ".ts":
3154
3452
  case ".tsx":
@@ -3201,8 +3499,72 @@ async function parseFile(f) {
3201
3499
  }
3202
3500
  }
3203
3501
 
3502
+ // src/scanner/parse-cache.ts
3503
+ var PARSE_CACHE_VERSION = 2;
3504
+ function emptyParseCache() {
3505
+ return { schema_version: PARSE_CACHE_VERSION, files: {} };
3506
+ }
3507
+ async function readParseCache(path) {
3508
+ try {
3509
+ const raw = await readFile8(path, "utf8");
3510
+ const parsed = JSON.parse(raw);
3511
+ if (parsed.schema_version !== PARSE_CACHE_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
3512
+ return emptyParseCache();
3513
+ }
3514
+ return { schema_version: PARSE_CACHE_VERSION, files: parsed.files };
3515
+ } catch {
3516
+ return emptyParseCache();
3517
+ }
3518
+ }
3519
+ async function writeParseCache(path, cache) {
3520
+ try {
3521
+ await mkdir5(dirname6(path), { recursive: true });
3522
+ await writeFile4(path, `${JSON.stringify(cache)}
3523
+ `, "utf8");
3524
+ } catch {
3525
+ }
3526
+ }
3527
+ async function incrementalParse(parsable, prev, opts = {}) {
3528
+ const cache = emptyParseCache();
3529
+ const parsed = [];
3530
+ let reused = 0;
3531
+ let reparsed = 0;
3532
+ let parseErrors = 0;
3533
+ for (const f of parsable) {
3534
+ let source;
3535
+ try {
3536
+ source = await readFile8(f.absPath, "utf8");
3537
+ } catch {
3538
+ continue;
3539
+ }
3540
+ const hash = fileHash(source);
3541
+ const cached = opts.full ? void 0 : prev.files[f.relPath];
3542
+ if (cached && cached.hash === hash) {
3543
+ parsed.push({
3544
+ file: f,
3545
+ source,
3546
+ symbols: cached.symbols,
3547
+ imports: cached.imports,
3548
+ calls: cached.calls
3549
+ });
3550
+ cache.files[f.relPath] = cached;
3551
+ reused += 1;
3552
+ continue;
3553
+ }
3554
+ try {
3555
+ const p = await parseSource(f, source);
3556
+ parsed.push(p);
3557
+ cache.files[f.relPath] = { hash, symbols: p.symbols, imports: p.imports, calls: p.calls };
3558
+ reparsed += 1;
3559
+ } catch {
3560
+ parseErrors += 1;
3561
+ }
3562
+ }
3563
+ return { parsed, cache, reused, reparsed, parseErrors };
3564
+ }
3565
+
3204
3566
  // src/scanner/walker.ts
3205
- import { readFile as readFile7, readdir, stat } from "fs/promises";
3567
+ import { readFile as readFile9, readdir, stat } from "fs/promises";
3206
3568
  import { extname, join as join7, relative as relative2, sep as sep2 } from "path";
3207
3569
  import ignore2 from "ignore";
3208
3570
  var DEFAULT_IGNORE = [
@@ -3283,7 +3645,7 @@ var BINARY_EXTS = /* @__PURE__ */ new Set([
3283
3645
  ]);
3284
3646
  async function readIgnoreFile2(path) {
3285
3647
  try {
3286
- const text = await readFile7(path, "utf8");
3648
+ const text = await readFile9(path, "utf8");
3287
3649
  return text.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
3288
3650
  } catch {
3289
3651
  return [];
@@ -3338,15 +3700,15 @@ async function* walk(root, options = {}) {
3338
3700
  }
3339
3701
 
3340
3702
  // src/graph/store.ts
3341
- import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
3342
- import { dirname as dirname5 } from "path";
3703
+ import { mkdir as mkdir6, readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
3704
+ import { dirname as dirname7 } from "path";
3343
3705
  async function writeJson(path, data, pretty) {
3344
- await mkdir4(dirname5(path), { recursive: true });
3706
+ await mkdir6(dirname7(path), { recursive: true });
3345
3707
  const text = pretty ? JSON.stringify(data, null, 2) : JSON.stringify(data);
3346
- await writeFile3(path, text + "\n", "utf8");
3708
+ await writeFile5(path, text + "\n", "utf8");
3347
3709
  }
3348
3710
  async function readJson(path) {
3349
- const text = await readFile8(path, "utf8");
3711
+ const text = await readFile10(path, "utf8");
3350
3712
  return JSON.parse(text);
3351
3713
  }
3352
3714
  async function writeGraph(path, graph) {
@@ -3364,12 +3726,12 @@ async function readSymbolIndex(path) {
3364
3726
  }
3365
3727
 
3366
3728
  // src/cli/bootstrap.ts
3367
- import { mkdir as mkdir5, readFile as readFile10, stat as stat2, writeFile as writeFile5 } from "fs/promises";
3729
+ import { mkdir as mkdir7, readFile as readFile12, stat as stat2, writeFile as writeFile7 } from "fs/promises";
3368
3730
  import { basename as basename4 } from "path";
3369
3731
 
3370
3732
  // src/hooks/claude-md.ts
3371
- import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
3372
- import { basename as basename3, dirname as dirname6 } from "path";
3733
+ import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
3734
+ import { basename as basename3, dirname as dirname8 } from "path";
3373
3735
  var POLICY_VERSION = 6;
3374
3736
  var POLICY_BEGIN = `<!-- synthra-policy v${POLICY_VERSION} BEGIN -->`;
3375
3737
  var POLICY_END = `<!-- synthra-policy v${POLICY_VERSION} END -->`;
@@ -3531,14 +3893,14 @@ function onboardingSkeleton(projectName) {
3531
3893
  async function patchClaudeMd(path, projectName) {
3532
3894
  let existing;
3533
3895
  try {
3534
- existing = await readFile9(path, "utf8");
3896
+ existing = await readFile11(path, "utf8");
3535
3897
  } catch {
3536
3898
  existing = null;
3537
3899
  }
3538
3900
  const block = policyBlock();
3539
3901
  if (existing === null) {
3540
- const name = projectName || basename3(dirname6(path)) || "this project";
3541
- await writeFile4(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
3902
+ const name = projectName || basename3(dirname8(path)) || "this project";
3903
+ await writeFile6(path, onboardingSkeleton(name) + "\n" + block + "\n", "utf8");
3542
3904
  return { created: true, updated: false, skipped: false };
3543
3905
  }
3544
3906
  const stripped = existing.replace(ANY_BLOCK_RE, "");
@@ -3547,7 +3909,7 @@ async function patchClaudeMd(path, projectName) {
3547
3909
  if (hadBlock && desired === existing) {
3548
3910
  return { created: false, updated: false, skipped: true };
3549
3911
  }
3550
- await writeFile4(path, desired, "utf8");
3912
+ await writeFile6(path, desired, "utf8");
3551
3913
  return { created: false, updated: true, skipped: false };
3552
3914
  }
3553
3915
 
@@ -3572,13 +3934,13 @@ async function exists(path) {
3572
3934
  }
3573
3935
  async function ensureDir(path) {
3574
3936
  const had = await exists(path);
3575
- await mkdir5(path, { recursive: true });
3937
+ await mkdir7(path, { recursive: true });
3576
3938
  return !had;
3577
3939
  }
3578
3940
  async function patchGitignore(path) {
3579
3941
  let existing = "";
3580
3942
  try {
3581
- existing = await readFile10(path, "utf8");
3943
+ existing = await readFile12(path, "utf8");
3582
3944
  } catch {
3583
3945
  }
3584
3946
  const trimmed = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
@@ -3587,7 +3949,7 @@ async function patchGitignore(path) {
3587
3949
  const block = missing.map((m) => `# ${m.comment}
3588
3950
  ${m.entry}`).join("\n") + "\n";
3589
3951
  const appendix = (existing.length === 0 || existing.endsWith("\n") ? "" : "\n") + (existing.length ? "\n" : "") + block;
3590
- await writeFile5(path, existing + appendix, "utf8");
3952
+ await writeFile7(path, existing + appendix, "utf8");
3591
3953
  return true;
3592
3954
  }
3593
3955
  async function bootstrap(paths) {
@@ -3661,25 +4023,22 @@ async function scanProject(projectRootRaw, opts = {}) {
3661
4023
  for await (const file of walk(projectRoot)) walked.push(file);
3662
4024
  if (verbose) log.info(` walked ${walked.length} files`);
3663
4025
  const parsable = walked.filter((f) => PARSABLE_EXTS.has(f.ext));
3664
- const parsed = [];
3665
- let parseErrors = 0;
3666
- for (const file of parsable) {
3667
- try {
3668
- parsed.push(await parseFile(file));
3669
- } catch (err2) {
3670
- parseErrors += 1;
3671
- if (verbose) log.debug(` parse failed: ${file.relPath} \u2014 ${err2.message}`);
3672
- }
3673
- }
4026
+ const prevCache = await readParseCache(paths.parseCache);
4027
+ const { parsed, cache, reused, reparsed, parseErrors } = await incrementalParse(
4028
+ parsable,
4029
+ prevCache,
4030
+ { full: opts.full }
4031
+ );
3674
4032
  if (verbose) {
3675
4033
  log.info(
3676
- ` parsed ${parsed.length} files (${walked.length - parsable.length} skipped` + (parseErrors ? `, ${parseErrors} errored` : "") + ")"
4034
+ ` parsed ${parsed.length} files (${reused} reused \xB7 ${reparsed} reparsed` + (parseErrors ? `, ${parseErrors} errored` : "") + `; ${walked.length - parsable.length} non-code skipped)`
3677
4035
  );
3678
4036
  }
3679
4037
  const graph = await buildGraph(projectRoot, parsed);
3680
4038
  const symbolIndex = buildSymbolIndex(graph);
3681
4039
  await writeGraph(paths.infoGraph, graph);
3682
4040
  await writeSymbolIndex(paths.symbolIndex, symbolIndex);
4041
+ await writeParseCache(paths.parseCache, cache);
3683
4042
  if (verbose) {
3684
4043
  log.info(
3685
4044
  ` wrote ${paths.infoGraph} \u2014 ${graph.symbol_count} symbols, ${graph.edge_count} edges`
@@ -3696,126 +4055,8 @@ async function scanProject(projectRootRaw, opts = {}) {
3696
4055
  durationMs
3697
4056
  };
3698
4057
  }
3699
- async function scanCommand(rawPath) {
3700
- return scanProject(rawPath);
3701
- }
3702
-
3703
- // src/learn/store.ts
3704
- import { appendFile as appendFile2, mkdir as mkdir6, readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
3705
- import { dirname as dirname7 } from "path";
3706
-
3707
- // src/learn/usage.ts
3708
- var LEARN_SCHEMA_VERSION = 1;
3709
- var DAY_MS = 24 * 60 * 60 * 1e3;
3710
- function halfLifeMs() {
3711
- const env = Number(process.env.SYN_LEARN_HALFLIFE_DAYS);
3712
- const days = Number.isFinite(env) && env > 0 ? env : 7;
3713
- return days * DAY_MS;
3714
- }
3715
- function weightFor(source) {
3716
- switch (source) {
3717
- case "register_edit":
3718
- return 2;
3719
- case "read":
3720
- return 1;
3721
- default:
3722
- return 0;
3723
- }
3724
- }
3725
- function emptyStore() {
3726
- return {
3727
- schema_version: LEARN_SCHEMA_VERSION,
3728
- asOf: (/* @__PURE__ */ new Date(0)).toISOString(),
3729
- files: {}
3730
- };
3731
- }
3732
- function decayFactor(fromTs, toMs, hl) {
3733
- const fromMs = Date.parse(fromTs);
3734
- if (!Number.isFinite(fromMs)) return 1;
3735
- const dt = toMs - fromMs;
3736
- if (dt <= 0) return 1;
3737
- return Math.exp(-(Math.LN2 / hl) * dt);
3738
- }
3739
- function foldEvent(store, ev) {
3740
- const w = weightFor(ev.source);
3741
- if (w <= 0 || !ev.path) return store;
3742
- const tMs = Date.parse(ev.ts);
3743
- if (!Number.isFinite(tMs)) return store;
3744
- const hl = halfLifeMs();
3745
- const prev = store.files[ev.path];
3746
- if (prev) {
3747
- const decayed = prev.decayed * decayFactor(prev.lastTs, tMs, hl) + w;
3748
- store.files[ev.path] = { count: prev.count + 1, decayed, lastTs: ev.ts };
3749
- } else {
3750
- store.files[ev.path] = { count: 1, decayed: w, lastTs: ev.ts };
3751
- }
3752
- return store;
3753
- }
3754
- function effectiveScores(store, nowMs) {
3755
- const hl = halfLifeMs();
3756
- const out = /* @__PURE__ */ new Map();
3757
- for (const [path, stat6] of Object.entries(store.files)) {
3758
- const eff = stat6.decayed * decayFactor(stat6.lastTs, nowMs, hl);
3759
- if (eff > 0.01) out.set(path, eff);
3760
- }
3761
- return out;
3762
- }
3763
- function recomputeFromLog(events) {
3764
- const store = emptyStore();
3765
- for (const ev of events) foldEvent(store, ev);
3766
- return store;
3767
- }
3768
-
3769
- // src/learn/store.ts
3770
- async function readLearnStore(path) {
3771
- try {
3772
- const raw = await readFile11(path, "utf8");
3773
- const parsed = JSON.parse(raw);
3774
- if (parsed.schema_version !== LEARN_SCHEMA_VERSION || typeof parsed.files !== "object" || parsed.files === null) {
3775
- return emptyStore();
3776
- }
3777
- return {
3778
- schema_version: LEARN_SCHEMA_VERSION,
3779
- asOf: typeof parsed.asOf === "string" ? parsed.asOf : emptyStore().asOf,
3780
- files: parsed.files
3781
- };
3782
- } catch {
3783
- return emptyStore();
3784
- }
3785
- }
3786
- async function writeLearnStore(path, store) {
3787
- try {
3788
- await mkdir6(dirname7(path), { recursive: true });
3789
- await writeFile6(path, JSON.stringify(store, null, 2) + "\n", "utf8");
3790
- } catch {
3791
- }
3792
- }
3793
- async function readAccessLog(path) {
3794
- try {
3795
- const raw = await readFile11(path, "utf8");
3796
- const out = [];
3797
- for (const line of raw.split("\n")) {
3798
- const t = line.trim();
3799
- if (!t) continue;
3800
- try {
3801
- const ev = JSON.parse(t);
3802
- if (ev && typeof ev.ts === "string" && typeof ev.path === "string" && typeof ev.source === "string") {
3803
- out.push(ev);
3804
- }
3805
- } catch {
3806
- }
3807
- }
3808
- return out;
3809
- } catch {
3810
- return [];
3811
- }
3812
- }
3813
- async function appendAccess(path, ev) {
3814
- try {
3815
- await mkdir6(dirname7(path), { recursive: true });
3816
- await appendFile2(path, JSON.stringify(ev) + "\n", "utf8");
3817
- } catch {
3818
- }
4058
+ async function scanCommand(rawPath, opts = {}) {
4059
+ return scanProject(rawPath, opts);
3819
4060
  }
3820
4061
 
3821
4062
  // src/learn/runtime.ts
@@ -3876,8 +4117,8 @@ var LearnRuntime = class _LearnRuntime {
3876
4117
  };
3877
4118
 
3878
4119
  // src/server/mcp.ts
3879
- import { appendFile as appendFile3, mkdir as mkdir9 } from "fs/promises";
3880
- import { dirname as dirname10 } from "path";
4120
+ import { appendFile as appendFile3, mkdir as mkdir10 } from "fs/promises";
4121
+ import { dirname as dirname11 } from "path";
3881
4122
 
3882
4123
  // src/graph/rank.ts
3883
4124
  var KW_BASE_WEIGHT = 2;
@@ -4130,14 +4371,14 @@ async function retrieve(graph, query, options = {}) {
4130
4371
 
4131
4372
  // src/memory/branches.ts
4132
4373
  import { execFile as execFile2 } from "child_process";
4133
- import { readFile as readFile12 } from "fs/promises";
4374
+ import { readFile as readFile13 } from "fs/promises";
4134
4375
  import { join as join8 } from "path";
4135
4376
  import { promisify as promisify2 } from "util";
4136
4377
  var execFileAsync2 = promisify2(execFile2);
4137
4378
  async function currentBranch(projectRoot) {
4138
4379
  try {
4139
4380
  const headPath = join8(projectRoot, ".git", "HEAD");
4140
- const head = await readFile12(headPath, "utf8");
4381
+ const head = await readFile13(headPath, "utf8");
4141
4382
  const trimmed = head.trim();
4142
4383
  const match = trimmed.match(/^ref:\s+refs\/heads\/(.+)$/);
4143
4384
  if (match?.[1]) return match[1];
@@ -4187,8 +4428,8 @@ function resolveBranchPaths(contextDir, branch, isDefault) {
4187
4428
  }
4188
4429
 
4189
4430
  // src/memory/context-md.ts
4190
- import { mkdir as mkdir7, readFile as readFile13, writeFile as writeFile7 } from "fs/promises";
4191
- import { dirname as dirname8 } from "path";
4431
+ import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
4432
+ import { dirname as dirname9 } from "path";
4192
4433
  var MAX_BULLETS = 3;
4193
4434
  function deriveContextMd(entries, branch) {
4194
4435
  const tasks = entries.filter((e) => e.type === "task").reverse();
@@ -4231,17 +4472,17 @@ function formatContextMd(ctx) {
4231
4472
  return lines.join("\n");
4232
4473
  }
4233
4474
  async function writeContextMd(path, ctx) {
4234
- await mkdir7(dirname8(path), { recursive: true });
4235
- await writeFile7(path, formatContextMd(ctx), "utf8");
4475
+ await mkdir8(dirname9(path), { recursive: true });
4476
+ await writeFile8(path, formatContextMd(ctx), "utf8");
4236
4477
  }
4237
4478
 
4238
4479
  // src/memory/context-store.ts
4239
- import { mkdir as mkdir8, readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
4240
- import { dirname as dirname9 } from "path";
4480
+ import { mkdir as mkdir9, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
4481
+ import { dirname as dirname10 } from "path";
4241
4482
  var SCHEMA_VERSION3 = 1;
4242
4483
  async function readEntries(path) {
4243
4484
  try {
4244
- const raw = await readFile14(path, "utf8");
4485
+ const raw = await readFile15(path, "utf8");
4245
4486
  const parsed = JSON.parse(raw);
4246
4487
  return Array.isArray(parsed.entries) ? parsed.entries : [];
4247
4488
  } catch {
@@ -4249,9 +4490,9 @@ async function readEntries(path) {
4249
4490
  }
4250
4491
  }
4251
4492
  async function writeEntries(path, entries) {
4252
- await mkdir8(dirname9(path), { recursive: true });
4493
+ await mkdir9(dirname10(path), { recursive: true });
4253
4494
  const store = { schema_version: SCHEMA_VERSION3, entries };
4254
- await writeFile8(path, JSON.stringify(store, null, 2) + "\n", "utf8");
4495
+ await writeFile9(path, JSON.stringify(store, null, 2) + "\n", "utf8");
4255
4496
  }
4256
4497
  async function appendEntry(path, entry) {
4257
4498
  const entries = await readEntries(path);
@@ -4628,7 +4869,7 @@ var TOOLS = [
4628
4869
  },
4629
4870
  {
4630
4871
  name: "blast_radius",
4631
- description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports + tests edges. Use BEFORE editing a widely-used file to see what could break. Symbol-level granularity is approximated at the file level (we don't track call edges in v0.1).",
4872
+ description: "Given a file (or 'file::symbol' target), return all files that depend on it transitively via imports, tests, and call edges (callers). Use BEFORE editing a widely-used file to see what could break. Call edges are name-resolved (precise within a file, unique-name across files) and projected to file granularity.",
4632
4873
  inputSchema: {
4633
4874
  type: "object",
4634
4875
  properties: {
@@ -4640,7 +4881,7 @@ var TOOLS = [
4640
4881
  },
4641
4882
  {
4642
4883
  name: "dead_code",
4643
- description: "Return files in the project that no other file imports and no test file references \u2014 strong candidates for unused/orphaned code. File-level granularity (v0.1 limitation \u2014 symbol-level needs call-graph edges). Common entry-point patterns (main, index, app, CLI, bin/) are excluded heuristically.",
4884
+ description: "Return files in the project that no other file imports and no test file references \u2014 strong candidates for unused/orphaned code. File-level granularity; symbol-level dead code (unused exports, on top of the call graph) is a planned follow-up. Common entry-point patterns (main, index, app, CLI, bin/) are excluded heuristically.",
4644
4885
  inputSchema: {
4645
4886
  type: "object",
4646
4887
  properties: {
@@ -4686,12 +4927,24 @@ function blastRadius(args, ctx) {
4686
4927
  const filePath = targetRaw.split("::", 1)[0]?.trim() ?? targetRaw;
4687
4928
  const root = ctx.graph.nodes.find((n) => n.kind === "file" && n.path === filePath);
4688
4929
  if (!root) return errorContent(`blast_radius: file not in graph: ${filePath}`);
4930
+ const fileIdBySymbol = /* @__PURE__ */ new Map();
4931
+ for (const n of ctx.graph.nodes) {
4932
+ if (n.kind === "symbol") fileIdBySymbol.set(n.id, `file:${n.file}`);
4933
+ }
4689
4934
  const incoming = /* @__PURE__ */ new Map();
4935
+ const addIncoming = (to, from, kind) => {
4936
+ const list = incoming.get(to) ?? [];
4937
+ list.push({ from, kind });
4938
+ incoming.set(to, list);
4939
+ };
4690
4940
  for (const e of ctx.graph.edges) {
4691
- if (e.kind !== "imports" && e.kind !== "tests") continue;
4692
- const list = incoming.get(e.to) ?? [];
4693
- list.push({ from: e.from, kind: e.kind });
4694
- incoming.set(e.to, list);
4941
+ if (e.kind === "imports" || e.kind === "tests") {
4942
+ addIncoming(e.to, e.from, e.kind);
4943
+ } else if (e.kind === "calls") {
4944
+ const fromFile = fileIdBySymbol.get(e.from);
4945
+ const toFile = fileIdBySymbol.get(e.to);
4946
+ if (fromFile && toFile && fromFile !== toFile) addIncoming(toFile, fromFile, "calls");
4947
+ }
4695
4948
  }
4696
4949
  const visited = /* @__PURE__ */ new Set([root.id]);
4697
4950
  const hits = [];
@@ -4769,7 +5022,7 @@ _(no file is unreferenced \u2014 every file is either imported by another, has a
4769
5022
  }
4770
5023
  lines.push("");
4771
5024
  lines.push(
4772
- `_v0.1 caveat:_ this is file-level only. Symbol-level dead code (unused exports) needs call-graph edges, which land in v0.2.`
5025
+ `_caveat:_ this is file-level only. Symbol-level dead code (unused exports), built on the now-populated call graph, is a planned follow-up.`
4773
5026
  );
4774
5027
  return textContent(lines.join("\n"));
4775
5028
  }
@@ -4921,7 +5174,7 @@ async function contextRecall(args, ctx) {
4921
5174
  }
4922
5175
  async function logToolCall(ctx, tool) {
4923
5176
  try {
4924
- await mkdir9(dirname10(ctx.paths.toolLog), { recursive: true });
5177
+ await mkdir10(dirname11(ctx.paths.toolLog), { recursive: true });
4925
5178
  await appendFile3(
4926
5179
  ctx.paths.toolLog,
4927
5180
  JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), tool }) + "\n",
@@ -5020,12 +5273,12 @@ async function getCommitsSince(projectRoot, sinceIso) {
5020
5273
  }
5021
5274
 
5022
5275
  // src/memory/session.ts
5023
- import { mkdir as mkdir10, readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
5024
- import { dirname as dirname11 } from "path";
5276
+ import { mkdir as mkdir11, readFile as readFile16, writeFile as writeFile10 } from "fs/promises";
5277
+ import { dirname as dirname12 } from "path";
5025
5278
  var SESSION_SCHEMA_VERSION = 1;
5026
5279
  async function readSession(path) {
5027
5280
  try {
5028
- const raw = await readFile15(path, "utf8");
5281
+ const raw = await readFile16(path, "utf8");
5029
5282
  const parsed = JSON.parse(raw);
5030
5283
  if (parsed.schema_version !== SESSION_SCHEMA_VERSION) return null;
5031
5284
  return parsed;
@@ -5034,8 +5287,8 @@ async function readSession(path) {
5034
5287
  }
5035
5288
  }
5036
5289
  async function writeSession(path, state) {
5037
- await mkdir10(dirname11(path), { recursive: true });
5038
- await writeFile9(path, JSON.stringify(state, null, 2) + "\n", "utf8");
5290
+ await mkdir11(dirname12(path), { recursive: true });
5291
+ await writeFile10(path, JSON.stringify(state, null, 2) + "\n", "utf8");
5039
5292
  }
5040
5293
 
5041
5294
  // src/server/routes/context-update.ts
@@ -5080,8 +5333,8 @@ async function handleContextUpdate(req, ctx) {
5080
5333
  }
5081
5334
 
5082
5335
  // src/server/routes/gate.ts
5083
- import { appendFile as appendFile4, mkdir as mkdir11 } from "fs/promises";
5084
- import { dirname as dirname12 } from "path";
5336
+ import { appendFile as appendFile4, mkdir as mkdir12 } from "fs/promises";
5337
+ import { dirname as dirname13 } from "path";
5085
5338
  var BLOCKABLE_TOOLS = /* @__PURE__ */ new Set(["Grep", "Glob"]);
5086
5339
  var RECENT_ACTIVITY_WINDOW_MS = 5 * 60 * 1e3;
5087
5340
  function extractQuery(toolName, input) {
@@ -5137,7 +5390,7 @@ function recentlyTouchedMatchesQuery(recentPaths, queryTokens, graph) {
5137
5390
  }
5138
5391
  async function logDecision(ctx, toolName, query, decision, reason) {
5139
5392
  try {
5140
- await mkdir11(dirname12(ctx.paths.gateLog), { recursive: true });
5393
+ await mkdir12(dirname13(ctx.paths.gateLog), { recursive: true });
5141
5394
  const entry = {
5142
5395
  ts: (/* @__PURE__ */ new Date()).toISOString(),
5143
5396
  tool: toolName,
@@ -5209,15 +5462,15 @@ async function handleGate(req, ctx) {
5209
5462
  }
5210
5463
 
5211
5464
  // src/server/routes/log.ts
5212
- import { appendFile as appendFile5, mkdir as mkdir12 } from "fs/promises";
5213
- import { dirname as dirname13 } from "path";
5465
+ import { appendFile as appendFile5, mkdir as mkdir13 } from "fs/promises";
5466
+ import { dirname as dirname14 } from "path";
5214
5467
  async function handleLog(entry, ctx) {
5215
5468
  if (!entry || typeof entry.input_tokens !== "number" || typeof entry.output_tokens !== "number") {
5216
5469
  throw new Error("log: input_tokens and output_tokens (number) are required");
5217
5470
  }
5218
5471
  const written_at = (/* @__PURE__ */ new Date()).toISOString();
5219
5472
  const record = { ...entry, written_at };
5220
- await mkdir12(dirname13(ctx.paths.tokenLog), { recursive: true });
5473
+ await mkdir13(dirname14(ctx.paths.tokenLog), { recursive: true });
5221
5474
  await appendFile5(ctx.paths.tokenLog, JSON.stringify(record) + "\n", "utf8");
5222
5475
  return { ok: true, written_at };
5223
5476
  }
@@ -5403,7 +5656,7 @@ async function startServer(paths, options = {}) {
5403
5656
  const port = options.port ?? await findFreePort();
5404
5657
  const app = buildApp(ctx, port);
5405
5658
  const nodeServer = serve2({ fetch: app.fetch, port, hostname: "127.0.0.1" });
5406
- await writeFile10(paths.mcpPort, String(port), "utf8");
5659
+ await writeFile11(paths.mcpPort, String(port), "utf8");
5407
5660
  const fileWatcher = createFileWatcher(paths.projectRoot, (e) => ctx.activity.add(e));
5408
5661
  const gitWatcher = createGitWatcher(paths.projectRoot, async (e) => {
5409
5662
  await ctx.activity.add(e);
@@ -5547,7 +5800,7 @@ async function dashboardCommand(rawPath) {
5547
5800
  }
5548
5801
 
5549
5802
  // src/cli/doctor-command.ts
5550
- import { readFile as readFile16, stat as stat4 } from "fs/promises";
5803
+ import { readFile as readFile17, stat as stat4 } from "fs/promises";
5551
5804
  import { join as join10, resolve as resolve3 } from "path";
5552
5805
  import spawn from "cross-spawn";
5553
5806
  var ICON = { ok: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
@@ -5616,7 +5869,7 @@ async function runDoctorChecks(projectRoot) {
5616
5869
  });
5617
5870
  } else {
5618
5871
  try {
5619
- const graph = JSON.parse(await readFile16(paths.infoGraph, "utf8"));
5872
+ const graph = JSON.parse(await readFile17(paths.infoGraph, "utf8"));
5620
5873
  const parts = [`${graph.symbol_count} symbols`, `${graph.file_count} files`];
5621
5874
  let status = "ok";
5622
5875
  const ageMs = Date.now() - Date.parse(graph.generated_at);
@@ -5657,7 +5910,7 @@ async function runDoctorChecks(projectRoot) {
5657
5910
  detail: "no CLAUDE.md \u2014 run `syn .` to scaffold + inject the policy block."
5658
5911
  });
5659
5912
  } else {
5660
- const md = await readFile16(paths.claudeMd, "utf8");
5913
+ const md = await readFile17(paths.claudeMd, "utf8");
5661
5914
  if (md.includes(`synthra-policy v${POLICY_VERSION} BEGIN`)) {
5662
5915
  checks.push({
5663
5916
  status: "ok",
@@ -5680,7 +5933,7 @@ async function runDoctorChecks(projectRoot) {
5680
5933
  detail: "no .claude/settings.local.json \u2014 run `syn .` to install hooks."
5681
5934
  });
5682
5935
  } else {
5683
- const s = await readFile16(paths.claudeSettings, "utf8");
5936
+ const s = await readFile17(paths.claudeSettings, "utf8");
5684
5937
  checks.push(
5685
5938
  s.includes("synthra-hook=true") ? { status: "ok", label: "Hooks", detail: "registered in .claude/settings.local.json" } : {
5686
5939
  status: "warn",
@@ -5710,7 +5963,7 @@ async function doctorCommand(rawPath) {
5710
5963
  }
5711
5964
 
5712
5965
  // src/cli/self-update.ts
5713
- import { mkdir as mkdir13, readFile as readFile17, writeFile as writeFile11 } from "fs/promises";
5966
+ import { mkdir as mkdir14, readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
5714
5967
  import { homedir as homedir3 } from "os";
5715
5968
  import { join as join11 } from "path";
5716
5969
  import { createInterface } from "readline/promises";
@@ -5768,7 +6021,7 @@ async function checkForUpdate() {
5768
6021
  }
5769
6022
  async function readLastSeen() {
5770
6023
  try {
5771
- const raw = await readFile17(LAST_SEEN_PATH, "utf8");
6024
+ const raw = await readFile18(LAST_SEEN_PATH, "utf8");
5772
6025
  const parsed = JSON.parse(raw);
5773
6026
  return parsed.version ?? null;
5774
6027
  } catch {
@@ -5777,9 +6030,9 @@ async function readLastSeen() {
5777
6030
  }
5778
6031
  async function writeLastSeen(version) {
5779
6032
  try {
5780
- await mkdir13(SYNTHRA_DIR, { recursive: true });
6033
+ await mkdir14(SYNTHRA_DIR, { recursive: true });
5781
6034
  const data = { version, updated_at: (/* @__PURE__ */ new Date()).toISOString() };
5782
- await writeFile11(LAST_SEEN_PATH, JSON.stringify(data, null, 2), "utf8");
6035
+ await writeFile12(LAST_SEEN_PATH, JSON.stringify(data, null, 2), "utf8");
5783
6036
  } catch {
5784
6037
  }
5785
6038
  }
@@ -5811,7 +6064,7 @@ async function readInstalledChangelog() {
5811
6064
  const root = await npmGlobalRoot();
5812
6065
  if (!root) return null;
5813
6066
  try {
5814
- return await readFile17(join11(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
6067
+ return await readFile18(join11(root, "@jefuriiij", "synthra", "CHANGELOG.md"), "utf8");
5815
6068
  } catch {
5816
6069
  return null;
5817
6070
  }
@@ -6018,7 +6271,7 @@ async function defaultFlow(rawPath, opts) {
6018
6271
  await runStartupChangelogCheck();
6019
6272
  await promptForUpdateOrLog();
6020
6273
  await recordProject(projectRoot);
6021
- const scan = await scanCommand(rawPath);
6274
+ const scan = await scanCommand(rawPath, { full: opts.full });
6022
6275
  const mcpHandle = await startServer(paths);
6023
6276
  let dashboardHandle = null;
6024
6277
  try {
@@ -6065,11 +6318,11 @@ function buildProgram() {
6065
6318
  {
6066
6319
  default: true
6067
6320
  }
6068
- ).option("--resume <id>", "Resume an existing Claude session (only with --launch-cli)").option("--launch-cli", "Also spawn `claude` CLI in this terminal (legacy M3 behavior)", false).action(async (path, opts) => {
6321
+ ).option("--resume <id>", "Resume an existing Claude session (only with --launch-cli)").option("--launch-cli", "Also spawn `claude` CLI in this terminal (legacy M3 behavior)", false).option("--full", "Re-parse every file, ignoring the incremental parse cache", false).action(async (path, opts) => {
6069
6322
  await defaultFlow(path ?? ".", opts);
6070
6323
  });
6071
- prog.command("scan [path]", "Scan only \u2014 walk + parse + write graph.").action(async (path) => {
6072
- await scanCommand(path ?? ".");
6324
+ prog.command("scan [path]", "Scan only \u2014 walk + parse + write graph.").option("--full", "Re-parse every file, ignoring the incremental parse cache", false).action(async (path, opts) => {
6325
+ await scanCommand(path ?? ".", { full: opts.full });
6073
6326
  });
6074
6327
  prog.command("serve [path]", "Start the HTTP MCP server against a scanned project.").action(async (path) => {
6075
6328
  await serveCommand(path ?? ".");