@progen-labs/spectra 0.1.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,291 @@
1
+ /**
2
+ * The machine-readable side of Spectra: what `--json` prints, what `--out`
3
+ * writes, and what editor / CI / AI integrations consume.
4
+ *
5
+ * Pure functions, no I/O. A "result" is a small, versioned envelope that
6
+ * points at the full reports on disk instead of embedding them:
7
+ *
8
+ * kind 'profile' one or more page-load / live recordings of one page,
9
+ * reduced to a median summary plus the findings
10
+ * kind 'evaluation' the golden-path suite: one summary per journey
11
+ * kind 'compare' before vs after, judged against run-to-run noise
12
+ *
13
+ * Every summary metric is "lower is better", which is what budgets assume.
14
+ */
15
+ import { compareSides, findingKey, median } from './compare.js';
16
+
17
+ /** Bumped on breaking changes to the saved report format. */
18
+ export const REPORT_SCHEMA_VERSION = 1;
19
+ /** Bumped on breaking changes to the result envelopes below. */
20
+ export const RESULT_SCHEMA_VERSION = 1;
21
+
22
+ const finite = v => typeof v === 'number' && Number.isFinite(v);
23
+ const round = (v, d = 1) => (finite(v) ? Math.round(v * 10 ** d) / 10 ** d : null);
24
+
25
+ // ------------------------------------------------------------------ summaries
26
+
27
+ /**
28
+ * The headline numbers budgets and summaries are built from. `get` reads a
29
+ * full report; every entry is lower-is-better.
30
+ */
31
+ export const SUMMARY_METRICS = [
32
+ { key: 'tbtMs', label: 'Total blocking time', unit: 'ms', get: r => r.vitals?.tbtMs },
33
+ { key: 'lcpMs', label: 'Largest contentful paint', unit: 'ms', get: r => r.vitals?.lcpMs },
34
+ { key: 'fcpMs', label: 'First contentful paint', unit: 'ms', get: r => r.vitals?.fcpMs },
35
+ { key: 'cls', label: 'Cumulative layout shift', unit: 'score', digits: 3, get: r => r.vitals?.cls },
36
+ { key: 'inpMs', label: 'Interaction to next paint', unit: 'ms', get: r => r.vitals?.inpMs },
37
+ { key: 'ttfbMs', label: 'Time to first byte', unit: 'ms', get: r => r.vitals?.ttfbMs },
38
+ { key: 'longTaskCount', label: 'Long tasks', unit: 'count', get: r => r.vitals?.longTaskCount },
39
+ { key: 'maxLongTaskMs', label: 'Longest task', unit: 'ms', get: r => r.vitals?.maxLongTaskMs },
40
+ { key: 'scriptingMs', label: 'Scripting time', unit: 'ms', get: r => r.totals?.scriptingMs },
41
+ { key: 'forcedLayoutCount', label: 'Forced layouts', unit: 'count', get: r => r.totals?.forcedLayoutCount },
42
+ { key: 'domNodes', label: 'DOM nodes', unit: 'count', get: r => r.memory?.domNodes },
43
+ { key: 'heapUsedMb', label: 'JS heap used', unit: 'MB', get: r => r.memory?.heapUsedMb },
44
+ {
45
+ key: 'transferKb',
46
+ label: 'Transferred',
47
+ unit: 'KB',
48
+ get: r => (finite(r.network?.totalEncoded) ? r.network.totalEncoded / 1024 : null),
49
+ },
50
+ {
51
+ key: 'criticalFindings',
52
+ label: 'Critical findings',
53
+ unit: 'count',
54
+ get: r => (Array.isArray(r.findings) ? r.findings.filter(f => f.severity === 'critical').length : null),
55
+ },
56
+ {
57
+ key: 'findingCount',
58
+ label: 'Findings',
59
+ unit: 'count',
60
+ get: r => (Array.isArray(r.findings) ? r.findings.length : null),
61
+ },
62
+ ];
63
+
64
+ const metricDef = key => SUMMARY_METRICS.find(m => m.key === key);
65
+ const valueOf = (def, r) => {
66
+ const v = def.get(r);
67
+ return finite(v) ? v : null;
68
+ };
69
+
70
+ /** Headline numbers of one report; null where the recording has no value. */
71
+ export function summarize(report) {
72
+ const out = {};
73
+ for (const def of SUMMARY_METRICS) out[def.key] = round(valueOf(def, report), def.digits ?? 1);
74
+ return out;
75
+ }
76
+
77
+ /**
78
+ * Median of each headline number across runs, plus the min / max spread
79
+ * (only when there is more than one run, so the spread means something).
80
+ */
81
+ export function summarizeRuns(reports) {
82
+ const summary = { runs: reports.length };
83
+ const spread = {};
84
+ for (const def of SUMMARY_METRICS) {
85
+ const values = reports.map(r => valueOf(def, r)).filter(finite);
86
+ summary[def.key] = values.length ? round(median(values), def.digits ?? 1) : null;
87
+ if (reports.length > 1 && values.length > 1) {
88
+ spread[def.key] = {
89
+ min: round(Math.min(...values), def.digits ?? 1),
90
+ max: round(Math.max(...values), def.digits ?? 1),
91
+ };
92
+ }
93
+ }
94
+ return { summary, spread };
95
+ }
96
+
97
+ /**
98
+ * The run that best stands for the set: closest to the median on main-thread
99
+ * cost. Its findings are the ones reported, so they match the summary.
100
+ */
101
+ export function representativeRun(reports) {
102
+ if (reports.length <= 2) return reports[reports.length - 1] || null;
103
+ const keys = ['tbtMs', 'scriptingMs', 'lcpMs'];
104
+ const medians = Object.fromEntries(
105
+ keys.map(k => [k, median(reports.map(r => valueOf(metricDef(k), r)).filter(finite))]),
106
+ );
107
+ let best = null;
108
+ let bestScore = Infinity;
109
+ for (const r of reports) {
110
+ let score = 0;
111
+ for (const k of keys) {
112
+ const v = valueOf(metricDef(k), r);
113
+ const m = medians[k];
114
+ if (finite(v) && finite(m)) score += Math.abs(v - m) / Math.max(Math.abs(m), 1);
115
+ }
116
+ if (score < bestScore) {
117
+ best = r;
118
+ bestScore = score;
119
+ }
120
+ }
121
+ return best;
122
+ }
123
+
124
+ /** A finding trimmed to what a reader (or an agent) acts on. */
125
+ export function briefFinding(f) {
126
+ const top = (f.culprits || [])[0];
127
+ return {
128
+ id: f.id,
129
+ severity: f.severity,
130
+ category: f.category,
131
+ title: f.title,
132
+ detail: f.detail || '',
133
+ impactMs: round(f.impactMs || 0),
134
+ file: f.file || top?.file || null,
135
+ line: top?.line ?? null,
136
+ fix: f.fix || '',
137
+ culprits: (f.culprits || []).slice(0, 5).map(c => ({
138
+ fn: c.fn,
139
+ file: c.file || null,
140
+ line: c.line ?? null,
141
+ lineKind: c.lineKind || null,
142
+ abs: c.abs || null,
143
+ ms: round(c.ms),
144
+ note: c.note || '',
145
+ })),
146
+ };
147
+ }
148
+
149
+ // ------------------------------------------------------------------ budgets
150
+
151
+ export const BUDGET_KEYS = SUMMARY_METRICS.map(m => m.key);
152
+
153
+ /**
154
+ * Validates a `budgets` object from spectra.config.json.
155
+ * @returns {{ok: true, budgets: object} | {ok: false, error: string}}
156
+ */
157
+ export function validateBudgets(budgets) {
158
+ if (budgets == null) return { ok: true, budgets: {} };
159
+ if (typeof budgets !== 'object' || Array.isArray(budgets))
160
+ return { ok: false, error: '"budgets" must be an object such as { "tbtMs": 200 }.' };
161
+ for (const [key, limit] of Object.entries(budgets)) {
162
+ if (!BUDGET_KEYS.includes(key))
163
+ return { ok: false, error: `Unknown budget "${key}". Known budgets: ${BUDGET_KEYS.join(', ')}.` };
164
+ if (!finite(limit) || limit < 0)
165
+ return { ok: false, error: `Budget "${key}" must be a number ≥ 0 (got ${JSON.stringify(limit)}).` };
166
+ }
167
+ return { ok: true, budgets };
168
+ }
169
+
170
+ /**
171
+ * Checks budgets (upper limits) against one or more measured summaries, e.g.
172
+ * the median of a profile, or every journey of an evaluation. A metric the
173
+ * recording did not measure is reported as skipped, never as a failure.
174
+ * @param {object} budgets { tbtMs: 200, ... } (already validated)
175
+ * @param {{name: string, summary: object}[]} items
176
+ */
177
+ export function checkBudgets(budgets, items) {
178
+ const entries = Object.entries(budgets || {});
179
+ if (!entries.length) return null;
180
+ const checks = [];
181
+ for (const [key, limit] of entries) {
182
+ const def = metricDef(key);
183
+ for (const item of items) {
184
+ const value = item.summary?.[key];
185
+ checks.push({
186
+ key,
187
+ label: def.label,
188
+ unit: def.unit,
189
+ limit,
190
+ value: finite(value) ? value : null,
191
+ where: item.name,
192
+ status: !finite(value) ? 'skipped' : value <= limit ? 'pass' : 'fail',
193
+ });
194
+ }
195
+ }
196
+ const failed = checks.filter(c => c.status === 'fail');
197
+ return { ok: failed.length === 0, failed: failed.length, checks };
198
+ }
199
+
200
+ // ------------------------------------------------------------------ envelopes
201
+
202
+ const envelope = (kind, spectraVersion) => ({
203
+ schemaVersion: RESULT_SCHEMA_VERSION,
204
+ kind,
205
+ spectraVersion: spectraVersion || null,
206
+ createdAt: new Date().toISOString(),
207
+ });
208
+
209
+ /**
210
+ * @param {object} o
211
+ * @param {object[]} o.reports full reports, in run order
212
+ * @param {(id: string) => string} o.fileOf where each report is saved
213
+ */
214
+ export function buildProfileResult({ reports, fileOf, target, spectraVersion, budgets }) {
215
+ const first = reports[0];
216
+ const { summary, spread } = summarizeRuns(reports);
217
+ const rep = representativeRun(reports);
218
+ const keysPerRun = reports.map(r => new Set((r.findings || []).map(findingKey)));
219
+ const findings = (rep?.findings || []).map(f => {
220
+ const brief = briefFinding(f);
221
+ if (reports.length > 1) brief.seenIn = keysPerRun.filter(s => s.has(findingKey(f))).length;
222
+ return brief;
223
+ });
224
+ return {
225
+ ...envelope('profile', spectraVersion),
226
+ target: target || null,
227
+ url: first?.meta?.url || null,
228
+ mode: first?.meta?.mode || null,
229
+ cpuThrottle: first?.meta?.cpuThrottle || 1,
230
+ git: first?.meta?.git || null,
231
+ runs: reports.map(r => ({ id: r.id, file: fileOf ? fileOf(r.id) : null, summary: summarize(r) })),
232
+ representativeRunId: rep?.id || null,
233
+ summary,
234
+ spread,
235
+ findings,
236
+ budgets: checkBudgets(budgets, [{ name: 'median', summary }]),
237
+ };
238
+ }
239
+
240
+ /**
241
+ * @param {object} o
242
+ * @param {object} o.evaluation the object runEvaluation() returns
243
+ * @param {(id: string) => object|null} o.readReport
244
+ */
245
+ export function buildEvaluationResult({ evaluation, readReport, fileOf, file, spectraVersion, budgets }) {
246
+ const journey = j => {
247
+ const report = j.id ? readReport(j.id) : null;
248
+ return {
249
+ title: j.title,
250
+ kind: j.kind,
251
+ ok: j.ok,
252
+ error: j.error || null,
253
+ reportId: j.id || null,
254
+ reportFile: j.id && fileOf ? fileOf(j.id) : null,
255
+ summary: report ? summarize(report) : null,
256
+ findings: report ? (report.findings || []).slice(0, 5).map(briefFinding) : [],
257
+ };
258
+ };
259
+ const journeys = [journey(evaluation.baseline), ...evaluation.paths.map(journey)];
260
+ return {
261
+ ...envelope('evaluation', spectraVersion),
262
+ target: evaluation.target || null,
263
+ url: evaluation.url,
264
+ file: file || null,
265
+ discovered: evaluation.discovered,
266
+ skippedDestructive: evaluation.skippedDestructive || [],
267
+ passed: evaluation.paths.filter(p => p.ok).length,
268
+ total: evaluation.paths.length,
269
+ journeys,
270
+ budgets: checkBudgets(
271
+ budgets,
272
+ journeys.filter(j => j.summary).map(j => ({ name: j.title, summary: j.summary })),
273
+ ),
274
+ };
275
+ }
276
+
277
+ /** Before vs after; the comparison itself is shared/compare.js (COMPARE_VERSION). */
278
+ export function buildCompareResult({ before, after, spectraVersion }) {
279
+ const comparison = compareSides({ runs: before }, { runs: after });
280
+ if (!comparison) return null;
281
+ return { ...envelope('compare', spectraVersion), comparison };
282
+ }
283
+
284
+ /** The saved reports, newest first (rows from recorder.listReports()). */
285
+ export function buildReportsResult({ reports, dir, spectraVersion }) {
286
+ return { ...envelope('reports', spectraVersion), dir: dir || null, reports };
287
+ }
288
+
289
+ /** A compare counts as a regression when any key metric got worse. */
290
+ export const isRegression = result =>
291
+ ['regressed', 'mixed'].includes(result?.comparison?.verdict);
package/src/styles.css CHANGED
@@ -656,14 +656,23 @@ label.switch {
656
656
  }
657
657
  .main-tabs {
658
658
  align-items: center;
659
+ flex-wrap: wrap;
660
+ row-gap: 0.4rem;
661
+ overflow-x: visible;
659
662
  padding: 0.5rem 0.9rem;
660
663
  border-bottom: 1px solid var(--border);
661
664
  }
665
+ /* Wrap rather than scroll: a scrolled-away tab (Compare commits, on a laptop
666
+ screen) is a tab nobody finds, since overlay scrollbars stay hidden. */
662
667
  .main-tabs .tab-list {
663
668
  display: flex;
669
+ flex-wrap: wrap;
664
670
  gap: 0.2rem;
665
671
  min-width: 0;
666
- overflow-x: auto;
672
+ }
673
+ .main-tabs label.switch {
674
+ white-space: nowrap;
675
+ margin-left: auto;
667
676
  }
668
677
  .tabs button {
669
678
  background: transparent;
@@ -1188,7 +1197,8 @@ tr.detail td {
1188
1197
  }
1189
1198
  .task-cols {
1190
1199
  display: grid;
1191
- grid-template-columns: 1fr 1fr;
1200
+ /* minmax(0, …): a long file path must not push its column out of the card */
1201
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
1192
1202
  gap: 1rem;
1193
1203
  }
1194
1204
  .task-cols h4 {
@@ -1198,17 +1208,24 @@ tr.detail td {
1198
1208
  letter-spacing: 0.04em;
1199
1209
  color: var(--text-2);
1200
1210
  }
1201
- .row {
1211
+ .row,
1212
+ button.link.row {
1202
1213
  display: flex;
1203
1214
  justify-content: space-between;
1204
1215
  gap: 0.6rem;
1205
1216
  width: 100%;
1217
+ text-align: left;
1206
1218
  }
1207
1219
  .row span {
1220
+ min-width: 0;
1208
1221
  overflow: hidden;
1209
1222
  text-overflow: ellipsis;
1210
1223
  white-space: nowrap;
1211
1224
  }
1225
+ .row b {
1226
+ flex: none;
1227
+ white-space: nowrap;
1228
+ }
1212
1229
 
1213
1230
  /* ---- source view ---- */
1214
1231
  .code-scroll {
@@ -1 +0,0 @@
1
- :root{--font-ui:-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", system-ui, sans-serif;--font-mono:ui-monospace, "SF Mono", Menlo, Consolas, monospace;--radius:14px;--radius-sm:9px;--btn-radius:9px;--blur:none;--on-accent:#fff;--label-case:none;--label-tracking:0}:root[data-theme=glass]{--radius:22px;--radius-sm:12px;--btn-radius:999px;--blur:blur(28px) saturate(180%);--font-ui:-apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", system-ui, sans-serif}:root[data-theme=glass][data-mode=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:radial-gradient(900px 600px at 8% -8%, #b9dcff 0%, transparent 62%), radial-gradient(800px 560px at 96% 4%, #ffd3ea 0%, transparent 58%), radial-gradient(1000px 700px at 62% 112%, #d6cfff 0%, transparent 62%), #eaeff7;--panel:#ffffff80;--panel-solid:#f4f7fc;--panel-border:#ffffffbf;--panel-edge:inset 0 1px 0 #ffffffe6, inset 0 -1px 0 #7887aa14;--shadow:0 1px 2px #283c6e0f, 0 12px 36px #283c6e1f;--shadow-pop:0 4px 12px #283c6e1f, 0 24px 60px #283c6e38;--surface:#f6f8fc;--surface-2:#ffffff9e;--control:#ffffffb3;--control-hover:#fffffff2;--border:#46557d29;--text:#0e1220;--text-2:#444c5e;--text-muted:#6d7588;--grid:#46557d1a;--accent:#0a67d1;--accent-strong:#0a6fdb;--accent-soft:#0a6fdb1f;--accent-glow:0 6px 18px #0a6fdb59;--heat:#f2602f;--cat-1:#0a6fdb;--cat-2:#f2602f;--cat-3:#1fa971;--cat-4:#eba000;--cat-5:#e5679b;--cat-6:#5b54d6;--cat-other:#a3a9b8;--good:#1a7f37;--warn:#a35607;--poor:#c9302c;--code-string:#17754f;--code-keyword:#5b54d6;--code-number:#a35607}:root[data-theme=glass][data-mode=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:radial-gradient(900px 600px at 6% -10%, #2463eb80 0%, transparent 62%), radial-gradient(800px 560px at 98% 0%, #a838c861 0%, transparent 58%), radial-gradient(1000px 700px at 60% 115%, #1496aa4d 0%, transparent 62%), #0a0c13;--panel:#22263685;--panel-solid:#161924;--panel-border:#ffffff24;--panel-edge:inset 0 1px 0 #ffffff2e, inset 0 -1px 0 #ffffff08;--shadow:0 1px 2px #0000004d, 0 16px 44px #00000061;--shadow-pop:0 4px 14px #0006, 0 28px 70px #0000008c;--surface:#12141d;--surface-2:#ffffff0f;--control:#ffffff17;--control-hover:#ffffff29;--border:#ffffff21;--text:#f4f6fb;--text-2:#b8bfd0;--text-muted:#8990a3;--grid:#ffffff12;--accent:#4da3ff;--accent-strong:#0a6fdb;--accent-soft:#4da3ff2e;--accent-glow:0 6px 22px #0a6fdb80;--heat:#ff7a4d;--cat-1:#4da3ff;--cat-2:#ff7a4d;--cat-3:#30c98a;--cat-4:#f0b429;--cat-5:#f06ea0;--cat-6:#9a93f5;--cat-other:#6c7386;--good:#3fcf6b;--warn:#f0b04a;--poor:#ff6b6b;--code-string:#3fd49b;--code-keyword:#a29bf7;--code-number:#f0b04a}:root[data-theme=space]{--radius:12px;--radius-sm:8px;--btn-radius:8px;--blur:blur(10px);--label-case:uppercase;--label-tracking:.08em;--font-ui:"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif}:root[data-theme=space][data-mode=dark]{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;--bg:radial-gradient(1px 1px at 22px 34px, #ffffffe6, transparent), radial-gradient(1px 1px at 96px 128px, #fff9, transparent), radial-gradient(1.5px 1.5px at 168px 62px, #a0c8ffe6, transparent), radial-gradient(1px 1px at 58px 176px, #ffffff80, transparent), radial-gradient(1.5px 1.5px at 190px 190px, #c8b4ffcc, transparent), radial-gradient(900px 620px at 82% -12%, #7c5cff4d, transparent 62%), radial-gradient(800px 600px at -6% 108%, #00beff2e, transparent 60%), #04050d;--bg-size:210px 210px, 210px 210px, 210px 210px, 210px 210px, 210px 210px, auto, auto, auto;--panel:#0c1024bd;--panel-solid:#0b0f24;--panel-border:#6e82ff3d;--panel-edge:inset 0 1px 0 #a0b4ff1a;--shadow:0 0 0 1px #0006, 0 18px 50px #0000008c;--shadow-pop:0 0 0 1px #6e82ff40, 0 0 40px #6464ff2e, 0 28px 70px #000000b3;--surface:#0a0d20;--surface-2:#6e82ff12;--control:#6e82ff1a;--control-hover:#6e82ff33;--border:#6e82ff38;--text:#eaeefc;--text-2:#aab3d6;--text-muted:#7b85ad;--grid:#6e82ff1a;--accent:#6f9bff;--accent-strong:#4565f0;--accent-soft:#6f9bff29;--accent-glow:0 0 22px #506eff8c;--accent-2:#22d3ee;--heat:#ff8a3d;--cat-1:#6f9bff;--cat-2:#ff8a3d;--cat-3:#2dd4a0;--cat-4:#fbbf24;--cat-5:#f472b6;--cat-6:#a78bfa;--cat-other:#5f6890;--good:#34d98b;--warn:#fbbf24;--poor:#ff6b81;--code-string:#34d9a0;--code-keyword:#b49bff;--code-number:#fbbf24}:root[data-theme=space][data-mode=light]{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light;--bg:radial-gradient(1px 1px at 22px 34px, #4f46e559, transparent), radial-gradient(1px 1px at 120px 130px, #0e74904d, transparent), radial-gradient(1.5px 1.5px at 170px 70px, #4f46e54d, transparent), radial-gradient(900px 600px at 84% -12%, #818cf852, transparent 62%), radial-gradient(800px 600px at -6% 108%, #22d3ee33, transparent 60%), #eef0fa;--bg-size:210px 210px, 210px 210px, 210px 210px, auto, auto, auto;--panel:#fffc;--panel-solid:#f8f9ff;--panel-border:#4f46e533;--panel-edge:inset 0 1px 0 #ffffffe6;--shadow:0 1px 2px #1e1b640f, 0 14px 40px #1e1b641f;--shadow-pop:0 0 0 1px #4f46e526, 0 24px 60px #1e1b643d;--surface:#f8f9ff;--surface-2:#4f46e50d;--control:#ffffffe6;--control-hover:#fff;--border:#4f46e533;--text:#0f1226;--text-2:#424a6b;--text-muted:#6a7192;--grid:#4f46e51a;--accent:#4338ca;--accent-strong:#4f46e5;--accent-soft:#4f46e51a;--accent-glow:0 6px 20px #4f46e559;--accent-2:#0e7490;--heat:#e0560c;--cat-1:#4f46e5;--cat-2:#e0560c;--cat-3:#0f9f78;--cat-4:#d99100;--cat-5:#d6458a;--cat-6:#7c3aed;--cat-other:#9aa0bd;--good:#14803f;--warn:#9a5200;--poor:#c62839;--code-string:#0f7a5a;--code-keyword:#6d3fd6;--code-number:#9a5200}:root[data-theme=glass] button.primary,:root[data-theme=glass] .tabs button[aria-selected=true]{background:linear-gradient(180deg, #ffffff47, transparent 55%), var(--accent-strong)}:root[data-theme=space] button.primary{background:linear-gradient(135deg, var(--accent-strong), #8b5cf6);border-color:#0000}:root[data-theme=space] .brand-mark{background:conic-gradient(from 210deg,#22d3ee,#6f6bff,#c084fc,#22d3ee)}:root[data-theme=glass] .brand-mark{background:linear-gradient(135deg,#5ac8fa,#0a84ff 55%,#bf5af2)}*{box-sizing:border-box;scrollbar-width:thin;scrollbar-color:var(--border) transparent}html,body,#root{height:100%;margin:0}body{background:var(--bg);background-size:var(--bg-size,auto);color:var(--text);font:.8125rem/1.45 var(--font-ui);-webkit-font-smoothing:antialiased;background-attachment:fixed}button,input,select{font:inherit;color:inherit}button,select,input:not([type=checkbox]):not([type=radio]){background:var(--control);border:1px solid var(--border);border-radius:var(--radius-sm);min-height:2rem;padding:.35rem .75rem;transition:background .15s,border-color .15s,box-shadow .15s}button{border-radius:var(--btn-radius);cursor:pointer;white-space:nowrap;justify-content:center;align-items:center;gap:.4rem;font-weight:500;display:inline-flex}select{cursor:pointer}input:not([type=checkbox]):not([type=radio]):focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft);outline:none}button:hover:not(:disabled),select:hover:not(:disabled){background:var(--control-hover);border-color:color-mix(in srgb, var(--accent) 55%, var(--border))}button:disabled,select:disabled,input:disabled{opacity:.5;cursor:default}button.primary{background:var(--accent-strong);color:var(--on-accent);box-shadow:var(--accent-glow);border-color:#0000}button.primary:hover:not(:disabled){background:var(--accent-strong);filter:brightness(1.1);border-color:#0000}button.danger{background:var(--poor);color:#fff;border-color:#0000}button.danger:hover:not(:disabled){background:var(--poor);filter:brightness(1.08);border-color:#0000}button.ghost{color:var(--text-2);background:0 0;border-color:#0000}button.ghost:hover:not(:disabled){background:var(--control);border-color:var(--border);color:var(--text)}button.ghost.destructive:hover:not(:disabled){color:var(--poor)}button.link{min-height:0;color:var(--accent);text-align:left;font-weight:inherit;background:0 0;border:0;padding:0;display:inline}button.link:hover:not(:disabled){background:0 0;text-decoration:underline}:focus-visible{outline:2px solid var(--accent);outline-offset:2px}code,.mono{font-family:var(--font-mono);font-size:12px}.muted{color:var(--text-muted);font-weight:400}.warn{color:var(--warn)}.spacer{flex:1}.pad{padding:1rem}.num{text-align:right;font-variant-numeric:tabular-nums}.empty{text-align:center;color:var(--text-muted);flex:1;place-content:center;gap:.4rem;padding:2rem;display:grid}.icon{flex:none}.app{flex-direction:column;gap:.7rem;height:100%;padding:.75rem;display:flex}.topbar,.session,.workspace{background:var(--panel);-webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);border:1px solid var(--panel-border);border-radius:var(--radius);box-shadow:var(--shadow), var(--panel-edge)}.topbar{z-index:10;align-items:center;gap:.8rem;padding:.55rem .7rem .55rem .9rem;display:flex;position:relative}.brand{align-items:center;gap:.7rem;min-width:0;display:flex}.brand-mark{border-radius:calc(var(--radius-sm) - 1px);color:#fff;place-items:center;width:2rem;height:2rem;display:grid;box-shadow:inset 0 1px #fff6,0 4px 14px #2850dc59}.brand h1{letter-spacing:-.01em;margin:0;font-size:.95rem;font-weight:650;line-height:1.2}.brand small{color:var(--text-muted);font-size:.72rem;font-family:var(--font-mono);display:block}.report-picker{align-items:center;gap:.3rem;display:inline-flex}.report-picker select{max-width:24rem}.error{background:color-mix(in srgb, var(--poor) 14%, var(--panel-solid));border:1px solid color-mix(in srgb, var(--poor) 55%, transparent);border-radius:var(--radius-sm);align-items:center;gap:.8rem;padding:.5rem .8rem;display:flex}.error button{margin-left:auto}.popover{display:inline-block;position:relative}.caret{opacity:.7;font-size:.7em}.popover-panel,.trace-note{background:color-mix(in srgb, var(--panel-solid) 98%, transparent);-webkit-backdrop-filter:blur(24px)saturate(160%);backdrop-filter:blur(24px)saturate(160%);border:1px solid var(--panel-border);border-radius:var(--radius);box-shadow:var(--shadow-pop), var(--panel-edge)}.popover-panel{z-index:40;min-width:15rem;padding:.9rem;position:absolute;top:calc(100% + 8px)}.popover-panel.align-right{right:0}.popover-panel.align-left{left:0}.appearance,.options-list{gap:1rem;width:17rem;display:grid}.choice-group{border:0;min-width:0;margin:0;padding:0}.choice-group legend,.field-label{color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:.4rem;padding:0;font-size:.7rem;font-weight:600}.choice input{opacity:0;pointer-events:none;position:absolute}.choice:focus-within{outline:2px solid var(--accent);outline-offset:2px}.theme-cards{grid-template-columns:1fr 1fr;gap:.5rem;display:grid}.theme-cards .choice{border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;background:var(--surface-2);gap:.15rem;padding:.45rem;transition:border-color .15s,box-shadow .15s;display:grid;position:relative}.theme-cards .choice:hover{border-color:color-mix(in srgb, var(--accent) 55%, var(--border))}.theme-cards .choice.selected{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.theme-cards .choice-label{margin-top:.25rem;font-weight:600}.theme-cards small{font-size:.7rem;line-height:1.25}.swatch{border-radius:calc(var(--radius-sm) - 3px);height:2.9rem;display:block;position:relative;overflow:hidden}.swatch i{border-radius:999px;height:.5rem;position:absolute;left:.45rem}.swatch i:first-child{width:55%;top:.5rem}.swatch i:last-child{width:35%;top:1.35rem}.swatch-glass{background:radial-gradient(circle at 20% 20%,#8ec5ff,#0000 60%),radial-gradient(circle at 85% 30%,#ffb8df,#0000 55%),linear-gradient(135deg,#c9bcff,#9fd6ff)}.swatch-glass i{background:#ffffffb3;box-shadow:inset 0 1px #fff}.swatch-space{background:radial-gradient(1px 1px at 70% 25%,#fff,#0000),radial-gradient(1px 1px at 88% 70%,#bcd4ff,#0000),radial-gradient(1px 1px at 45% 80%,#fff,#0000),radial-gradient(circle at 90% 0,#7c5cff99,#0000 60%),#05071a}.swatch-space i{background:linear-gradient(90deg,#22d3ee,#6f6bff)}.segmented,.segmented-control{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--btn-radius);gap:2px;padding:3px;display:inline-flex}.segmented-control{display:flex}.segmented button,.segmented-control .choice{border-radius:calc(var(--btn-radius) - 2px);min-height:1.7rem;color:var(--text-2);text-align:center;cursor:pointer;background:0 0;border:0;flex:1;padding:.15rem .8rem;font-weight:500;position:relative}.segmented-control .choice{place-items:center;display:grid}.segmented button:hover:not(:disabled){background:var(--control);border:0}.segmented button[aria-pressed=true],.segmented-control .choice.selected{background:var(--accent-soft);color:var(--accent);font-weight:600}label.switch{cursor:pointer;color:var(--text-2);-webkit-user-select:none;user-select:none;align-items:center;gap:.5rem;display:inline-flex}.switch input{appearance:none;background:var(--control);border:1px solid var(--border);cursor:pointer;border-radius:999px;flex:none;width:2rem;height:1.15rem;margin:0;transition:background .15s;position:relative}.switch input:after{content:"";background:var(--text-muted);border-radius:50%;width:calc(1.15rem - 4px);height:calc(1.15rem - 4px);transition:transform .15s,background .15s;position:absolute;top:1px;left:1px}.switch input:checked{background:var(--accent-strong);border-color:#0000}.switch input:checked:after{background:#fff;transform:translate(.85rem)}.switch input:disabled{cursor:default}.session{z-index:5;gap:.6rem;padding:.7rem .8rem;display:grid;position:relative}.session-row{flex-wrap:wrap;align-items:center;gap:.5rem;display:flex}.session-row+.session-row{border-top:1px solid var(--border);padding-top:.6rem}.session label,.connect label{color:var(--text-2)}.address{min-width:14rem;font-family:var(--font-mono);flex:1;font-size:12px}.btn-group{align-items:center;gap:.4rem;display:inline-flex}.connect{gap:.7rem;display:grid}.connect-head h2{letter-spacing:-.01em;margin:0;font-size:1rem}.connect-head p{margin:.15rem 0 0}.connect-main{flex-wrap:wrap;align-items:flex-end;gap:.5rem;display:flex}.connect-main .field{flex:1;min-width:16rem}.field input{width:100%}.field-row input{flex:1;min-width:0}.advanced{border-top:1px solid var(--border);gap:.8rem;padding-top:.7rem;display:grid}.advanced-section{gap:.4rem;display:grid}.advanced-section+.advanced-section{border-top:1px dashed var(--border);padding-top:.7rem}.advanced-row{flex-wrap:wrap;align-items:flex-end;gap:.5rem;display:flex}.advanced-row .field{flex:1;min-width:16rem}.pill{border:1px solid var(--border);background:var(--surface-2);white-space:nowrap;border-radius:999px;align-items:center;gap:.35rem;padding:.1rem .65rem;font-size:.75rem;display:inline-flex}.pill.ok{color:var(--good);border-color:color-mix(in srgb, var(--good) 40%, transparent)}.pill.warn{color:var(--warn);border-color:color-mix(in srgb, var(--warn) 45%, transparent)}.rec-strip{flex-wrap:wrap;flex:1;align-items:center;gap:.8rem;display:flex}.rec-strip.recording{border-radius:var(--radius-sm);background:color-mix(in srgb, var(--poor) 10%, transparent);border:1px solid color-mix(in srgb, var(--poor) 35%, transparent);padding:.4rem .6rem}.rec{align-items:center;gap:.5rem;font-weight:600;display:inline-flex}.rec i{background:var(--poor);width:10px;height:10px;box-shadow:0 0 0 4px color-mix(in srgb, var(--poor) 25%, transparent);border-radius:50%;animation:1s infinite pulse}.rec i.spin{background:var(--accent);box-shadow:0 0 0 4px var(--accent-soft)}.elapsed{font-family:var(--font-mono);font-variant-numeric:tabular-nums;color:var(--text-2);font-weight:400}@keyframes pulse{50%{opacity:.3}}@media (prefers-reduced-motion:reduce){.rec i{animation:none}*{transition:none!important}}.workspace{flex-direction:column;flex:1;min-height:0;display:flex;overflow:hidden}.report-bar{border-bottom:1px solid var(--border);flex-wrap:wrap;align-items:center;gap:.8rem;padding:.6rem .9rem;display:flex}.report-title{text-overflow:ellipsis;white-space:nowrap;max-width:28rem;font-size:.9rem;font-weight:600;overflow:hidden}.report-meta{flex-wrap:wrap;gap:.35rem;display:flex}.tabs{gap:.2rem;display:flex;overflow-x:auto}.main-tabs{border-bottom:1px solid var(--border);align-items:center;padding:.5rem .9rem}.main-tabs .tab-list{gap:.2rem;min-width:0;display:flex;overflow-x:auto}.tabs button{min-height:1.9rem;color:var(--text-2);background:0 0;border:0;padding:.25rem .8rem}.tabs button:hover:not(:disabled){background:var(--control);color:var(--text);border:0}.tabs button[aria-selected=true]{background:var(--accent-strong);color:var(--on-accent);box-shadow:var(--accent-glow)}.count{background:var(--accent-soft);color:var(--accent);border-radius:999px;margin-left:.1rem;padding:0 .45rem;font-size:11px;font-weight:600}.tabs button[aria-selected=true] .count{color:inherit;background:#ffffff40}.main{flex:1;min-height:0;display:flex}.content{flex-direction:column;flex:1;min-width:0;display:flex;overflow:auto}.welcome{text-align:center;flex:1;align-content:center;justify-items:center;gap:1.6rem;padding:2rem 1.5rem;display:grid;overflow:auto}.welcome h2{letter-spacing:-.02em;margin:0;font-size:1.6rem}.welcome-lead{color:var(--text-2);margin:.4rem 0 0;font-size:.95rem}.steps{text-align:left;grid-template-columns:repeat(3,minmax(0,17rem));gap:.9rem;margin:0;padding:0;list-style:none;display:grid}.steps li{background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius);align-content:start;gap:.35rem;padding:1rem;line-height:1.55;display:grid}.step-num{background:var(--accent-soft);width:1.7rem;height:1.7rem;color:var(--accent);border-radius:50%;place-items:center;font-weight:700;display:grid}.steps h3{margin:.2rem 0 0;font-size:.9rem}.steps p{color:var(--text-2);margin:0}.welcome-foot{max-width:44rem;color:var(--text-muted);margin:0}@media (width<=900px){.steps{grid-template-columns:1fr}}.culprits{align-content:start;gap:1.2rem;padding:1rem 1.2rem;display:grid}.culprits h2,.network h2,.long-tasks h2{margin:0 0 .5rem;font-size:.9rem}.vital-label{color:var(--text-2);font-size:12px}.vital-value{font-variant-numeric:tabular-nums;font-size:1.25rem;font-weight:600}.vital-rating{font-size:12px}.rating-good{border-left-color:var(--good)}.rating-good .vital-rating{color:var(--good)}.rating-warn{border-left-color:var(--warn)}.rating-warn .vital-rating{color:var(--warn)}.rating-poor{border-left-color:var(--poor)}.rating-poor .vital-rating{color:var(--poor)}.rating-none{border-left-color:var(--border)}.rating-none .vital-rating{color:var(--text-muted)}.breakdown-bar{background:var(--surface);border-radius:4px;gap:2px;height:16px;display:flex;overflow:hidden}.breakdown-bar.small{height:8px;margin:.35rem 0}.breakdown-bar span{min-width:2px}.breakdown-legend{flex-wrap:wrap;gap:.4rem 1.2rem;margin:.5rem 0 0;padding:0;list-style:none;display:flex}.breakdown-legend i{border-radius:2px;width:10px;height:10px;margin-right:.35rem;display:inline-block}.sev-critical{border-left-color:var(--poor)}.sev-warning{border-left-color:var(--warn)}.sev-info{border-left-color:var(--accent)}.badge-sev{font-size:12px;font-weight:600}.sev-critical .badge-sev{color:var(--poor)}.sev-warning .badge-sev{color:var(--warn)}.sev-info .badge-sev{color:var(--accent)}.chip{border:1px solid var(--border);color:var(--text-2);border-radius:4px;padding:0 .4rem;font-size:11px}.chip-warn{color:var(--warn);border-color:var(--warn)}.tree-pane{flex-direction:column;flex:1;min-height:0;display:flex}.tree-toolbar{border-bottom:1px solid var(--border);flex-wrap:wrap;align-items:center;gap:.8rem;padding:.4rem 1rem;display:flex}.tree-svg{cursor:grab;background:radial-gradient(var(--grid) 1px, transparent 1px) 0 0 / 22px 22px;flex:1;width:100%;min-height:0;display:block}.tree-svg:active{cursor:grabbing}.tree-links path{fill:none;stroke:var(--border);stroke-width:1.2px}.node-hit{fill:#0000}.node:hover .node-hit{fill:var(--surface-2)}.node-dot{fill:color-mix(in srgb, var(--heat) var(--heat-pct,20%), var(--surface));stroke:var(--heat);stroke-width:1.5px;cursor:pointer}.node.vendor .node-dot{fill:var(--surface);stroke:var(--text-muted);stroke-dasharray:2 2}.node.dir .node-dot{stroke-width:2px}.node.selected .node-hit{fill:color-mix(in srgb, var(--accent) 22%, transparent)}.node.selected .node-label{font-weight:700}.node-glyph{fill:var(--text);pointer-events:none;font-size:10px}.node-label{fill:var(--text);cursor:pointer;font-size:12px}.node.dir .node-label{fill:var(--text-2);font-weight:600}.legend{border-top:1px solid var(--border);color:var(--text-2);flex-wrap:wrap;align-items:center;gap:1.2rem;padding:.4rem 1rem;display:flex}.ramp{vertical-align:middle;background:linear-gradient(90deg, color-mix(in srgb, var(--heat) 12%, var(--surface)), var(--heat));border:1px solid var(--heat);border-radius:2px;width:90px;height:10px;margin-right:.4rem;display:inline-block}.ring{border:1.5px dashed var(--text-muted);vertical-align:middle;border-radius:50%;width:10px;height:10px;margin-right:.35rem;display:inline-block}.file-panel{border-left:1px solid var(--border);flex-direction:column;flex:none;width:34rem;max-width:45vw;min-height:0;display:flex}.panel-head{border-bottom:1px solid var(--border);align-items:center;gap:.5rem;padding:.5rem .8rem;display:flex}.panel-head h2{text-overflow:ellipsis;white-space:nowrap;flex:1;margin:0;font-family:ui-monospace,Menlo,monospace;font-size:.8125rem;overflow:hidden}.metrics{grid-template-columns:repeat(3,1fr);gap:.4rem;padding:.6rem .8rem;display:grid}.metric{background:var(--surface-2);border:1px solid var(--border);border-radius:6px;padding:.3rem .5rem}.metric-value{font-variant-numeric:tabular-nums;font-weight:600}.metric.warn .metric-value{color:var(--warn)}.metric-label{color:var(--text-muted);font-size:11px}.mini-findings{gap:.3rem;margin:0 .8rem .5rem;padding:0;list-style:none;display:grid}.mini-findings li{border-left:3px solid;padding-left:.5rem}.file-panel .tabs{border-bottom:1px solid var(--border);padding:0 .8rem}.panel-body{flex-direction:column;flex:1;min-height:0;display:flex;overflow:auto}table.data{border-collapse:collapse;width:100%}table.data th{text-align:left;color:var(--text-2);background:var(--panel-solid);border-bottom:1px solid var(--border);padding:.4rem .6rem;font-weight:600;position:sticky;top:0}table.data th button{color:inherit;background:0 0;border:0;padding:0;font-weight:600}table.data td{border-bottom:1px solid var(--grid);vertical-align:top;padding:.25rem .6rem}tr.open td,tr.detail td{background:var(--surface-2)}.detail-grid{grid-template-columns:1fr 2fr;gap:1.5rem;display:grid}.detail-grid h4{text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:0 0 .3rem;font-size:12px}.detail-grid ul{margin:0;padding-left:1.1rem}.summary-row{color:var(--text-2);border-bottom:1px solid var(--border);flex-wrap:wrap;gap:1.4rem;padding:.6rem 1rem;display:flex}.bar{background:var(--grid);border-radius:2px;height:3px;margin-top:3px}.bar span{background:var(--heat);border-radius:2px;height:100%;display:block}.react-panel,.network{overflow:auto}.network section{margin:1.2rem 0}.timeline{background:var(--surface-2);border-bottom:1px solid var(--border);width:100%;height:130px}.timeline .task{fill:var(--cat-other)}.timeline .task.long{fill:var(--poor)}.timeline .limit{stroke:var(--warn);stroke-dasharray:4 4;vector-effect:non-scaling-stroke}.timeline .axis{fill:var(--text-muted);font-size:11px}.task-list{gap:.7rem;margin:0;padding:.8rem 1rem;list-style:none;display:grid}.task-list>li{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);border-left-width:4px;padding:.6rem .9rem}.task-head{flex-wrap:wrap;align-items:center;gap:.7rem;display:flex}.task-cols{grid-template-columns:1fr 1fr;gap:1rem;display:grid}.task-cols h4{text-transform:uppercase;letter-spacing:.04em;color:var(--text-2);margin:.3rem 0;font-size:12px}.row{justify-content:space-between;gap:.6rem;width:100%;display:flex}.row span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.code-scroll{flex:1;align-items:flex-start;font:12px/18px ui-monospace,Menlo,Consolas,monospace;display:flex;overflow:auto}.gutter{background:var(--surface-2);border-right:1px solid var(--border);text-align:right;flex:none;position:sticky;left:0}.gutter-line{height:18px;color:var(--text-muted);white-space:nowrap;padding:0 .5rem}.gutter-hot{background:color-mix(in srgb, var(--heat) 22%, transparent);color:var(--text);font-weight:600}.code{flex:1;margin:0;padding:0 1rem}.code code.hljs{font:inherit;white-space:pre;background:0 0;padding:0;display:block}.hljs-comment,.hljs-quote{color:var(--text-muted);font-style:italic}.hljs-keyword,.hljs-selector-tag,.hljs-literal{color:var(--code-keyword)}.hljs-string,.hljs-regexp,.hljs-attr{color:var(--code-string)}.hljs-number,.hljs-built_in,.hljs-type{color:var(--code-number)}.hljs-title,.hljs-function,.hljs-name{color:var(--accent)}.culprit-box{border:1px dashed var(--border);background:var(--surface);border-radius:6px;margin:.5rem 0 .2rem;padding:.5rem .7rem}.culprit-box h4{text-transform:uppercase;letter-spacing:.05em;color:var(--text-2);margin:0 0 .3rem;font-size:11px}.culprit-box ul{gap:.25rem;margin:0;padding:0;list-style:none;display:grid}code.fn{color:var(--text);font-weight:700}.code-ref{flex-wrap:wrap;align-items:baseline;gap:.4rem;display:inline-flex}.code-ref b{font-weight:700}.editor-link{color:var(--accent);border:1px solid var(--border);border-radius:4px;padding:0 .3rem;font-size:11px;text-decoration:none}.editor-link:hover{border-color:var(--accent)}table.hot-functions td{vertical-align:middle}tr.vendor-row code.fn{color:var(--text-2);font-weight:400}.trace-button{position:relative}.trace-note{z-index:40;width:30rem;max-width:80vw;padding:.8rem 2rem .8rem 1rem;line-height:1.5;position:absolute;top:calc(100% + 8px);right:0}.trace-note.error{border-color:var(--poor)}.trace-note ol{margin:.4rem 0 0;padding-left:1.2rem}.trace-note code{word-break:break-all}.trace-note .close{background:0 0;border:0;min-height:0;padding:0 .3rem;font-size:1.1rem;line-height:1;position:absolute;top:.4rem;right:.4rem}kbd{border:1px solid var(--border);border-radius:4px;padding:0 .3rem;font-size:11px}.compare-panel{align-content:start;gap:1rem;padding:1rem 1.2rem 2rem;display:grid}.compare-intro h2{margin:0 0 .3rem;font-size:1rem}.sides{grid-template-columns:1fr auto 1fr;align-items:start;gap:.8rem;display:grid}.swap{align-self:center}.swap button{padding:.3rem .6rem;font-size:1.1rem}.side-picker{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);gap:.5rem;padding:.7rem .9rem;display:grid}.side-picker h3{margin:0;font-size:.9rem}.field{color:var(--text-2);gap:.2rem;display:grid}.field-row{gap:.4rem;display:flex}.field input{flex:1;min-width:0}.git-facts{grid-template-columns:5.5rem 1fr;gap:.15rem .6rem;margin:0;display:grid}.git-facts dt{color:var(--text-muted)}.git-facts dd{overflow-wrap:anywhere;margin:0}.dropzone{border:1.5px dashed var(--border);border-radius:var(--radius-sm);flex-wrap:wrap;align-items:center;gap:.6rem;padding:.6rem;display:flex}.dropzone.dragging{border-color:var(--accent);background:color-mix(in srgb, var(--accent) 10%, transparent)}.run-chips{gap:.3rem;margin:0;padding:0;list-style:none;display:grid}.run-chips li{background:var(--surface);border:1px solid var(--border);border-radius:6px;align-items:center;gap:.6rem;padding:.2rem .5rem;display:flex}.run-chips li .mono{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}.run-chips button{background:0 0;border:0;padding:0 .3rem;font-size:1rem}.compare-toolbar{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);z-index:5;flex-wrap:wrap;align-items:center;gap:.6rem;padding:.5rem .7rem;display:flex;position:sticky;top:0}.pdf-buttons{align-items:center;gap:.5rem;display:inline-flex}.compare-results{gap:1.2rem;display:grid}.compare-results h2{margin:0 0 .5rem;font-size:.95rem}.verdict-banner{border:1.5px solid var(--c);border-radius:var(--radius-sm);border-left-width:6px;padding:.7rem 1rem}.verdict-banner .big{color:var(--c);font-size:1.4rem;font-weight:700}.verdict-banner p{margin:.25rem 0}.verdict-banner .counts span{margin-right:1.2rem;font-weight:600}.good{--c:var(--good)}.bad{--c:var(--poor)}.warn{--c:var(--warn)}.same{--c:var(--text-2)}.na{--c:var(--text-muted)}.info{--c:var(--accent)}.verdict-banner .counts .good,.verdict-banner .counts .bad,.verdict-banner .counts .same,.verdict{color:var(--c)}.verdict{white-space:nowrap;font-weight:700}ul.icons{gap:.25rem;margin:0;padding:0;list-style:none;display:grid}ul.icons li{color:var(--text)}ul.icons li .i,ul.checks li .i{width:1.4rem;color:var(--c);font-weight:700;display:inline-block}ul.checks{gap:.25rem;margin:0;padding:0;list-style:none;display:grid}ul.checks li{align-items:baseline;display:flex}.compare-table td,.compare-table th{white-space:nowrap}.compare-table td:first-child{white-space:normal}.compare-table tr.dim td{color:var(--text-muted)}tr.group-row td{background:var(--surface-2);text-transform:uppercase;letter-spacing:.05em;color:var(--text-2);font-size:11px;font-weight:700}.small{font-size:11px}svg.change-bar{display:block}svg.change-bar .axis{stroke:var(--text-2);stroke-width:1px}svg.change-bar rect{fill:var(--c)}.compare-results .segmented{margin-bottom:.5rem}.detail-pane{gap:.6rem;padding-top:.6rem;display:grid}.entity-group{border:1px solid var(--border);border-left:4px solid var(--c,var(--border));border-radius:6px;padding:.3rem .7rem}.entity-group summary{cursor:pointer;padding:.2rem 0;font-weight:600}.entity-name{width:40%}.entity-name>*{margin-right:.4rem}.finding-cards{gap:.5rem;margin:0 0 .6rem;padding:0;list-style:none;display:grid}.finding-card{border:1px solid var(--border);border-left:4px solid var(--c);border-radius:6px;padding:.5rem .8rem}.finding-card h4{margin:.2rem 0}.finding-card .fix{margin:.3rem 0 0}.sev{color:var(--c);font-weight:700}.trace-note.snapshot{gap:.5rem;width:28rem;display:grid}.trace-note.snapshot p{margin:0}@media (width<=1100px){.sides{grid-template-columns:1fr}.swap{justify-self:center}}.page-map{grid-template-columns:minmax(0,1fr) 20rem;align-items:start;gap:1rem;padding:1rem 1.2rem;display:grid}.page-caption{margin:0 0 .6rem}.page-frame{width:100%;position:relative}.page-frame img{border:1px solid var(--border);border-radius:var(--radius-sm);height:100%;box-shadow:var(--shadow);background:var(--surface);box-sizing:border-box;display:block;position:absolute;top:0}.callout-svg{pointer-events:none;width:100%;height:100%;position:absolute;inset:0}.callout{--pin:var(--heat)}.callout.sev-warn{--pin:var(--warn)}.callout.sev-mild{--pin:var(--accent)}.callout rect{fill:none;stroke:var(--pin);stroke-width:1px;stroke-opacity:.4;vector-effect:non-scaling-stroke}.callout line{stroke:var(--pin);stroke-width:1px;stroke-opacity:.5;vector-effect:non-scaling-stroke}.callout circle{fill:var(--pin);fill-opacity:.8}.callout.active rect{fill:var(--pin);fill-opacity:.16;stroke-opacity:1;stroke-width:2px}.callout.active line{stroke-opacity:1;stroke-width:2px}.callout.active circle{fill-opacity:1}button.marker{--pin:var(--heat);z-index:5;border:2px solid var(--panel-solid);background:var(--pin);color:#fff;border-radius:50%;place-items:center;width:1.7rem;height:1.7rem;min-height:0;padding:0;font-size:11px;font-weight:700;display:grid;position:absolute;transform:translate(-50%,-50%);box-shadow:0 1px 4px #00000059}button.marker.sev-warn{--pin:var(--warn)}button.marker.sev-mild{--pin:var(--accent-strong)}button.marker:hover:not(:disabled),button.marker.active{background:var(--pin);border-color:var(--panel-solid);box-shadow:0 0 0 3px color-mix(in srgb, var(--pin) 40%, transparent);transform:translate(-50%,-50%)scale(1.2)}.pin-num{color:#fff;background:var(--accent-strong);border-radius:999px;place-items:center;min-width:1.4rem;height:1.4rem;padding:0 .3rem;font-size:11px;font-weight:700;display:inline-grid}.pin-card{z-index:20;pointer-events:none;background:color-mix(in srgb, var(--panel-solid) 98%, transparent);border:1px solid var(--panel-border);border-radius:var(--radius-sm);width:20rem;max-width:90%;box-shadow:var(--shadow-pop);color:var(--text);margin:.5rem 0;padding:.8rem .9rem;position:absolute}.pin-card-title{align-items:center;gap:.5rem;margin-bottom:.2rem;display:flex}.pin-file{color:var(--text-muted);overflow-wrap:anywhere;margin-bottom:.5rem;display:block}.pin-stats{grid-template-columns:repeat(4,1fr);gap:.4rem;margin:0 0 .5rem;display:grid}.pin-stats dt{text-transform:uppercase;letter-spacing:.06em;color:var(--text-muted);font-size:10px}.pin-stats dd{font-variant-numeric:tabular-nums;margin:0;font-weight:650}.pin-card p{color:var(--text-2);margin:.3rem 0 0}.pin-card .pin-fix{color:var(--text)}.page-list h2{margin:0 0 .5rem;font-size:.9rem}.page-list ol{gap:.3rem;margin:0;padding:0;list-style:none;display:grid}button.hotspot{text-align:left;background:var(--surface-2);white-space:normal;justify-content:flex-start;gap:.6rem;width:100%;padding:.4rem .6rem}button.hotspot.active{border-color:var(--accent);background:var(--accent-soft)}.hotspot-main{flex:1;min-width:0;display:grid}.hotspot-main .mono{text-overflow:ellipsis;white-space:nowrap;font-size:11px;overflow:hidden}.hotspot-time{font-variant-numeric:tabular-nums;color:var(--text-2);font-weight:600}@media (width<=1100px){.page-map{grid-template-columns:1fr}}.visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.badge-sev{align-items:center;gap:.3rem;font-size:12px;font-weight:600;display:inline-flex}.culprits-hero{grid-template-columns:minmax(18rem,34rem) minmax(0,1fr);align-items:start;gap:1.4rem;display:grid}.hero-sphere h2,.hero-detail h2{margin:0 0 .3rem}.sphere{aspect-ratio:1;cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none;border-radius:50%;width:100%;max-width:34rem;margin:0 auto;position:relative}.sphere:active{cursor:grabbing}.sphere-glow{background:radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--accent) 26%, transparent), color-mix(in srgb, var(--accent) 6%, transparent) 55%, transparent 72%);box-shadow:inset 0 0 40px color-mix(in srgb, var(--accent) 18%, transparent), 0 0 60px color-mix(in srgb, var(--accent) 14%, transparent);border-radius:50%;position:absolute;inset:12%}.sphere-wire{width:100%;height:100%;position:absolute;inset:0;overflow:visible}.sphere-wire path{fill:none;stroke:var(--accent);stroke-opacity:.22;stroke-width:.006px}button.orb{--orb:var(--accent);background:color-mix(in srgb, var(--panel-solid) 78%, transparent);border:1px solid color-mix(in srgb, var(--orb) 55%, transparent);will-change:transform, opacity;border-radius:999px;justify-items:center;gap:0;min-height:0;padding:.2rem .6rem;line-height:1.2;display:grid;position:absolute;box-shadow:0 2px 10px #0000001f}button.orb.sev-critical{--orb:var(--poor)}button.orb.sev-warning{--orb:var(--warn)}button.orb .orb-name{color:var(--text);font-weight:650}button.orb .orb-ms{color:var(--orb);font-variant-numeric:tabular-nums;font-size:.78em;font-weight:600}button.orb:hover:not(:disabled),button.orb.active{background:color-mix(in srgb, var(--orb) 18%, var(--panel-solid));border-color:var(--orb);box-shadow:0 0 0 3px color-mix(in srgb, var(--orb) 30%, transparent)}.insight{border:1px solid var(--border);border-left:4px solid var(--accent);border-radius:var(--radius-sm);background:var(--surface-2);gap:.35rem;padding:.8rem 1rem;display:grid}.insight.sev-critical{border-left-color:var(--poor)}.insight.sev-warning{border-left-color:var(--warn)}.insight.sev-critical .badge-sev{color:var(--poor)}.insight.sev-warning .badge-sev{color:var(--warn)}.insight.sev-info .badge-sev{color:var(--accent)}.insight-head{flex-wrap:wrap;align-items:baseline;gap:.6rem;display:flex}.insight-name{font-family:var(--font-mono);font-size:.95rem}.insight-ms{font-variant-numeric:tabular-nums;margin-left:auto;font-weight:700}.insight p{margin:0}.insight button{justify-self:start}.rank-list{gap:2px;margin:.8rem 0 0;padding:0;list-style:none;display:grid}button.rank-row{--orb:var(--accent);border-radius:var(--radius-sm);text-align:left;background:0 0;border-color:#0000;grid-template-columns:1.4rem minmax(0,1fr) 7rem 4.6rem;align-items:center;gap:.6rem;width:100%;min-height:1.9rem;padding:.15rem .5rem;font-weight:400;display:grid}button.rank-row.sev-critical{--orb:var(--poor)}button.rank-row.sev-warning{--orb:var(--warn)}button.rank-row.active{background:var(--accent-soft);border-color:color-mix(in srgb, var(--accent) 40%, transparent)}.rank-n{color:var(--text-muted);font-variant-numeric:tabular-nums}.rank-name{font-family:var(--font-mono);text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}.rank-bar{background:var(--grid);border-radius:3px;height:6px;overflow:hidden}.rank-bar span{background:var(--orb);border-radius:3px;height:100%;display:block}.rank-ms{text-align:right;font-variant-numeric:tabular-nums;color:var(--text-2)}@media (width<=1000px){.culprits-hero{grid-template-columns:1fr}}.vitals-block{gap:.7rem;display:grid}.vitals-hero{grid-template-columns:repeat(auto-fit,minmax(13rem,1fr));gap:.7rem;display:grid}.vital-hero{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);border-left-width:4px;gap:.15rem;padding:.8rem 1rem;display:grid}.vital-hero .vital-label{color:var(--text-2)}.vital-hero .vital-value{letter-spacing:-.02em;font-variant-numeric:tabular-nums;font-size:1.9rem;font-weight:650;line-height:1.15}.vital-hero .vital-rating{font-size:12px;font-weight:600}.vital-target{font-size:11px}.rating-good{--rate:var(--good);border-left-color:var(--good)}.rating-warn{--rate:var(--warn);border-left-color:var(--warn)}.rating-poor{--rate:var(--poor);border-left-color:var(--poor)}.rating-none{--rate:var(--text-muted);border-left-color:var(--border)}.vital-rating{color:var(--rate)}.gauge{background:linear-gradient(90deg, color-mix(in srgb, var(--good) 45%, transparent) 0 var(--g), color-mix(in srgb, var(--warn) 45%, transparent) var(--g) var(--p), color-mix(in srgb, var(--poor) 45%, transparent) var(--p) 100%);border-radius:3px;height:6px;margin:.45rem 0 .2rem;position:relative}.gauge-needle{background:var(--rate);width:4px;height:12px;box-shadow:0 0 0 2px var(--panel-solid);border-radius:2px;margin-left:-2px;position:absolute;top:-3px}.vitals-secondary{grid-template-columns:repeat(auto-fit,minmax(11.5rem,1fr));gap:.4rem;display:grid}.vital-chip{border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface-2);border-left-width:1px;align-items:center;gap:.5rem;padding:.35rem .7rem;display:flex}.vital-chip-icon{width:1.1rem;height:1.1rem;color:var(--panel-solid);background:var(--rate);border-radius:50%;place-items:center;font-size:10px;font-weight:700;display:grid}.vital-chip-label{color:var(--text-2);flex:1}.vital-chip b{font-variant-numeric:tabular-nums}.findings-toolbar{flex-wrap:wrap;align-items:center;gap:.5rem;margin-bottom:.6rem;display:flex}.frows,.members{gap:.4rem;margin:0;padding:0;list-style:none;display:grid}.frow{--sev:var(--accent);border:1px solid var(--border);border-left:4px solid var(--sev);border-radius:var(--radius-sm);background:var(--surface-2);overflow:hidden}.frow.sev-critical{--sev:var(--poor)}.frow.sev-warning{--sev:var(--warn)}.frow.open{border-color:color-mix(in srgb, var(--sev) 45%, var(--border))}.frow.group{background:color-mix(in srgb, var(--sev) 6%, var(--surface-2))}button.frow-head{text-align:left;white-space:normal;background:0 0;border:0;border-radius:0;grid-template-columns:1.5rem minmax(0,1fr) auto 9.5rem 1rem;align-items:center;gap:.7rem;width:100%;padding:.55rem .8rem;font-weight:400;display:grid}button.frow-head:hover:not(:disabled){background:var(--control);border:0}.sev-badge{width:1.4rem;height:1.4rem;color:var(--panel-solid);background:var(--sev);border-radius:50%;place-items:center;font-size:11px;font-weight:700;display:grid}.frow-main{min-width:0;display:grid}.frow-title{text-overflow:ellipsis;white-space:nowrap;font-weight:600;overflow:hidden}.frow.open>.frow-head .frow-title{white-space:normal}.frow-sub{font-family:var(--font-mono);font-size:11px}.frow-chips{gap:.25rem;display:flex}.impact{grid-template-columns:1fr auto;align-items:center;gap:.5rem;display:grid}.impact-bar{background:var(--grid);border-radius:3px;height:6px;overflow:hidden}.impact-bar span{background:var(--sev);border-radius:3px;height:100%;display:block}.impact-ms{text-align:right;font-variant-numeric:tabular-nums;min-width:3.4rem;font-weight:600}.fbody{gap:.8rem;padding:.2rem 1rem 1rem 3.1rem;display:grid}.fix-callout{border-radius:var(--radius-sm);background:var(--accent-soft);border:1px solid color-mix(in srgb, var(--accent) 35%, transparent);padding:.6rem .9rem}.fix-callout p{margin:.15rem 0 0}.fix-label{text-transform:uppercase;letter-spacing:.08em;color:var(--accent);font-size:.7rem;font-weight:700}.fdetail{color:var(--text-2);margin:0}.fdetail b{text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);font-size:.7rem;display:block}.culprit-table th{background:0 0;position:static}.culprit-table td{vertical-align:middle}.members-title{text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);margin:.2rem 0 0;font-size:.7rem}.members .frow{background:var(--surface)}@media (width<=900px){button.frow-head{grid-template-columns:1.5rem minmax(0,1fr) 1rem}.frow-chips,.impact{display:none}.fbody{padding-left:1rem}}.eval-panel{padding:16px 20px 28px;overflow:auto}.eval-head{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:12px;display:flex}.eval-summary{flex-wrap:wrap;gap:6px;display:inline-flex}.eval-target{color:var(--text);margin-bottom:14px;font-size:13px}.eval-table-wrap{border:1px solid var(--border);border-radius:var(--radius-sm);overflow-x:auto}table.eval-matrix{border-collapse:collapse;width:100%;font-size:13px}table.eval-matrix th,table.eval-matrix td{text-align:left;border-bottom:1px solid var(--border);vertical-align:top;padding:9px 12px}table.eval-matrix th{font:inherit;letter-spacing:var(--label-tracking);text-transform:var(--label-case);color:var(--text-muted);background:var(--surface);font-size:11px}table.eval-matrix td.num,table.eval-matrix th.num{text-align:right;font-family:var(--font-mono);white-space:nowrap}table.eval-matrix tr:last-child td{border-bottom:none}table.eval-matrix tr.row-fail td{background:color-mix(in srgb, var(--poor) 8%, transparent)}.eval-path-title{font-weight:600}.eval-path-error{color:var(--poor);font-size:12px;font-family:var(--font-mono);margin-top:3px}.badge{letter-spacing:.04em;border-radius:999px;padding:2px 8px;font-size:11px;font-weight:700;display:inline-block}.badge-pass{background:color-mix(in srgb, var(--good) 20%, transparent);color:var(--good)}.badge-fail{background:color-mix(in srgb, var(--poor) 20%, transparent);color:var(--poor)}.top-finding{font-size:12px}.eval-foot{margin-top:12px;font-size:12px}