desic-okx-agent 0.3.2 → 0.3.3

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.
@@ -1,6 +1,7 @@
1
1
  import { activeLocale, activeMessages } from "../i18n/locale.js";
2
2
  import { MINIMUM_ANNUALIZATION_DAYS } from "../strategy/constants.js";
3
3
  import { CHART_SCRIPT } from "./chart-script.js";
4
+ import { BRAND_FONT_WOFF2 } from "./brand-font.js";
4
5
  /**
5
6
  * Renders a self-contained HTML report.
6
7
  *
@@ -16,7 +17,7 @@ export function renderReportHtml(input) {
16
17
  const { run, equity, trades } = input;
17
18
  const text = activeMessages();
18
19
  const metrics = (run.metrics ?? {});
19
- const sampled = downsample(equity, 900);
20
+ const sampled = downsample(equity, 3_000);
20
21
  const drawdown = drawdownSeries(sampled, num(metrics.initialEquityUsdt) ?? 10_000);
21
22
  return `<!DOCTYPE html>
22
23
  <html lang="${activeLang()}">
@@ -24,37 +25,47 @@ export function renderReportHtml(input) {
24
25
  <meta charset="utf-8">
25
26
  <meta name="viewport" content="width=device-width, initial-scale=1">
26
27
  <title>${escapeHtml(run.id)} · ${escapeHtml(run.strategyName)}</title>
27
- <style>${STYLES}</style>
28
+ <style>@font-face{font-family:BubbledotICG-FinePos;src:url(data:font/woff2;base64,${BRAND_FONT_WOFF2}) format("woff2");font-weight:400;font-style:normal;font-display:swap}${STYLES}</style>
28
29
  </head>
29
- <body>
30
- <main>
31
- <header>
32
- <div>
33
- <h1>${escapeHtml(run.strategyName)}</h1>
34
- <p class="sub">${escapeHtml(run.instId)} · ${escapeHtml(run.id)}</p>
35
- </div>
36
- <div class="headline ${sign(num(metrics.returnPct))}">
37
- <span class="value">${signed(num(metrics.returnPct), "%")}</span>
38
- <span class="label">${escapeHtml(text.returnLabel)}</span>
39
- </div>
40
- </header>
41
-
42
- ${renderFacts(run, metrics, text)}
30
+ <body class="single-report">
31
+ <div class="scroll-progress" aria-hidden="true"></div>
32
+ ${renderCommandRail(text)}
33
+ <main class="report">
34
+ ${renderCover(run, metrics, text)}
43
35
  ${renderWarnings(metrics, text)}
44
36
 
45
- <section>
46
- <h2>${escapeHtml(text.equityHeading)}</h2>
47
- ${renderChart(sampled, drawdown, num(metrics.initialEquityUsdt) ?? 10_000, text)}
37
+ <section class="observatory-section reveal reveal-2" id="observatory" data-report-section>
38
+ <div class="observatory-heading">
39
+ ${renderSectionHeading("01", text.observatoryHeading)}
40
+ ${renderModeControls(text)}
41
+ </div>
42
+ <div class="observatory-frame" data-mode="overview">
43
+ <div class="plot-module">
44
+ ${renderRangeControls(text)}
45
+ ${renderChart(sampled, drawdown, trades, num(metrics.initialEquityUsdt) ?? 10_000, text, equity)}
46
+ ${renderWindowMetrics(run, metrics, text)}
47
+ </div>
48
+ <aside class="telemetry-module">
49
+ ${renderExecutionOrbit(metrics, text)}
50
+ ${renderKeyMetrics(metrics, text)}
51
+ </aside>
52
+ </div>
53
+ ${renderMetricGroups(metrics, text)}
48
54
  </section>
49
55
 
50
- ${renderMetricGroups(metrics, text)}
56
+ <section class="report-section regime-section" id="regime" data-report-section>
57
+ ${renderSectionHeading("02", text.regimeHeading)}
58
+ <div class="regime-layout">
59
+ ${renderMonthly(equity, text)}
60
+ ${renderDistribution(trades, text)}
61
+ </div>
62
+ </section>
51
63
  ${renderAssumptions(run, text)}
52
- ${renderMonthly(equity, text)}
53
- ${renderDistribution(trades, text)}
54
64
  ${renderTrades(trades, text)}
55
65
 
56
- <footer>
66
+ <footer class="report-footer">
57
67
  <p>${escapeHtml(text.simulationNote)}</p>
68
+ <p class="type-credit">BubbledotICG-FinePos · Web Fonts · CC BY 4.0</p>
58
69
  <p class="mono">${escapeHtml(run.dataSnapshotId ?? "-")}</p>
59
70
  </footer>
60
71
  </main>
@@ -62,12 +73,150 @@ export function renderReportHtml(input) {
62
73
  </body>
63
74
  </html>`;
64
75
  }
76
+ function renderCover(run, metrics, text) {
77
+ const returnPct = num(metrics.returnPct);
78
+ const statusTone = run.status === "completed" ? "complete" : run.status === "failed" ? "failed" : "active";
79
+ const netProfit = num(metrics.netProfitUsdt);
80
+ return `<header class="observatory-mast">
81
+ <div class="mast-rail reveal reveal-1">
82
+ <span class="wordmark">DESIC / OBSERVATORY</span>
83
+ <span class="run-status status-${statusTone}"><i></i>${escapeHtml(run.status)}</span>
84
+ <span class="run-id mono">${escapeHtml(run.id)}</span>
85
+ <button class="theme-toggle" type="button" data-theme-toggle aria-label="${escapeHtml(text.themeLabel)}" aria-pressed="false"><span aria-hidden="true"></span></button>
86
+ </div>
87
+ <div class="mast-grid reveal reveal-1">
88
+ <div class="mast-identity">
89
+ <p class="instrument mono">${escapeHtml(run.instId)}</p>
90
+ <h1>${breakableIdentifier(run.strategyName)}</h1>
91
+ </div>
92
+ <div class="mast-outcome ${sign(returnPct)}">
93
+ <span>${escapeHtml(text.returnLabel)}</span>
94
+ <strong>${signed(returnPct, "%")}</strong>
95
+ <small>${signed(netProfit, "")} USDT</small>
96
+ </div>
97
+ ${renderFacts(run, metrics, text)}
98
+ </div>
99
+ </header>`;
100
+ }
101
+ function renderCommandRail(text) {
102
+ const items = [
103
+ ["observatory", "01", "OBS", text.observatoryHeading],
104
+ ["regime", "02", "MAP", text.regimeHeading],
105
+ ["method", "03", "MODEL", text.assumptionsHeading],
106
+ ["trades", "04", "TAPE", text.tradesHeading]
107
+ ];
108
+ return `<nav class="command-rail" aria-label="${escapeHtml(text.observatoryHeading)}">${items
109
+ .map(([id, number, short, label], index) => `<a href="#${id}" data-section-link="${id}"${index === 0 ? ' class="active"' : ""} aria-label="${escapeHtml(label)}"><span>${number}</span><b>${short}</b></a>`)
110
+ .join("")}</nav>`;
111
+ }
112
+ function renderModeControls(text) {
113
+ const modes = [
114
+ ["overview", text.modeOverview],
115
+ ["equity", text.modeEquity],
116
+ ["drawdown", text.modeDrawdown],
117
+ ["trades", text.modeTrades]
118
+ ];
119
+ return `<div class="mode-controls" role="group" aria-label="${escapeHtml(text.observatoryHeading)}">${modes
120
+ .map(([mode, label], index) => `<button type="button" data-chart-mode="${mode}" aria-pressed="${index === 0 ? "true" : "false"}">${escapeHtml(label)}</button>`)
121
+ .join("")}</div>`;
122
+ }
123
+ function renderRangeControls(text) {
124
+ const ranges = [["30", "30D"], ["90", "90D"], ["180", "180D"], ["all", text.rangeAll]];
125
+ return `<div class="range-toolbar">
126
+ <span>${escapeHtml(text.selectedWindow)}</span>
127
+ <div role="group" aria-label="${escapeHtml(text.selectedWindow)}">${ranges
128
+ .map(([days, label], index) => `<button type="button" data-range-days="${days}" aria-pressed="${index === ranges.length - 1 ? "true" : "false"}">${escapeHtml(label)}</button>`)
129
+ .join("")}</div>
130
+ </div>`;
131
+ }
132
+ function renderWindowMetrics(run, metrics, text) {
133
+ const start = num(run.assumptions?.evaluationStartMs);
134
+ const end = num(run.assumptions?.evaluationEndMs);
135
+ const period = start === null || end === null ? "-" : `${isoMinute(start)} — ${isoMinute(end)}`;
136
+ const returnPct = num(metrics.returnPct);
137
+ const drawdown = num(metrics.maxDrawdownPct);
138
+ return `<div class="window-readouts" aria-live="polite">
139
+ <div class="window-period"><span>${escapeHtml(text.selectedWindow)}</span><strong class="mono" id="window-period">${escapeHtml(period)}</strong></div>
140
+ <div><span>${escapeHtml(text.returnLabel)}</span><strong id="window-return" class="${sign(returnPct)}">${signed(returnPct, "%")}</strong></div>
141
+ <div><span>${escapeHtml(text.maxDrawdown)}</span><strong id="window-drawdown" class="down">${plain(drawdown, "%")}</strong></div>
142
+ <div><span>${escapeHtml(text.hoverEquity)}</span><strong id="window-equity">${plain(num(metrics.finalEquityUsdt), "")} USDT</strong></div>
143
+ </div>`;
144
+ }
145
+ function renderExecutionOrbit(metrics, text) {
146
+ const trades = Math.max(0, num(metrics.tradeCount) ?? 0);
147
+ const longTrades = Math.max(0, num(metrics.longTradeCount) ?? 0);
148
+ const shortTrades = Math.max(0, num(metrics.shortTradeCount) ?? 0);
149
+ const longPct = trades > 0 ? Math.min(100, Math.max(0, (longTrades / trades) * 100)) : 0;
150
+ const shortPct = trades > 0 ? Math.min(100, Math.max(0, (shortTrades / trades) * 100)) : 0;
151
+ const winPct = trades > 0 ? Math.min(100, Math.max(0, num(metrics.winRatePct) ?? 0)) : 0;
152
+ const lossPct = trades > 0 ? Math.max(0, 100 - winPct) : 0;
153
+ const wins = Math.round((trades * winPct) / 100);
154
+ const losses = Math.max(0, Math.round(trades) - wins);
155
+ const feePct = Math.max(0, num(metrics.feesPctOfGross) ?? 0);
156
+ const feeArc = Math.min(100, feePct);
157
+ const retainedPct = Math.max(0, 100 - feeArc);
158
+ const defaultValue = plain(num(metrics.profitFactor), "");
159
+ const tradeDetail = `${formatNumber(trades)} ${text.tradesLabel}`;
160
+ const segment = (key, className, radius, value, offset, label, detail, displayValue = `${value.toFixed(1)}%`) => {
161
+ const dash = `${value.toFixed(2)} ${(100 - value).toFixed(2)}`;
162
+ const dashOffset = (-offset).toFixed(2);
163
+ const aria = `${label} ${displayValue}, ${detail}`;
164
+ return `<circle class="orbit-segment ${className}" data-orbit-shape="${key}" pathLength="100" stroke-dasharray="${dash}" stroke-dashoffset="${dashOffset}" cx="140" cy="140" r="${radius}" aria-hidden="true"/>
165
+ <circle class="orbit-hit" pathLength="100" stroke-dasharray="${dash}" stroke-dashoffset="${dashOffset}" cx="140" cy="140" r="${radius}" tabindex="0" focusable="true" role="button" aria-pressed="false" aria-label="${escapeHtml(aria)}" data-orbit-key="${key}" data-orbit-label="${escapeHtml(label)}" data-orbit-value="${escapeHtml(displayValue)}" data-orbit-detail="${escapeHtml(detail)}"/>`;
166
+ };
167
+ return `<div class="execution-orbit">
168
+ <div class="orbit-heading"><span>${escapeHtml(text.executionOrbit)}</span><small>${tradeDetail}</small></div>
169
+ <div class="orbit-visual" role="group" aria-label="${escapeHtml(text.executionOrbit)}" data-default-label="${escapeHtml(text.profitFactor)}" data-default-value="${escapeHtml(defaultValue)}" data-default-detail="${escapeHtml(tradeDetail)}">
170
+ <svg viewBox="0 0 280 280" role="group" aria-label="${escapeHtml(text.executionOrbit)}">
171
+ <circle class="orbit-track" cx="140" cy="140" r="112" aria-hidden="true"/>
172
+ ${segment("short", "orbit-short", 112, shortPct, longPct, text.shortLabel, `${formatNumber(shortTrades)} / ${tradeDetail}`)}
173
+ ${segment("long", "orbit-long", 112, longPct, 0, text.longLabel, `${formatNumber(longTrades)} / ${tradeDetail}`)}
174
+ <circle class="orbit-track" cx="140" cy="140" r="84" aria-hidden="true"/>
175
+ ${segment("loss", "orbit-loss", 84, lossPct, winPct, text.lossesLabel, `${formatNumber(losses)} / ${tradeDetail}`)}
176
+ ${segment("win", "orbit-win", 84, winPct, 0, text.winsLabel, `${formatNumber(wins)} / ${tradeDetail}`)}
177
+ <circle class="orbit-track" cx="140" cy="140" r="56" aria-hidden="true"/>
178
+ ${segment("retained", "orbit-retained", 56, retainedPct, feeArc, text.retainedLabel, text.netOfFees)}
179
+ ${segment("fee", "orbit-fee", 56, feeArc, 0, text.feesLabel, `${plain(num(metrics.totalFeesUsdt), "")} USDT`, `${feePct.toFixed(1)}%`)}
180
+ </svg>
181
+ <div class="orbit-core" aria-live="polite" aria-atomic="true"><span>${escapeHtml(text.profitFactor)}</span><strong>${defaultValue}</strong><small>${escapeHtml(tradeDetail)}</small></div>
182
+ </div>
183
+ <div class="orbit-legend">
184
+ <span data-orbit-legend="long"><i class="long"></i>${escapeHtml(text.longLabel)} <b>${longPct.toFixed(1)}%</b></span>
185
+ <span data-orbit-legend="short"><i class="short"></i>${escapeHtml(text.shortLabel)} <b>${shortPct.toFixed(1)}%</b></span>
186
+ <span data-orbit-legend="win"><i class="win"></i>${escapeHtml(text.winsLabel)} <b>${winPct.toFixed(1)}%</b></span>
187
+ <span data-orbit-legend="loss"><i class="loss"></i>${escapeHtml(text.lossesLabel)} <b>${lossPct.toFixed(1)}%</b></span>
188
+ <span data-orbit-legend="fee"><i class="fee"></i>${escapeHtml(text.feesLabel)} <b>${feePct.toFixed(1)}%</b></span>
189
+ <span data-orbit-legend="retained"><i class="retained"></i>${escapeHtml(text.retainedLabel)} <b>${retainedPct.toFixed(1)}%</b></span>
190
+ </div>
191
+ </div>`;
192
+ }
193
+ function renderKeyMetrics(metrics, text) {
194
+ const maxDrawdown = num(metrics.maxDrawdownPct);
195
+ const sharpe = num(metrics.sharpe);
196
+ const profitFactor = num(metrics.profitFactor);
197
+ const winRate = num(metrics.winRatePct);
198
+ const rows = [
199
+ { name: text.hoverEquity, value: `${plain(num(metrics.finalEquityUsdt), "")} USDT`, featured: true },
200
+ {
201
+ name: text.maxDrawdown,
202
+ value: plain(maxDrawdown, "%"),
203
+ detail: `${plain(num(metrics.maxDrawdownUsdt), "")} USDT`,
204
+ tone: maxDrawdown === null ? "neutral" : "negative",
205
+ featured: true
206
+ },
207
+ { name: text.sharpeLabel, value: plain(sharpe, ""), tone: ratioMetricTone(sharpe) },
208
+ { name: text.profitFactor, value: plain(profitFactor, ""), tone: profitFactorMetricTone(profitFactor) },
209
+ { name: text.winRate, value: plain(winRate, "%"), tone: winRate === null ? "neutral" : "info" }
210
+ ];
211
+ return `<dl class="telemetry-readouts" aria-label="${escapeHtml(text.performance)}">${rows
212
+ .map((row) => renderMetric(row, "telemetry-readout"))
213
+ .join("")}</dl>`;
214
+ }
65
215
  function renderFacts(run, metrics, text) {
66
216
  const days = num(metrics.evaluationDays);
67
217
  const items = [
68
- [text.statusLabel, run.status],
69
218
  [text.barsLabel, `${formatNumber(run.barCount)} (${formatNumber(run.preloadBars)} preload)`],
70
- ["", days === null ? "-" : `${days.toFixed(1)}d`],
219
+ [text.settingDays, days === null ? "-" : `${days.toFixed(1)}d`],
71
220
  [
72
221
  text.elapsedLabel,
73
222
  run.startedAt !== null && run.finishedAt !== null
@@ -76,10 +225,12 @@ function renderFacts(run, metrics, text) {
76
225
  ]
77
226
  ];
78
227
  return `<div class="facts">${items
79
- .filter(([label]) => label !== "")
80
228
  .map(([label, value]) => `<div><span>${escapeHtml(label)}</span><strong>${escapeHtml(value)}</strong></div>`)
81
229
  .join("")}</div>`;
82
230
  }
231
+ function renderSectionHeading(index, title) {
232
+ return `<div class="section-heading"><span class="section-index mono">${escapeHtml(index)}</span><h2>${escapeHtml(title)}</h2></div>`;
233
+ }
83
234
  function renderWarnings(metrics, text) {
84
235
  const warnings = [];
85
236
  const feeShare = num(metrics.feesPctOfGross);
@@ -104,39 +255,79 @@ function renderWarnings(metrics, text) {
104
255
  * Drawdown is the question a bare equity line answers badly: two runs ending at
105
256
  * the same number can differ entirely in how much they gave back on the way.
106
257
  */
107
- function renderChart(points, drawdown, initialEquity, text) {
258
+ function renderChart(points, drawdown, trades, initialEquity, text, rawPoints) {
108
259
  if (points.length < 2)
109
260
  return `<p class="empty">${escapeHtml(text.notEnoughPoints)}</p>`;
110
261
  const width = 1000;
111
- const equityHeight = 260;
112
- const drawdownHeight = 90;
262
+ const equityHeight = 300;
263
+ const drawdownHeight = 120;
113
264
  const values = points.map((point) => point.equityUsdt);
114
265
  const minimum = Math.min(...values);
115
266
  const maximum = Math.max(...values);
116
267
  const span = maximum - minimum || 1;
117
268
  const worst = Math.max(...drawdown, 0.0001);
118
- const x = (index) => (index / (points.length - 1)) * width;
269
+ const first = points[0];
270
+ const last = points.at(-1);
271
+ const timeSpan = Math.max(1, last.timeMs - first.timeMs);
272
+ const x = (index) => ((points[index].timeMs - first.timeMs) / timeSpan) * width;
119
273
  const y = (value) => equityHeight - ((value - minimum) / span) * equityHeight;
274
+ const pointIndexAt = (timeMs) => {
275
+ let low = 0;
276
+ let high = points.length - 1;
277
+ while (low < high) {
278
+ const middle = Math.floor((low + high) / 2);
279
+ if (points[middle].timeMs < timeMs)
280
+ low = middle + 1;
281
+ else
282
+ high = middle;
283
+ }
284
+ if (low > 0 && Math.abs(points[low - 1].timeMs - timeMs) < Math.abs(points[low].timeMs - timeMs))
285
+ return low - 1;
286
+ return low;
287
+ };
120
288
  const line = values.map((value, index) => `${index === 0 ? "M" : "L"}${x(index).toFixed(1)},${y(value).toFixed(1)}`).join("");
121
289
  const area = `${line}L${width},${equityHeight}L0,${equityHeight}Z`;
122
290
  const ddPath = drawdown
123
291
  .map((value, index) => `${index === 0 ? "M" : "L"}${x(index).toFixed(1)},${((value / worst) * drawdownHeight).toFixed(1)}`)
124
292
  .join("");
125
293
  const ddArea = `${ddPath}L${width},0L0,0Z`;
126
- const first = points[0];
127
- const last = points.at(-1);
128
- // The break-even line makes "is this above water" answerable at a glance,
129
- // which a curve scaled to its own range cannot show.
294
+ const grid = [0, 1, 2, 3, 4]
295
+ .map((index) => `<line x1="0" y1="${(index * equityHeight) / 4}" x2="${width}" y2="${(index * equityHeight) / 4}"/>`)
296
+ .join("") + [0, 1, 2, 3, 4, 5, 6]
297
+ .map((index) => `<line x1="${(index * width) / 6}" y1="0" x2="${(index * width) / 6}" y2="${equityHeight}"/>`)
298
+ .join("");
299
+ const worstIndex = drawdown.indexOf(worst);
300
+ let peakIndex = 0;
301
+ for (let index = 1; index <= Math.max(0, worstIndex); index += 1) {
302
+ if (values[index] > values[peakIndex])
303
+ peakIndex = index;
304
+ }
305
+ const eventChips = [
306
+ [peakIndex, text.peakLabel, plain(values[peakIndex] ?? null, "")],
307
+ [Math.max(0, worstIndex), text.troughLabel, `-${worst.toFixed(2)}%`]
308
+ ]
309
+ .map(([index, label, value]) => `<button type="button" class="event-chip" data-point-index="${index}" style="--event-x:${((x(Number(index)) / width) * 100).toFixed(2)}%"><span>${escapeHtml(String(label))}</span><b>${escapeHtml(String(value))}</b></button>`)
310
+ .join("");
311
+ const markerStep = Math.max(1, Math.ceil(trades.length / 120));
312
+ const tradeMarkers = trades
313
+ .filter((_, index) => index % markerStep === 0)
314
+ .map((trade) => {
315
+ const closed = num(trade.closedAtMs);
316
+ const net = num(trade.netPnlUsdt ?? trade.realizedPnlUsdt);
317
+ if (closed === null || net === null)
318
+ return "";
319
+ const index = pointIndexAt(closed);
320
+ return `<ellipse class="trade-marker ${sign(net)}" cx="${x(index).toFixed(1)}" cy="${y(values[index]).toFixed(1)}" rx="3.2" ry="3.2" data-marker-radius="3.2"/>`;
321
+ })
322
+ .join("");
130
323
  const breakEven = initialEquity > minimum && initialEquity < maximum
131
324
  ? `<line class="baseline" x1="0" y1="${y(initialEquity).toFixed(1)}" x2="${width}" y2="${y(initialEquity).toFixed(1)}"/>`
132
325
  : "";
133
- // Series data for the hover readout. Sent as three parallel arrays rather than
134
- // an array of objects: on a 900-point curve that is roughly a third of the
135
- // bytes, and the report is read as one file.
136
326
  const series = {
137
327
  t: points.map((point) => point.timeMs),
138
328
  e: points.map((point) => Number(point.equityUsdt.toFixed(4))),
139
329
  d: drawdown.map((value) => Number(value.toFixed(4))),
330
+ timeline: encodeEquityTimeline(rawPoints, points.length),
140
331
  initial: initialEquity,
141
332
  labels: {
142
333
  time: text.hoverTime,
@@ -145,111 +336,172 @@ function renderChart(points, drawdown, initialEquity, text) {
145
336
  drawdown: text.hoverDrawdown
146
337
  }
147
338
  };
148
- return `
149
- <div class="chart" id="equity-chart" data-series='${escapeAttribute(JSON.stringify(series))}'>
150
- <svg viewBox="0 -8 ${width} ${equityHeight + 16}" preserveAspectRatio="none" role="img" aria-label="equity curve">
151
- <defs>
152
- <linearGradient id="fill" x1="0" y1="0" x2="0" y2="1">
153
- <stop offset="0%" stop-color="#4f9cf9" stop-opacity="0.28"/>
154
- <stop offset="100%" stop-color="#4f9cf9" stop-opacity="0"/>
155
- </linearGradient>
156
- </defs>
157
- <path d="${area}" fill="url(#fill)"/>
158
- ${breakEven}
159
- <path d="${line}" fill="none" stroke="#4f9cf9" stroke-width="1.6" vector-effect="non-scaling-stroke"/>
160
- <line class="crosshair" x1="0" y1="-8" x2="0" y2="${equityHeight}" hidden/>
161
- <circle class="marker" r="3.5" cx="0" cy="0" hidden/>
162
- </svg>
163
- <div class="axis"><span>${formatNumber(maximum)}</span><span>${formatNumber(minimum)}</span></div>
164
- <div class="tooltip" hidden></div>
165
- </div>
166
- <div class="chart drawdown" id="drawdown-chart">
167
- <svg viewBox="0 0 ${width} ${drawdownHeight}" preserveAspectRatio="none" role="img" aria-label="drawdown">
168
- <path d="${ddArea}" fill="#f2777a" fill-opacity="0.22"/>
169
- <path d="${ddPath}" fill="none" stroke="#f2777a" stroke-width="1.2" vector-effect="non-scaling-stroke"/>
170
- <line class="crosshair" x1="0" y1="0" x2="0" y2="${drawdownHeight}" hidden/>
171
- </svg>
172
- <div class="axis"><span>0%</span><span>-${worst.toFixed(1)}%</span></div>
173
- </div>
174
- <p class="range mono">${escapeHtml(isoMinute(first.timeMs))} → ${escapeHtml(isoMinute(last.timeMs))} UTC · ${formatNumber(points.length)} · ${escapeHtml(text.hoverPrompt)}</p>`;
339
+ return `<div class="chart-stack">
340
+ <div class="chart equity-track" id="equity-chart" data-series='${escapeAttribute(JSON.stringify(series))}'>
341
+ <div class="event-rail">${eventChips}</div>
342
+ <svg viewBox="0 -8 ${width} ${equityHeight + 16}" preserveAspectRatio="none" role="img" aria-label="equity curve">
343
+ <g class="plot-grid">${grid}</g>
344
+ <path class="equity-area" d="${area}"/>
345
+ ${breakEven}
346
+ <path class="equity-line" pathLength="1" d="${line}" vector-effect="non-scaling-stroke"/>
347
+ <g class="trade-markers">${tradeMarkers}</g>
348
+ <line class="crosshair" x1="0" y1="-8" x2="0" y2="${equityHeight}" hidden/>
349
+ <ellipse class="marker" rx="4" ry="4" cx="0" cy="0" data-marker-radius="4" hidden/>
350
+ </svg>
351
+ <div class="axis"><span>${formatNumber(maximum)}</span><span>${formatNumber(minimum)}</span></div>
352
+ <div class="tooltip" hidden></div>
353
+ </div>
354
+ <div class="chart drawdown-track" id="drawdown-chart">
355
+ <svg viewBox="0 0 ${width} ${drawdownHeight}" preserveAspectRatio="none" role="img" aria-label="drawdown">
356
+ <path class="drawdown-area" d="${ddArea}"/>
357
+ <path class="drawdown-line" pathLength="1" d="${ddPath}" vector-effect="non-scaling-stroke"/>
358
+ <line class="crosshair" x1="0" y1="0" x2="0" y2="${drawdownHeight}" hidden/>
359
+ </svg>
360
+ <div class="axis"><span>0%</span><span>-${worst.toFixed(1)}%</span></div>
361
+ </div>
362
+ <p class="chart-range mono">${escapeHtml(isoMinute(first.timeMs))} → ${escapeHtml(isoMinute(last.timeMs))} UTC · ${formatNumber(points.length)} · ${escapeHtml(text.hoverPrompt)}</p>
363
+ </div>`;
364
+ }
365
+ /**
366
+ * Encodes a uniform raw equity timeline without repeating flat values.
367
+ *
368
+ * The SVG only needs the sampled curve, while pointer inspection needs every bar.
369
+ * A sparse change list restores the exact value for any minute with a binary
370
+ * search and is especially compact for low-exposure strategies.
371
+ */
372
+ function encodeEquityTimeline(points, sampledCount) {
373
+ if (points.length <= sampledCount || points.length < 2)
374
+ return undefined;
375
+ const step = points[1].timeMs - points[0].timeMs;
376
+ if (step <= 0)
377
+ return undefined;
378
+ for (let index = 2; index < points.length; index += 1) {
379
+ if (points[index].timeMs !== points[0].timeMs + step * index)
380
+ return undefined;
381
+ }
382
+ const quantum = 100;
383
+ const indexDeltas = [0];
384
+ const firstEquity = Math.round(points[0].equityUsdt * quantum);
385
+ const equityDeltas = [firstEquity];
386
+ let previousIndex = 0;
387
+ let previousEquity = firstEquity;
388
+ const maximumSparseChanges = sampledCount * 4;
389
+ for (let index = 1; index < points.length; index += 1) {
390
+ if (points[index].equityUsdt === points[index - 1].equityUsdt)
391
+ continue;
392
+ const equity = Math.round(points[index].equityUsdt * quantum);
393
+ indexDeltas.push(index - previousIndex);
394
+ equityDeltas.push(equity - previousEquity);
395
+ previousIndex = index;
396
+ previousEquity = equity;
397
+ // A continuously changing high-frequency series is not sparse. In that case
398
+ // the sampled arrays remain the bounded fallback instead of doubling the file.
399
+ if (indexDeltas.length > maximumSparseChanges)
400
+ return undefined;
401
+ }
402
+ return { t: points[0].timeMs, s: step, n: points.length, q: quantum, d: indexDeltas, v: equityDeltas };
403
+ }
404
+ function renderMetric(row, extraClass = "") {
405
+ const { name, value, detail, tone = "neutral", featured = false } = row;
406
+ return `<div class="metric metric-${tone}${featured ? " featured" : ""}${extraClass ? ` ${extraClass}` : ""}"><dt>${escapeHtml(name)}</dt><dd>${escapeHtml(value)}${detail ? `<small>${escapeHtml(detail)}</small>` : ""}</dd></div>`;
175
407
  }
176
408
  function renderMetricGroups(metrics, text) {
409
+ const annualizedReturn = num(metrics.annualizedReturnPct);
410
+ const sortino = num(metrics.sortino);
411
+ const calmar = num(metrics.calmar);
412
+ const expectancy = num(metrics.expectancyUsdt);
413
+ const feesPctOfGross = num(metrics.feesPctOfGross);
177
414
  const groups = [
178
- [
179
- text.performance,
180
- [
181
- [text.netProfit, `${signed(num(metrics.netProfitUsdt), "")} USDT`],
182
- [text.returnLabel, signed(num(metrics.returnPct), "%")],
183
- [
184
- text.annualized,
185
- metrics.annualizedReturnPct === null || metrics.annualizedReturnPct === undefined
415
+ {
416
+ title: text.riskAdjusted,
417
+ className: "risk-block",
418
+ rows: [
419
+ {
420
+ name: text.annualized,
421
+ value: metrics.annualizedReturnPct === null || metrics.annualizedReturnPct === undefined
186
422
  ? "-"
187
- : signed(num(metrics.annualizedReturnPct), "%")
188
- ],
189
- [text.maxDrawdown, `${plain(num(metrics.maxDrawdownPct), "%")} (${plain(num(metrics.maxDrawdownUsdt), "")})`],
190
- [text.drawdownRecovery, `${formatNumber(num(metrics.longestDrawdownBars) ?? 0)} bars`]
191
- ]
192
- ],
193
- [
194
- text.riskAdjusted,
195
- [
196
- [text.sharpeLabel, plain(num(metrics.sharpe), "")],
197
- [text.sortinoLabel, plain(num(metrics.sortino), "")],
198
- [text.calmarLabel, plain(num(metrics.calmar), "")]
423
+ : signed(annualizedReturn, "%"),
424
+ tone: signedMetricTone(annualizedReturn)
425
+ },
426
+ { name: text.sortinoLabel, value: plain(sortino, ""), tone: ratioMetricTone(sortino) },
427
+ { name: text.calmarLabel, value: plain(calmar, ""), tone: ratioMetricTone(calmar) },
428
+ { name: text.drawdownRecovery, value: `${formatNumber(num(metrics.longestDrawdownBars) ?? 0)} bars` }
199
429
  ]
200
- ],
201
- [
202
- text.activity,
203
- [
204
- [
205
- text.tradesLabel,
206
- `${formatNumber(num(metrics.tradeCount) ?? 0)} (${text.longShort(num(metrics.longTradeCount) ?? 0, num(metrics.shortTradeCount) ?? 0)})`
207
- ],
208
- [text.winRate, plain(num(metrics.winRatePct), "%")],
209
- [text.profitFactor, plain(num(metrics.profitFactor), "")],
210
- [text.expectancy, `${signed(num(metrics.expectancyUsdt), "")} USDT`],
211
- [
212
- text.averageWinLoss,
213
- `${signed(num(metrics.averageWinUsdt), "")} / ${signed(num(metrics.averageLossUsdt), "")}`
214
- ],
215
- [
216
- text.largestWinLoss,
217
- `${signed(num(metrics.largestWinUsdt), "")} / ${signed(num(metrics.largestLossUsdt), "")}`
218
- ],
219
- [
220
- text.streaks,
221
- `${formatNumber(num(metrics.maxConsecutiveWins) ?? 0)}W / ${formatNumber(num(metrics.maxConsecutiveLosses) ?? 0)}L`
222
- ],
223
- [text.holdingTime, `${plain(num(metrics.averageHoldingBars), "")} bars`],
224
- [text.tradesPerDay, plain(num(metrics.tradesPerDay), "")]
430
+ },
431
+ {
432
+ title: text.activity,
433
+ className: "activity-block",
434
+ rows: [
435
+ {
436
+ name: text.tradesLabel,
437
+ value: `${formatNumber(num(metrics.tradeCount) ?? 0)} (${text.longShort(num(metrics.longTradeCount) ?? 0, num(metrics.shortTradeCount) ?? 0)})`
438
+ },
439
+ { name: text.expectancy, value: `${signed(expectancy, "")} USDT`, tone: signedMetricTone(expectancy) },
440
+ {
441
+ name: text.averageWinLoss,
442
+ value: `${signed(num(metrics.averageWinUsdt), "")} / ${signed(num(metrics.averageLossUsdt), "")}`
443
+ },
444
+ {
445
+ name: text.largestWinLoss,
446
+ value: `${signed(num(metrics.largestWinUsdt), "")} / ${signed(num(metrics.largestLossUsdt), "")}`
447
+ },
448
+ {
449
+ name: text.streaks,
450
+ value: `${formatNumber(num(metrics.maxConsecutiveWins) ?? 0)}W / ${formatNumber(num(metrics.maxConsecutiveLosses) ?? 0)}L`
451
+ },
452
+ { name: text.holdingTime, value: `${plain(num(metrics.averageHoldingBars), "")} bars` },
453
+ { name: text.tradesPerDay, value: plain(num(metrics.tradesPerDay), "") }
225
454
  ]
226
- ],
227
- [
228
- text.costs,
229
- [
230
- [text.feesLabel, `${plain(num(metrics.totalFeesUsdt), "")} USDT`],
231
- [text.feesOfGross, plain(num(metrics.feesPctOfGross), "%")],
232
- [text.turnover, `${plain(num(metrics.turnoverUsdt), "")} USDT`],
233
- [text.exposure, plain(num(metrics.exposurePct), "%")]
455
+ },
456
+ {
457
+ title: text.costs,
458
+ className: "cost-block",
459
+ rows: [
460
+ { name: text.feesLabel, value: `${plain(num(metrics.totalFeesUsdt), "")} USDT` },
461
+ { name: text.feesOfGross, value: plain(feesPctOfGross, "%"), tone: feeMetricTone(feesPctOfGross) },
462
+ { name: text.turnover, value: `${plain(num(metrics.turnoverUsdt), "")} USDT` },
463
+ { name: text.exposure, value: plain(num(metrics.exposurePct), "%") }
234
464
  ]
235
- ]
465
+ }
236
466
  ];
237
- return groups
238
- .map(([title, rows]) => `
239
- <section>
240
- <h2>${escapeHtml(title)}</h2>
241
- <dl>${rows
242
- .map(([name, value]) => `<div><dt>${escapeHtml(name)}</dt><dd>${escapeHtml(value)}</dd></div>`)
243
- .join("")}</dl>
244
- </section>`)
245
- .join("");
467
+ const erosion = Math.min(100, Math.max(0, feesPctOfGross ?? 0));
468
+ return `<div class="telemetry-band">${groups
469
+ .map(({ title, className, rows }) => `<article class="telemetry-channel ${className}">
470
+ <div class="channel-heading"><span></span><h3>${escapeHtml(title)}</h3></div>
471
+ <dl class="metric-list">${rows.map((row) => renderMetric(row, "detail-metric")).join("")}</dl>
472
+ ${className === "cost-block" ? `<div class="erosion-meter"><span style="--erosion:${erosion.toFixed(1)}%"></span></div>` : ""}
473
+ </article>`)
474
+ .join("")}</div>`;
475
+ }
476
+ function signedMetricTone(value) {
477
+ if (value === null || value === 0)
478
+ return "neutral";
479
+ return value > 0 ? "positive" : "negative";
480
+ }
481
+ function ratioMetricTone(value) {
482
+ if (value === null)
483
+ return "neutral";
484
+ if (value < 0)
485
+ return "negative";
486
+ if (value < 1)
487
+ return "warning";
488
+ return "positive";
489
+ }
490
+ function profitFactorMetricTone(value) {
491
+ if (value === null || value === 1)
492
+ return "neutral";
493
+ return value > 1 ? "positive" : "negative";
494
+ }
495
+ function feeMetricTone(value) {
496
+ if (value === null || value < 50)
497
+ return "neutral";
498
+ return value > 100 ? "negative" : "warning";
246
499
  }
247
500
  function renderTrades(trades, text) {
248
501
  if (trades.length === 0) {
249
- return `<section><h2>${escapeHtml(text.tradesHeading)}</h2><p class="empty">${escapeHtml(text.noClosedTrades)}</p></section>`;
502
+ return `<section class="report-section trades-section" id="trades" data-report-section>${renderSectionHeading("04", text.tradesHeading)}<p class="empty">${escapeHtml(text.noClosedTrades)}</p></section>`;
250
503
  }
251
- // Running equity per trade answers "when did this go wrong", which a list of
252
- // individual results cannot.
504
+ const maximum = Math.max(...trades.map((trade) => Math.abs(num(trade.netPnlUsdt ?? trade.realizedPnlUsdt) ?? 0)), 1);
253
505
  let cumulative = 0;
254
506
  const rows = trades
255
507
  .map((trade, index) => {
@@ -257,38 +509,39 @@ function renderTrades(trades, text) {
257
509
  cumulative += net ?? 0;
258
510
  const opened = num(trade.openedAtMs) ?? 0;
259
511
  const closed = num(trade.closedAtMs) ?? 0;
260
- return `<tr class="${sign(net)}">
261
- <td class="n dim">${formatNumber(index + 1)}</td>
262
- <td>${escapeHtml(String(trade.side ?? "-"))}</td>
263
- <td class="n">${plain(num(trade.quantity), "")}</td>
264
- <td class="n">${plain(num(trade.entryPrice), "")}</td>
265
- <td class="n">${plain(num(trade.exitPrice), "")}</td>
266
- <td class="n">${signed(net, "")}</td>
267
- <td class="n">${signed(num(trade.returnPct), "%")}</td>
268
- <td class="n dim">${signed(cumulative, "")}</td>
269
- <td class="n dim">${plain(num(trade.feesUsdt), "")}</td>
270
- <td class="n">${escapeHtml(formatHolding(num(trade.holdingBars) ?? 0))}</td>
271
- <td class="mono">${escapeHtml(isoMinute(opened))}</td>
272
- <td class="mono">${escapeHtml(closed > 0 ? isoMinute(closed) : "-")}</td>
273
- </tr>`;
512
+ const side = String(trade.side ?? "-").toLowerCase();
513
+ const outcome = net === null || net === 0 ? "flat" : net > 0 ? "wins" : "losses";
514
+ const magnitude = net === null ? 0 : Math.max(3, (Math.abs(net) / maximum) * 100);
515
+ return `<div class="tape-row ${sign(net)}" data-side="${escapeHtml(side)}" data-outcome="${outcome}">
516
+ <span class="tape-index mono">${String(index + 1).padStart(2, "0")}</span>
517
+ <div class="tape-side"><strong>${escapeHtml(side)}</strong><small>${escapeHtml(text.colQty)} ${plain(num(trade.quantity), "")}</small></div>
518
+ <div class="tape-prices"><span><small>${escapeHtml(text.colEntry)}</small>${plain(num(trade.entryPrice), "")}</span><i>→</i><span><small>${escapeHtml(text.colExit)}</small>${plain(num(trade.exitPrice), "")}</span></div>
519
+ <div class="tape-result">
520
+ <span class="pnl-bar" style="--pnl-size:${magnitude.toFixed(1)}%"></span>
521
+ <strong>${signed(net, "")} USDT</strong>
522
+ <small>${signed(num(trade.returnPct), "%")} · ${escapeHtml(text.colCumulative)} ${signed(cumulative, "")}</small>
523
+ </div>
524
+ <div class="tape-meta"><span>${escapeHtml(text.colFees)} <b>${plain(num(trade.feesUsdt), "")}</b></span><span>${escapeHtml(text.colHeld)} <b>${escapeHtml(formatHolding(num(trade.holdingBars) ?? 0))}</b></span></div>
525
+ <div class="tape-time mono"><span><small>${escapeHtml(text.colOpened)}</small>${escapeHtml(isoMinute(opened))}</span><span><small>${escapeHtml(text.colClosed)}</small>${escapeHtml(closed > 0 ? isoMinute(closed) : "-")}</span></div>
526
+ </div>`;
274
527
  })
275
528
  .join("");
276
- return `
277
- <section>
278
- <h2>${escapeHtml(text.tradesHeading)} <span class="count">${formatNumber(trades.length)}</span></h2>
279
- <div class="scroll tall">
280
- <table>
281
- <thead><tr>
282
- <th class="n">#</th><th>${escapeHtml(text.colSide)}</th><th class="n">${escapeHtml(text.colQty)}</th>
283
- <th class="n">${escapeHtml(text.colEntry)}</th><th class="n">${escapeHtml(text.colExit)}</th>
284
- <th class="n">${escapeHtml(text.colNetPnl)}</th><th class="n">${escapeHtml(text.colReturn)}</th>
285
- <th class="n">${escapeHtml(text.colCumulative)}</th><th class="n">${escapeHtml(text.colFees)}</th>
286
- <th class="n">${escapeHtml(text.colHeld)}</th>
287
- <th>${escapeHtml(text.colOpened)}</th><th>${escapeHtml(text.colClosed)}</th>
288
- </tr></thead>
289
- <tbody>${rows}</tbody>
290
- </table>
529
+ const filters = [
530
+ ["all", text.tradeFilterAll],
531
+ ["long", text.longLabel],
532
+ ["short", text.shortLabel],
533
+ ["wins", text.tradeFilterWins],
534
+ ["losses", text.tradeFilterLosses]
535
+ ];
536
+ return `<section class="report-section trades-section" id="trades" data-report-section>
537
+ <div class="tape-heading">
538
+ ${renderSectionHeading("04", text.tradesHeading)}
539
+ <div class="tape-controls" role="group" aria-label="${escapeHtml(text.tradesHeading)}">${filters
540
+ .map(([filter, label], index) => `<button type="button" data-trade-filter="${filter}" aria-pressed="${index === 0 ? "true" : "false"}">${escapeHtml(label)}</button>`)
541
+ .join("")}</div>
542
+ <output id="trade-visible-count">${formatNumber(trades.length)}</output>
291
543
  </div>
544
+ <div class="trade-tape">${rows}</div>
292
545
  </section>`;
293
546
  }
294
547
  /**
@@ -303,12 +556,22 @@ function renderAssumptions(run, text) {
303
556
  return "";
304
557
  const contract = (assumptions.contract ?? {});
305
558
  const fixedBudget = num(assumptions.entryBudgetUsdt);
559
+ const evaluationStart = num(assumptions.evaluationStartMs);
560
+ const evaluationEnd = num(assumptions.evaluationEndMs);
561
+ const intervals = Array.isArray(assumptions.intervals) ? assumptions.intervals : [];
306
562
  const rows = [
563
+ [
564
+ text.settingEvaluationWindow,
565
+ evaluationStart === null || evaluationEnd === null
566
+ ? "-"
567
+ : `${isoMinute(evaluationStart)} — ${isoMinute(evaluationEnd)}`
568
+ ],
569
+ [text.settingTimeframes, intervals.length ? intervals.join(", ") : "-"],
307
570
  [text.settingEquity, `${plain(num(assumptions.initialEquityUsdt), "")} USDT`],
308
571
  [text.settingLeverage, `${plain(num(assumptions.leverage), "")}x`],
309
572
  [text.settingMarginSafety, `${plain(num(assumptions.marginSafetyMultiplier), "")}x`],
310
573
  [
311
- text.settingBudgetPct,
574
+ fixedBudget === null ? text.settingBudgetPct : text.settingBudgetUsdt,
312
575
  fixedBudget === null
313
576
  ? asPercent(num(assumptions.entryBudgetPct))
314
577
  : `${plain(fixedBudget, "")} USDT`
@@ -320,27 +583,54 @@ function renderAssumptions(run, text) {
320
583
  [text.settingPreload, `${formatNumber(num(assumptions.preloadBars) ?? 0)}`],
321
584
  [text.settingCloseAtEnd, assumptions.closeAtEnd === false ? text.no : text.yes],
322
585
  [
323
- "contract",
324
- `${plain(num(contract.contractValue), "")} ${String(contract.contractValueCurrency ?? "")} x${plain(num(contract.contractMultiplier), "")}`
586
+ text.settingContract,
587
+ `${plain(num(contract.contractValue), "")} ${String(contract.contractValueCurrency ?? "")} × ${plain(num(contract.contractMultiplier), "")}`
325
588
  ],
326
- ["lot / min", `${plain(num(contract.lotSize), "")} / ${plain(num(contract.minSize), "")}`]
589
+ [text.settingLotAndMinimum, `${plain(num(contract.lotSize), "")} / ${plain(num(contract.minSize), "")}`]
327
590
  ];
328
591
  const params = assumptions.params;
329
- const paramRow = params && Object.keys(params).length
330
- ? `<div><dt>params</dt><dd class="mono">${escapeHtml(Object.entries(params).map(([key, value]) => `${key}=${String(value)}`).join(" "))}</dd></div>`
331
- : "";
332
- const intervals = Array.isArray(assumptions.intervals) ? assumptions.intervals : [];
333
- const intervalRow = intervals.length
334
- ? `<div><dt>timeframes</dt><dd class="mono">${escapeHtml(intervals.join(", "))}</dd></div>`
335
- : "";
336
- return `
337
- <section>
338
- <h2>${escapeHtml(text.assumptionsHeading)}</h2>
339
- <dl>${rows
592
+ const parameterEntries = Object.entries(params ?? {}).sort(([left], [right]) => left.localeCompare(right));
593
+ const parameterContent = parameterEntries.length
594
+ ? parameterEntries
595
+ .map(([key, value]) => `<div><code>${escapeHtml(key)}</code><strong>${escapeHtml(formatSettingValue(value))}</strong></div>`)
596
+ .join("")
597
+ : `<p class="empty">${escapeHtml(text.settingNoStrategyParams)}</p>`;
598
+ return `<section class="report-section method-section" id="method" data-report-section>
599
+ ${renderSectionHeading("03", text.assumptionsHeading)}
600
+ <div class="blueprint-grid">
601
+ <details class="blueprint-panel settings-panel" open>
602
+ <summary><span>01</span><strong>${escapeHtml(text.settingsTitle)}</strong><i></i></summary>
603
+ <dl class="settings-ledger">${rows
340
604
  .map(([name, value]) => `<div><dt>${escapeHtml(name)}</dt><dd>${escapeHtml(value)}</dd></div>`)
341
- .join("")}${intervalRow}${paramRow}</dl>
605
+ .join("")}</dl>
606
+ </details>
607
+ <details class="blueprint-panel parameter-panel" open>
608
+ <summary><span>02</span><strong>${escapeHtml(text.settingStrategyParams)}</strong><i></i></summary>
609
+ <div class="parameter-grid">${parameterContent}</div>
610
+ </details>
611
+ <details class="blueprint-panel provenance-panel">
612
+ <summary><span>03</span><strong>PROVENANCE</strong><i></i></summary>
613
+ <dl class="provenance-list">
614
+ <div><dt>run</dt><dd class="mono">${escapeHtml(run.id)}</dd></div>
615
+ <div><dt>strategy sha256</dt><dd class="mono break">${escapeHtml(run.strategyHash)}</dd></div>
616
+ <div><dt>data snapshot</dt><dd class="mono break">${escapeHtml(run.dataSnapshotId ?? "-")}</dd></div>
617
+ </dl>
618
+ </details>
619
+ </div>
342
620
  </section>`;
343
621
  }
622
+ function formatSettingValue(value) {
623
+ if (typeof value === "string")
624
+ return value;
625
+ if (typeof value === "number" || typeof value === "boolean" || value === null)
626
+ return String(value);
627
+ try {
628
+ return JSON.stringify(value) ?? String(value);
629
+ }
630
+ catch {
631
+ return String(value);
632
+ }
633
+ }
344
634
  function asBps(value) {
345
635
  return value === null ? "-" : `${(value * 10_000).toFixed(2)} bps`;
346
636
  }
@@ -361,38 +651,40 @@ function renderMonthly(equity, text) {
361
651
  const key = new Date(point.timeMs).toISOString().slice(0, 7);
362
652
  const found = months.get(key);
363
653
  if (!found) {
364
- months.set(key, { start: point.equityUsdt, end: point.equityUsdt, low: point.equityUsdt, high: point.equityUsdt });
654
+ months.set(key, {
655
+ start: point.equityUsdt,
656
+ end: point.equityUsdt,
657
+ low: point.equityUsdt,
658
+ high: point.equityUsdt,
659
+ startTime: point.timeMs,
660
+ endTime: point.timeMs
661
+ });
365
662
  continue;
366
663
  }
367
664
  found.end = point.equityUsdt;
368
665
  found.low = Math.min(found.low, point.equityUsdt);
369
666
  found.high = Math.max(found.high, point.equityUsdt);
667
+ found.endTime = point.timeMs;
370
668
  }
371
669
  if (months.size < 2)
372
670
  return "";
373
- const rows = [...months.entries()]
671
+ const cells = [...months.entries()]
374
672
  .map(([month, values]) => {
375
673
  const change = values.start > 0 ? (values.end / values.start - 1) * 100 : 0;
376
674
  const depth = values.high > 0 ? ((values.high - values.low) / values.high) * 100 : 0;
377
- return `<tr class="${sign(change)}">
378
- <td class="mono">${escapeHtml(month)}</td>
379
- <td class="n">${plain(values.start, "")}</td>
380
- <td class="n">${plain(values.end, "")}</td>
381
- <td class="n">${signed(change, "%")}</td>
382
- <td class="n dim">${plain(depth, "%")}</td>
383
- </tr>`;
675
+ const intensity = Math.min(1, Math.abs(change) / 5);
676
+ return `<button type="button" class="month-cell ${sign(change)}" data-month-start="${values.startTime}" data-month-end="${values.endTime}" aria-pressed="false" style="--month-fill:${(96 - intensity * 24).toFixed(1)}%">
677
+ <span class="mono">${escapeHtml(month)}</span>
678
+ <strong>${signed(change, "%")}</strong>
679
+ <small>${plain(values.start, "")} → ${plain(values.end, "")}</small>
680
+ <em>${escapeHtml(text.colWorstDrop)} ${plain(depth, "%")}</em>
681
+ </button>`;
384
682
  })
385
683
  .join("");
386
- return `
387
- <section>
388
- <h2>${escapeHtml(text.monthlyHeading)}</h2>
389
- <div class="scroll">
390
- <table>
391
- <thead><tr><th>${escapeHtml(text.colMonth)}</th><th class="n">${escapeHtml(text.colOpenEquity)}</th><th class="n">${escapeHtml(text.colCloseEquity)}</th><th class="n">${escapeHtml(text.colChange)}</th><th class="n">${escapeHtml(text.colWorstDrop)}</th></tr></thead>
392
- <tbody>${rows}</tbody>
393
- </table>
394
- </div>
395
- </section>`;
684
+ return `<div class="month-map">
685
+ <div class="map-heading"><h3>${escapeHtml(text.monthlyHeading)}</h3><span>${formatNumber(months.size)}M</span></div>
686
+ <div class="month-strip">${cells}</div>
687
+ </div>`;
396
688
  }
397
689
  /**
398
690
  * Histogram of per-trade results.
@@ -423,19 +715,21 @@ function renderDistribution(trades, text) {
423
715
  const from = minimum + index * width;
424
716
  const to = from + width;
425
717
  const share = peak > 0 ? (count / peak) * 100 : 0;
718
+ const tradeShare = (count / values.length) * 100;
426
719
  const losing = to <= 0;
427
- return `<div class="bin">
428
- <span class="range mono">${escapeHtml(compact(from))}…${escapeHtml(compact(to))}</span>
429
- <span class="track"><span class="fill ${losing ? "down" : "up"}" style="width:${share.toFixed(1)}%"></span></span>
430
- <span class="count">${formatNumber(count)}</span>
431
- </div>`;
720
+ const range = `${compact(from)}…${compact(to)} USDT`;
721
+ const detail = `${formatNumber(count)} ${text.tradesLabel} · ${tradeShare.toFixed(1)}%`;
722
+ return `<button type="button" class="spectrum-bin" aria-pressed="false" aria-label="${escapeHtml(`${range}, ${detail}`)}" data-spectrum-range="${escapeHtml(range)}" data-spectrum-detail="${escapeHtml(detail)}">
723
+ <span>${formatNumber(count)}</span>
724
+ <i class="${losing ? "down" : "up"}" style="--bin-height:${share.toFixed(1)}%"></i>
725
+ <small class="mono">${escapeHtml(compact((from + to) / 2))}</small>
726
+ </button>`;
432
727
  })
433
728
  .join("");
434
- return `
435
- <section>
436
- <h2>${escapeHtml(text.distributionHeading)}</h2>
437
- <div class="histogram">${bars}</div>
438
- </section>`;
729
+ return `<div class="distribution-map">
730
+ <div class="map-heading"><h3>${escapeHtml(text.distributionHeading)}</h3><span>${formatNumber(values.length)}T</span></div>
731
+ <div class="pnl-spectrum"><output class="spectrum-readout" aria-live="polite" aria-atomic="true" hidden><strong></strong><span></span></output>${bars}</div>
732
+ </div>`;
439
733
  }
440
734
  /** Bars as a duration, since "4210 bars" is harder to judge than "2d 22h". */
441
735
  function formatHolding(bars) {
@@ -456,6 +750,26 @@ function formatHolding(bars) {
456
750
  * extreme, which erases exactly the moves a reader is scanning for.
457
751
  */
458
752
  export function downsample(points, target) {
753
+ if (points.length <= target)
754
+ return points;
755
+ // A time-bucket envelope protects the global shape and long flat periods. Most
756
+ // points in a low-frequency strategy are flat, though, so that envelope alone
757
+ // spends almost its entire budget on empty time and reduces a held position to
758
+ // its entry/exit extremes. Reserve the rest for actual mark-to-market changes.
759
+ const backbone = extremaSample(points, Math.max(2, Math.floor(target * 0.3)));
760
+ const changing = points.filter((point, index) => {
761
+ if (index === 0 || index === points.length - 1)
762
+ return true;
763
+ return point.equityUsdt !== points[index - 1].equityUsdt || point.equityUsdt !== points[index + 1].equityUsdt;
764
+ });
765
+ const detailBudget = Math.max(2, target - backbone.length);
766
+ const detail = changing.length <= detailBudget ? changing : extremaSample(changing, detailBudget);
767
+ const merged = [...new Set([...backbone, ...detail, points[0], points.at(-1)])].sort((left, right) => left.timeMs - right.timeMs);
768
+ if (merged.length <= target)
769
+ return merged;
770
+ return [merged[0], ...extremaSample(merged.slice(1, -1), target - 2), merged.at(-1)];
771
+ }
772
+ function extremaSample(points, target) {
459
773
  if (points.length <= target)
460
774
  return points;
461
775
  const buckets = Math.max(1, Math.floor(target / 2));
@@ -544,6 +858,10 @@ export function escapeHtml(value) {
544
858
  .replace(/"/g, "&quot;")
545
859
  .replace(/'/g, "&#39;");
546
860
  }
861
+ /** Adds safe wrap opportunities to long strategy identifiers. */
862
+ function breakableIdentifier(value) {
863
+ return escapeHtml(value).replace(/_/g, "_<wbr>");
864
+ }
547
865
  /**
548
866
  * Escapes a value for a single-quoted HTML attribute.
549
867
  *
@@ -556,68 +874,330 @@ export function escapeAttribute(value) {
556
874
  return value.replace(/&/g, "&amp;").replace(/'/g, "&#39;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
557
875
  }
558
876
  const STYLES = `
559
- :root{--bg:#0f1116;--panel:#171a21;--line:#242833;--text:#e6e8ee;--dim:#8b90a0;--up:#3fb950;--down:#f2777a;--accent:#4f9cf9}
560
- @media(prefers-color-scheme:light){:root{--bg:#f7f8fa;--panel:#fff;--line:#e3e6ec;--text:#1a1d24;--dim:#6b7280;--up:#1a7f37;--down:#c9372c}}
877
+ :root{--bg:#0d1015;--panel:#141922;--panel-raised:#1a202b;--line:#28303d;--line-strong:#3a4555;--text:#edf1f7;--dim:#96a0b1;--faint:#7f8b9e;--up:#47c875;--down:#ff6b74;--accent:#5794ff;--warning:oklch(78% .13 78);--info:oklch(75% .13 252);--shadow:0 24px 70px rgba(0,0,0,.22);--ease-out:cubic-bezier(.22,1,.36,1)}
878
+ @media(prefers-color-scheme:light){:root{--bg:#f1f4f7;--panel:#fbfcfd;--panel-raised:#fff;--line:#dbe1e8;--line-strong:#bcc7d4;--text:#151a22;--dim:#626d7d;--faint:#647080;--up:#147a3d;--down:#c8323c;--accent:#155dd7;--warning:oklch(51% .13 70);--info:oklch(48% .16 255);--shadow:0 24px 70px rgba(37,49,65,.10)}}
561
879
  *{box-sizing:border-box}
562
- body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue","PingFang SC","Microsoft YaHei",sans-serif}
563
- main{max-width:1080px;margin:0 auto;padding:32px 24px 64px}
564
- header{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap;padding-bottom:20px;border-bottom:1px solid var(--line)}
565
- h1{margin:0;font-size:26px;font-weight:600;letter-spacing:-.01em}
566
- .sub{margin:4px 0 0;color:var(--dim);font-size:13px}
567
- .headline{text-align:right}
568
- .headline .value{display:block;font-size:34px;font-weight:650;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
569
- .headline .label{color:var(--dim);font-size:12px;text-transform:uppercase;letter-spacing:.08em}
880
+ [hidden]{display:none!important}
881
+ html{background:var(--bg)}
882
+ body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 "Avenir Next","Segoe UI Variable","PingFang SC","Microsoft YaHei",sans-serif;text-rendering:optimizeLegibility}
883
+ main{width:min(1240px,100%);margin:0 auto;padding:36px 36px 72px}
884
+ main>header:not(.report-cover){display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap;padding-bottom:22px;border-bottom:1px solid var(--line)}
885
+ main:not(.report) section{margin-top:44px}
886
+ main:not(.report) section>.chart{margin-top:18px;padding:14px;border:1px solid var(--line);border-radius:8px;background:var(--panel)}
887
+ h1{margin:0;font-size:28px;font-weight:650;letter-spacing:0;overflow-wrap:anywhere}
888
+ h2{margin:0 0 16px;color:var(--text);font-size:18px;font-weight:650;letter-spacing:0}
889
+ h3{margin:0;color:var(--text);font-size:13px;font-weight:650;letter-spacing:0;text-transform:uppercase}
890
+ .sub{margin:5px 0 0;color:var(--dim);font-size:13px}
570
891
  .up{color:var(--up)}.down{color:var(--down)}.flat{color:var(--dim)}
571
- .facts{display:flex;gap:28px;flex-wrap:wrap;margin:20px 0 8px;color:var(--dim);font-size:13px}
572
- .facts span{display:block;text-transform:uppercase;letter-spacing:.06em;font-size:11px}
573
- .facts strong{color:var(--text);font-weight:550;font-variant-numeric:tabular-nums}
574
- .warnings{margin:20px 0 0;padding:12px 16px;border-left:3px solid var(--down);background:color-mix(in srgb,var(--down) 8%,transparent);border-radius:0 6px 6px 0}
575
- .warnings p{margin:4px 0;font-size:13px}
576
- section{margin-top:32px}
577
- h2{margin:0 0 14px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.1em;color:var(--dim)}
578
- h2 .count{color:var(--dim);font-weight:400}
579
- dl{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:1px;margin:0;background:var(--line);border:1px solid var(--line);border-radius:8px;overflow:hidden}
580
- dl>div{background:var(--panel);padding:12px 14px}
581
- dt{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.06em}
582
- dd{margin:3px 0 0;font-size:16px;font-weight:550;font-variant-numeric:tabular-nums}
583
- .chart{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:10px 12px;display:flex;gap:10px;align-items:stretch}
584
- .chart.drawdown{margin-top:8px;border-radius:8px}
585
- .chart svg{flex:1;height:240px;width:100%;display:block}
586
- .chart.drawdown svg{height:80px}
587
- .axis{display:flex;flex-direction:column;justify-content:space-between;color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums;min-width:62px;text-align:right}
588
- .range{margin:8px 0 0;color:var(--dim);font-size:12px}
589
- .chart{position:relative}
590
- .chart svg{cursor:crosshair}
591
- .chart:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
592
- .baseline{stroke:var(--dim);stroke-width:1;stroke-dasharray:3 4;opacity:.5;vector-effect:non-scaling-stroke}
892
+ .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px}
893
+ .break{overflow-wrap:anywhere;word-break:break-word}
894
+ .report-cover{padding:0 0 34px;border-bottom:1px solid var(--line-strong)}
895
+ .cover-rail{display:grid;grid-template-columns:auto auto 1fr;align-items:center;gap:24px;padding:0 0 18px;border-bottom:1px solid var(--line);color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.08em}
896
+ .wordmark{color:var(--accent);font-weight:750}
897
+ .run-status{display:inline-flex;align-items:center;gap:7px}
898
+ .run-status i{width:7px;height:7px;border-radius:50%;background:var(--warning)}
899
+ .run-status.status-complete i{background:var(--up)}
900
+ .run-status.status-failed i{background:var(--down)}
901
+ .run-id{justify-self:end;color:var(--faint);letter-spacing:0;text-transform:none}
902
+ .cover-grid{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:56px;padding:46px 0 38px}
903
+ .instrument{margin:0 0 12px;color:var(--accent);font-weight:650;letter-spacing:.06em}
904
+ .cover-identity{min-width:0}
905
+ .cover-identity h1{max-width:820px;font-size:56px;line-height:1.02;font-weight:700}
906
+ .headline{text-align:right}
907
+ .hero-result{min-width:250px;padding-bottom:4px}
908
+ .hero-result .label{display:block;margin-bottom:8px;color:var(--dim);font-size:11px;font-weight:650;text-transform:uppercase;letter-spacing:.08em}
909
+ .hero-result .value{display:block;font-size:72px;line-height:.9;font-weight:700;font-variant-numeric:tabular-nums;white-space:nowrap}
910
+ .facts{display:flex;gap:28px;flex-wrap:wrap;margin-top:25px}
911
+ .facts span{display:block;margin-bottom:3px;color:var(--faint);font-size:10px;font-weight:650;text-transform:uppercase;letter-spacing:.08em}
912
+ .facts strong{color:var(--text);font-size:13px;font-weight:600;font-variant-numeric:tabular-nums}
913
+ .key-metrics{display:grid;grid-template-columns:1.25fr repeat(4,1fr);margin:0;padding:0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
914
+ .metric{--metric-color:var(--text);min-width:0}
915
+ .metric-positive{--metric-color:var(--up)}
916
+ .metric-negative{--metric-color:var(--down)}
917
+ .metric-warning{--metric-color:var(--warning)}
918
+ .metric-info{--metric-color:var(--info)}
919
+ .metric:not(.metric-neutral) dt::before{content:"";display:inline-block;width:6px;height:6px;margin-right:7px;border-radius:2px;background:var(--metric-color);vertical-align:1px}
920
+ .metric:not(.metric-neutral) dd{color:var(--metric-color)}
921
+ .key-metric{padding:18px 20px;border-left:1px solid var(--line)}
922
+ .key-metric:first-child{padding-left:0;border-left:0}
923
+ .key-metric:last-child{padding-right:0}
924
+ dt{color:var(--dim);font-size:10px;font-weight:650;text-transform:uppercase;letter-spacing:.07em}
925
+ dd{margin:5px 0 0;color:var(--text);font-size:18px;font-weight:650;font-variant-numeric:tabular-nums}
926
+ .key-metric.featured dd{font-size:25px}
927
+ .key-metric dd small{display:block;margin-top:2px;color:var(--dim);font-size:10px;font-weight:650}
928
+ .warnings{margin:28px 0 0;padding:15px 18px;border:1px solid color-mix(in srgb,var(--down) 34%,var(--line));border-radius:6px;background:color-mix(in srgb,var(--down) 7%,var(--panel));color:var(--text)}
929
+ .warnings p{margin:3px 0;font-size:13px}
930
+ .report-section{margin-top:80px}
931
+ .section-heading{display:grid;grid-template-columns:48px minmax(0,1fr);align-items:baseline;gap:14px;padding-top:16px;border-top:1px solid var(--line-strong)}
932
+ .section-heading h2{margin:0;font-size:22px;line-height:1.25}
933
+ .section-index{color:var(--accent);font-weight:700}
934
+ .chart-stage{margin-top:24px;padding:15px;border:1px solid var(--line);border-radius:8px;background:var(--panel);box-shadow:var(--shadow)}
935
+ .chart{position:relative;display:flex;align-items:stretch;gap:12px;padding:8px;background:transparent}
936
+ .chart.drawdown{margin-top:6px;padding-top:14px;border-top:1px solid var(--line)}
937
+ .chart svg{display:block;flex:1;width:100%;height:360px;cursor:crosshair}
938
+ .chart.drawdown svg{height:96px}
939
+ .equity-area{fill:color-mix(in srgb,var(--accent) 14%,transparent);opacity:1}
940
+ .equity-line{fill:none;stroke:var(--accent);stroke-width:1.8}
941
+ .drawdown-area{fill:color-mix(in srgb,var(--down) 19%,transparent)}
942
+ .drawdown-line{fill:none;stroke:var(--down);stroke-width:1.25}
943
+ .axis{display:flex;min-width:62px;flex-direction:column;justify-content:space-between;color:var(--faint);font-size:10px;font-variant-numeric:tabular-nums;text-align:right}
944
+ .range{margin:9px 8px 2px;color:var(--faint);font-size:11px}
945
+ .chart:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
946
+ .baseline{stroke:var(--faint);stroke-width:1;stroke-dasharray:3 5;opacity:.55;vector-effect:non-scaling-stroke}
593
947
  .crosshair{stroke:var(--dim);stroke-width:1;vector-effect:non-scaling-stroke;pointer-events:none}
594
948
  .marker{fill:var(--accent);stroke:var(--panel);stroke-width:1.5;pointer-events:none}
595
- .tooltip{position:absolute;top:10px;pointer-events:none;background:var(--panel);border:1px solid var(--line);border-radius:6px;padding:8px 10px;font-size:12px;min-width:180px;box-shadow:0 4px 16px rgba(0,0,0,.28);z-index:2}
596
- .tooltip .row{display:flex;justify-content:space-between;gap:14px;padding:1px 0}
949
+ .tooltip{position:absolute;top:10px;min-width:190px;padding:9px 11px;border:1px solid var(--line-strong);border-radius:6px;background:var(--panel-raised);box-shadow:var(--shadow);font-size:12px;pointer-events:none;z-index:2}
950
+ .tooltip .row{display:flex;justify-content:space-between;gap:16px;padding:2px 0}
597
951
  .tooltip .row span{color:var(--dim)}
598
- .tooltip .row b{font-weight:550;font-variant-numeric:tabular-nums}
599
- .scroll{overflow-x:auto;border:1px solid var(--line);border-radius:8px}
600
- table{width:100%;border-collapse:collapse;font-size:13px}
601
- th,td{padding:8px 12px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
602
- th{background:var(--panel);color:var(--dim);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.06em;position:sticky;top:0}
603
- tbody tr:last-child td{border-bottom:none}
952
+ .tooltip .row b{font-weight:650;font-variant-numeric:tabular-nums}
953
+ .analysis-grid{display:grid;grid-template-columns:.9fr 1.45fr .9fr;gap:0;margin-top:28px;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
954
+ .diagnostic-block{min-width:0;padding:24px 28px;border-left:1px solid var(--line)}
955
+ .diagnostic-block:first-child{padding-left:0;border-left:0}
956
+ .diagnostic-block:last-child{padding-right:0}
957
+ .diagnostic-block h3{margin-bottom:12px;color:var(--dim)}
958
+ .metric-list{display:grid;margin:0}
959
+ .activity-block .metric-list{grid-template-columns:repeat(2,minmax(0,1fr));column-gap:26px}
960
+ .detail-metric{display:flex;align-items:baseline;justify-content:space-between;gap:18px;padding:10px 0;border-bottom:1px solid var(--line)}
961
+ .detail-metric:nth-last-child(-n+1){border-bottom:0}
962
+ .activity-block .detail-metric:nth-last-child(-n+2){border-bottom:0}
963
+ .detail-metric dt{min-width:0}
964
+ .detail-metric dd{flex-shrink:0;margin:0;font-size:14px;text-align:right}
965
+ .method-layout{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(280px,.75fr);gap:52px;margin-top:28px}
966
+ .settings-ledger{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:36px;margin:0}
967
+ .settings-ledger>div{display:flex;align-items:baseline;justify-content:space-between;gap:20px;padding:11px 0;border-bottom:1px solid var(--line)}
968
+ .settings-ledger dd{flex-shrink:0;margin:0;font-size:14px;text-align:right}
969
+ .method-aside{min-width:0;padding-left:32px;border-left:1px solid var(--line-strong)}
970
+ .parameter-sheet h3{margin-bottom:14px;color:var(--dim)}
971
+ .parameter-grid{display:grid;gap:0}
972
+ .parameter-grid>div{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:9px 0;border-bottom:1px solid var(--line)}
973
+ .parameter-grid code{color:var(--dim);font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}
974
+ .parameter-grid strong{font-size:13px;font-weight:650;overflow-wrap:anywhere;text-align:right}
975
+ .provenance-list{display:grid;gap:12px;margin:30px 0 0;padding-top:18px;border-top:1px solid var(--line)}
976
+ .provenance-list>div{min-width:0}
977
+ .provenance-list dd{margin-top:4px;color:var(--faint);font-size:11px;font-weight:500}
978
+ .secondary-grid{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(360px,.75fr);gap:34px;align-items:start}
979
+ .secondary-grid>.report-section{margin-top:80px}
980
+ .scroll{overflow-x:auto;border:1px solid var(--line);border-radius:6px;background:var(--panel)}
981
+ table{width:100%;border-collapse:collapse;font-size:12px}
982
+ th,td{padding:10px 12px;border-bottom:1px solid var(--line);text-align:left;white-space:nowrap}
983
+ th{position:sticky;top:0;background:var(--panel-raised);color:var(--dim);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em}
984
+ tbody tr:last-child td{border-bottom:0}
985
+ tbody tr{transition:background-color 160ms ease}
986
+ tbody tr:hover{background:color-mix(in srgb,var(--accent) 5%,transparent)}
604
987
  td.n{text-align:right;font-variant-numeric:tabular-nums}
605
- tr.up td:nth-child(5),tr.up td:nth-child(6){color:var(--up)}
606
- tr.down td:nth-child(5),tr.down td:nth-child(6){color:var(--down)}
607
- .mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
988
+ tbody tr.up,tbody tr.down{color:var(--text)}
989
+ .trades-section tr.up td:nth-child(6),.trades-section tr.up td:nth-child(7){color:var(--up)}
990
+ .trades-section tr.down td:nth-child(6),.trades-section tr.down td:nth-child(7){color:var(--down)}
608
991
  td.dim{color:var(--dim)}
609
- .scroll.tall{max-height:520px;overflow-y:auto}
610
- .histogram{background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:12px 14px}
611
- .bin{display:flex;align-items:center;gap:10px;padding:3px 0}
612
- .bin .range{min-width:132px;color:var(--dim);text-align:right;flex-shrink:0}
613
- .bin .track{flex:1;height:12px;background:color-mix(in srgb,var(--line) 60%,transparent);border-radius:3px;overflow:hidden}
614
- .bin .fill{display:block;height:100%;border-radius:3px}
992
+ .scroll.tall{max-height:560px;overflow-y:auto}
993
+ .histogram{padding:15px 16px;border:1px solid var(--line);border-radius:6px;background:var(--panel)}
994
+ .bin{display:flex;align-items:center;gap:10px;padding:5px 0}
995
+ .bin .range{min-width:116px;margin:0;color:var(--dim);text-align:right;flex-shrink:0}
996
+ .bin .track{flex:1;height:8px;border-radius:2px;background:var(--line);overflow:hidden}
997
+ .bin .fill{display:block;height:100%;border-radius:2px}
615
998
  .bin .fill.up{background:var(--up)}
616
999
  .bin .fill.down{background:var(--down)}
617
- .bin>.count{min-width:44px;text-align:right;color:var(--dim);font-size:12px;font-variant-numeric:tabular-nums;flex-shrink:0}
618
- .empty{color:var(--dim);font-size:13px}
619
- footer{margin-top:40px;padding-top:16px;border-top:1px solid var(--line);color:var(--dim);font-size:12px}
620
- footer p{margin:4px 0}
1000
+ .bin>.count{min-width:28px;color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums;text-align:right;flex-shrink:0}
1001
+ .empty{margin:0;color:var(--dim);font-size:13px}
1002
+ .report-footer,footer{display:flex;justify-content:space-between;gap:24px;margin-top:72px;padding-top:18px;border-top:1px solid var(--line-strong);color:var(--faint);font-size:11px}
1003
+ footer p{margin:0}
1004
+ @media(prefers-reduced-motion:no-preference){.reveal{animation:reveal .72s var(--ease-out) both}.reveal-1{animation-delay:40ms}.reveal-2{animation-delay:110ms}.reveal-3{animation-delay:180ms}.reveal-4{animation-delay:250ms}.reveal-5{animation-delay:320ms}.equity-line,.drawdown-line{stroke-dasharray:1;stroke-dashoffset:1;animation:trace 1.1s var(--ease-out) .28s forwards}.equity-area,.drawdown-area{opacity:0;animation:plot-in .7s ease-out .55s forwards}}
1005
+ @keyframes reveal{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
1006
+ @keyframes trace{to{stroke-dashoffset:0}}
1007
+ @keyframes plot-in{to{opacity:1}}
1008
+ @media(max-width:900px){main{padding:28px 24px 60px}.cover-identity h1{font-size:44px}.hero-result .value{font-size:60px}.analysis-grid{grid-template-columns:1fr 1fr}.cost-block{grid-column:1/-1;padding-left:0;border-left:0;border-top:1px solid var(--line)}.cost-block .metric-list{grid-template-columns:repeat(4,minmax(0,1fr));column-gap:24px}.method-layout{grid-template-columns:1fr;gap:34px}.method-aside{padding:26px 0 0;border-top:1px solid var(--line-strong);border-left:0}.secondary-grid{grid-template-columns:1fr}}
1009
+ @media(max-width:640px){main{padding:22px 18px 48px}.cover-rail{grid-template-columns:auto 1fr;gap:10px 16px}.run-id{grid-column:1/-1;justify-self:start}.cover-grid{grid-template-columns:1fr;gap:28px;padding:34px 0 28px}.cover-identity h1{font-size:36px;line-height:1.08}.headline{text-align:left}.hero-result{min-width:0}.hero-result .value{font-size:54px}.facts{gap:18px}.key-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.key-metric,.key-metric:first-child,.key-metric:last-child{padding:14px 12px;border-top:1px solid var(--line);border-left:1px solid var(--line)}.key-metric:nth-child(odd){padding-left:0;border-left:0}.key-metric:nth-child(even){padding-right:0}.key-metric:nth-child(-n+2){border-top:0}.key-metric:last-child{grid-column:1/-1;padding-left:0;border-left:0}.key-metric.featured dd{font-size:21px}.report-section{margin-top:58px}.section-heading{grid-template-columns:34px 1fr;gap:10px}.section-heading h2{font-size:19px}.chart-stage{margin-top:18px;padding:7px}.chart{padding:5px;gap:7px}.chart svg{height:250px}.chart.drawdown svg{height:76px}.axis{min-width:50px}.analysis-grid{grid-template-columns:1fr}.diagnostic-block,.diagnostic-block:first-child,.diagnostic-block:last-child{padding:22px 0;border-top:1px solid var(--line);border-left:0}.diagnostic-block:first-child{border-top:0}.activity-block .metric-list,.cost-block .metric-list{grid-template-columns:repeat(2,minmax(0,1fr));column-gap:22px}.settings-ledger{grid-template-columns:1fr}.secondary-grid>.report-section{margin-top:58px}.report-footer,footer{display:block}.report-footer p+ p,footer p+ p{margin-top:7px}}
1010
+ @media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}
1011
+
1012
+ /* Trading Observatory: the single-run report is an instrument, not a document. */
1013
+ body.single-report{--bg:#020305;--panel:#080a0e;--panel-raised:#11131a;--panel-soft:#05070a;--line:rgba(174,186,210,.17);--line-strong:rgba(174,186,210,.34);--text:#f5f7fb;--dim:#aab1c0;--faint:#8a93a3;--accent:#955cff;--accent-2:#7138e8;--accent-ink:#e6d9ff;--focus:#b89aff;--up:#0ecb81;--down:#f6465d;--warning:#f5a524;--info:#9e9eff;--orbit-short:#596170;--grid:rgba(174,186,210,.05);--brand-tint:rgba(149,92,255,.14);color-scheme:dark;background-color:var(--bg);background-image:radial-gradient(58% 42% at 50% 18%,var(--brand-tint),transparent 74%),linear-gradient(var(--grid) 1px,transparent 1px),linear-gradient(90deg,var(--grid) 1px,transparent 1px);background-size:auto,88px 88px,88px 88px;min-height:100vh}
1014
+ @media(prefers-color-scheme:light){body.single-report{--bg:#f3f4f6;--panel:#fff;--panel-raised:#fafafa;--panel-soft:#f0f1f3;--line:rgba(28,29,33,.14);--line-strong:rgba(28,29,33,.31);--text:#171719;--dim:#585962;--faint:#6b6d78;--accent:#7138e8;--accent-2:#955cff;--accent-ink:#3d176f;--focus:#7138e8;--up:#087653;--down:#c92f48;--warning:#946000;--info:#5940b5;--orbit-short:#85808d;--grid:rgba(28,29,33,.045);--brand-tint:rgba(149,92,255,.055);color-scheme:light}}
1015
+ body.single-report[data-theme=dark]{--bg:#020305;--panel:#080a0e;--panel-raised:#11131a;--panel-soft:#05070a;--line:rgba(174,186,210,.17);--line-strong:rgba(174,186,210,.34);--text:#f5f7fb;--dim:#aab1c0;--faint:#8a93a3;--accent:#955cff;--accent-2:#7138e8;--accent-ink:#e6d9ff;--focus:#b89aff;--up:#0ecb81;--down:#f6465d;--warning:#f5a524;--info:#9e9eff;--orbit-short:#596170;--grid:rgba(174,186,210,.05);--brand-tint:rgba(149,92,255,.14);color-scheme:dark}
1016
+ body.single-report[data-theme=light]{--bg:#f3f4f6;--panel:#fff;--panel-raised:#fafafa;--panel-soft:#f0f1f3;--line:rgba(28,29,33,.14);--line-strong:rgba(28,29,33,.31);--text:#171719;--dim:#585962;--faint:#6b6d78;--accent:#7138e8;--accent-2:#955cff;--accent-ink:#3d176f;--focus:#7138e8;--up:#087653;--down:#c92f48;--warning:#946000;--info:#5940b5;--orbit-short:#85808d;--grid:rgba(28,29,33,.045);--brand-tint:rgba(149,92,255,.055);color-scheme:light}
1017
+ .single-report *{letter-spacing:0}
1018
+ .single-report{scroll-behavior:smooth}
1019
+ .single-report .scroll-progress{position:fixed;z-index:30;top:0;left:0;width:100%;height:3px;background:var(--accent);transform:scaleX(0);transform-origin:left center;box-shadow:0 0 18px color-mix(in srgb,var(--accent) 55%,transparent)}
1020
+ .single-report .command-rail{position:fixed;z-index:20;left:18px;top:50%;display:flex;flex-direction:column;gap:3px;padding:5px;border:0;border-radius:28px;background:#28282a;box-shadow:0 20px 60px rgba(0,0,0,.45);transform:translateY(-50%)}
1021
+ .single-report .command-rail a{display:grid;place-items:center;width:50px;height:52px;border:0;border-radius:22px;color:#a9a9ad;text-decoration:none;transition:color 180ms ease,background-color 180ms ease,transform 180ms ease}
1022
+ .single-report .command-rail a:hover,.single-report .command-rail a:focus-visible{color:#fff;background:#353538;outline:0}
1023
+ .single-report .command-rail a.active{color:#171719;background:#fff;transform:scale(.96)}
1024
+ .single-report .command-rail span{font:700 9px/1 ui-monospace,SFMono-Regular,Menlo,monospace}
1025
+ .single-report .command-rail b{margin-top:4px;font-size:9px;font-weight:750}
1026
+ .single-report main.report{width:min(1500px,calc(100% - 112px));margin:0 auto;padding:30px 34px 88px}
1027
+ .single-report .observatory-mast{padding:0;border:0}
1028
+ .single-report .mast-rail{display:grid;grid-template-columns:auto auto 1fr auto;align-items:center;gap:22px;padding-bottom:14px;border-bottom:1px solid var(--line);color:var(--dim);font-size:10px;text-transform:uppercase}
1029
+ .single-report .wordmark{color:var(--focus);font-family:BubbledotICG-FinePos,ui-monospace,SFMono-Regular,monospace;font-size:12px;font-weight:400}
1030
+ .single-report .run-status{display:inline-flex;align-items:center;gap:7px}
1031
+ .single-report .run-status i{width:6px;height:6px;border-radius:50%;background:var(--warning);box-shadow:0 0 0 4px color-mix(in srgb,var(--warning) 12%,transparent)}
1032
+ .single-report .run-status.status-complete i{background:var(--up);box-shadow:0 0 0 4px color-mix(in srgb,var(--up) 12%,transparent)}
1033
+ .single-report .run-status.status-failed i{background:var(--down);box-shadow:0 0 0 4px color-mix(in srgb,var(--down) 12%,transparent)}
1034
+ .single-report .run-id{justify-self:end;color:var(--faint);text-transform:none}
1035
+ .single-report .theme-toggle{display:grid;place-items:center;width:34px;height:34px;padding:0;border:1px solid rgba(255,255,255,.34);border-radius:50%;background:#28282a;color:#fff;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.16);transition:transform 160ms ease,box-shadow 160ms ease}
1036
+ .single-report .theme-toggle:hover{transform:rotate(14deg);box-shadow:0 0 0 1px color-mix(in srgb,var(--focus) 55%,transparent),0 0 20px color-mix(in srgb,var(--focus) 24%,transparent)}
1037
+ .single-report .theme-toggle span{width:14px;height:14px;border:1px solid currentColor;border-radius:50%;background:linear-gradient(90deg,currentColor 50%,transparent 50%)}
1038
+ .single-report .theme-toggle[aria-pressed=true] span{transform:rotate(180deg)}
1039
+ .single-report .mast-grid{display:grid;grid-template-columns:minmax(0,1fr) 270px;gap:20px 56px;align-items:end;padding:30px 0 24px}
1040
+ .single-report .mast-identity{min-width:0}
1041
+ .single-report .instrument{margin:0 0 8px;color:var(--accent);font-size:11px;font-weight:750}
1042
+ .single-report .mast-identity h1{max-width:940px;margin:0;font:400 58px/1.04 BubbledotICG-FinePos,ui-monospace,SFMono-Regular,"PingFang SC",monospace;overflow-wrap:normal;word-break:normal}
1043
+ .single-report .mast-outcome{align-self:stretch;display:flex;flex-direction:column;justify-content:flex-end;padding-left:24px;border-left:1px solid var(--line-strong);text-align:right}
1044
+ .single-report .mast-outcome span{color:var(--dim);font-size:10px;font-weight:700;text-transform:uppercase}
1045
+ .single-report .mast-outcome strong{margin-top:2px;font:800 52px/.95 "Avenir Next Condensed","Arial Narrow",sans-serif;font-variant-numeric:tabular-nums}
1046
+ .single-report .mast-outcome small{margin-top:7px;color:currentColor;font-size:12px;font-weight:700}
1047
+ .single-report .mast-grid .facts{grid-column:1/-1;display:flex;gap:0;margin:0;padding-top:17px;border-top:1px solid var(--line)}
1048
+ .single-report .facts div{min-width:150px;padding-right:32px}
1049
+ .single-report .facts span{display:block;color:var(--faint);font-size:9px;font-weight:700;text-transform:uppercase}
1050
+ .single-report .facts strong{display:block;margin-top:4px;color:var(--text);font-size:12px;font-weight:700;font-variant-numeric:tabular-nums}
1051
+ .single-report .warning{margin:0 0 20px;border:1px solid color-mix(in srgb,var(--down) 42%,var(--line));border-left:4px solid var(--down);border-radius:2px;background:color-mix(in srgb,var(--down) 7%,var(--panel));box-shadow:none}
1052
+ .single-report .observatory-section{scroll-margin-top:24px;margin-top:10px}
1053
+ .single-report .observatory-heading,.single-report .tape-heading{display:flex;align-items:end;justify-content:space-between;gap:24px;margin-bottom:14px}
1054
+ .single-report .section-heading{display:grid;grid-template-columns:44px 1fr;align-items:center;gap:12px;margin:0;padding:0;border:0}
1055
+ .single-report .section-heading .section-index{display:grid;place-items:center;width:44px;height:44px;border:1px solid var(--line-strong);color:var(--focus);font:400 14px/1 BubbledotICG-FinePos,ui-monospace,SFMono-Regular,monospace}
1056
+ .single-report .section-heading h2{margin:0;font:700 27px/1.08 Inter,"Segoe UI","PingFang SC",system-ui,sans-serif}
1057
+ .single-report .mode-controls,.single-report .range-toolbar>div,.single-report .tape-controls{display:flex;gap:2px;padding:4px;border:0;border-radius:999px;background:#fff;box-shadow:0 4px 14px rgba(0,0,0,.16)}
1058
+ .single-report button{font:inherit}
1059
+ .single-report .mode-controls button,.single-report .range-toolbar button,.single-report .tape-controls button{min-height:36px;padding:0 14px;border:0;border-radius:999px;background:transparent;color:#777;font-size:10px;font-weight:700;cursor:pointer;transition:background-color 160ms ease,color 160ms ease,transform 120ms ease}
1060
+ .single-report .mode-controls button:hover,.single-report .range-toolbar button:hover,.single-report .tape-controls button:hover{color:#111;background:#eee}
1061
+ .single-report .mode-controls button:active,.single-report .range-toolbar button:active,.single-report .tape-controls button:active{transform:scale(.97)}
1062
+ .single-report .mode-controls button[aria-pressed=true],.single-report .range-toolbar button[aria-pressed=true],.single-report .tape-controls button[aria-pressed=true]{background:#28282a;color:#fff}
1063
+ .single-report button:focus-visible,.single-report summary:focus-visible,.single-report a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
1064
+ .single-report .observatory-frame{display:grid;grid-template-columns:minmax(0,1fr) 340px;border:1px solid var(--line-strong);border-radius:4px;background:var(--panel);box-shadow:0 28px 90px rgba(0,0,0,.22);overflow:hidden}
1065
+ .single-report .plot-module{min-width:0;border-right:1px solid var(--line-strong)}
1066
+ .single-report .range-toolbar{display:flex;align-items:center;justify-content:space-between;gap:20px;min-height:54px;padding:8px 14px;border-bottom:1px solid var(--line);background:var(--panel-soft)}
1067
+ .single-report .range-toolbar>span{color:var(--faint);font-size:9px;font-weight:750;text-transform:uppercase}
1068
+ .single-report .range-toolbar button{min-height:30px;min-width:50px;padding:0 10px}
1069
+ .single-report .chart-stack{display:grid;grid-template-rows:340px 122px auto;padding:10px 12px 0;background:var(--panel);overflow:hidden}
1070
+ .single-report .chart{position:relative;display:block;min-width:0;padding:0;border:0;border-radius:0;background:transparent;overflow:hidden;user-select:none;transition:opacity 260ms var(--ease-out),transform 340ms var(--ease-out),filter 260ms ease}
1071
+ .single-report .chart svg{cursor:grab;touch-action:pan-y}.single-report .chart.is-dragging svg{cursor:grabbing}
1072
+ .single-report .chart svg{display:block;width:100%;height:100%;overflow:visible}
1073
+ .single-report .equity-track{border-bottom:1px solid var(--line)}
1074
+ .single-report .drawdown-track{transform-origin:center top}
1075
+ .single-report .plot-grid line{stroke:var(--line);stroke-width:1;vector-effect:non-scaling-stroke;opacity:.68}
1076
+ .single-report .equity-area{fill:color-mix(in srgb,var(--accent) 17%,transparent)}
1077
+ .single-report .equity-line{fill:none;stroke:var(--accent);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;animation:none;filter:drop-shadow(0 0 5px color-mix(in srgb,var(--accent) 32%,transparent))}
1078
+ .single-report .baseline{stroke:var(--dim);stroke-width:1;stroke-dasharray:5 6;opacity:.5;vector-effect:non-scaling-stroke}
1079
+ .single-report .drawdown-area{fill:color-mix(in srgb,var(--down) 20%,transparent)}
1080
+ .single-report .drawdown-line{fill:none;stroke:var(--down);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-dashoffset:0;animation:none}
1081
+ .single-report .crosshair{stroke:var(--text);stroke-width:1;stroke-dasharray:3 4;opacity:.56;vector-effect:non-scaling-stroke}
1082
+ .single-report .marker{fill:var(--panel-raised);stroke:var(--accent);stroke-width:3;vector-effect:non-scaling-stroke}
1083
+ .single-report .trade-markers{opacity:.18;transition:opacity 260ms ease}
1084
+ .single-report .trade-marker{stroke:var(--panel);stroke-width:1.5;vector-effect:non-scaling-stroke}
1085
+ .single-report .trade-marker.up{fill:var(--up)}.single-report .trade-marker.down{fill:var(--down)}
1086
+ .single-report .axis{position:absolute;z-index:2;top:10px;right:5px;bottom:10px;display:flex;flex-direction:column;justify-content:space-between;min-width:54px;color:var(--faint);font:600 9px/1 ui-monospace,SFMono-Regular,Menlo,monospace;text-align:right;pointer-events:none}
1087
+ .single-report .drawdown-track .axis{top:9px;bottom:9px}
1088
+ .single-report .event-rail{position:absolute;z-index:4;top:9px;left:9px;right:62px;height:48px;pointer-events:none}
1089
+ .single-report .event-chip{position:absolute;left:var(--event-x);display:grid;gap:1px;padding:5px 7px;border:1px solid var(--line-strong);border-radius:2px;background:color-mix(in srgb,var(--panel-raised) 90%,transparent);color:var(--text);box-shadow:0 8px 24px rgba(0,0,0,.16);transform:translateX(-50%);pointer-events:auto;cursor:pointer;white-space:nowrap}
1090
+ .single-report .event-chip::after{content:"";position:absolute;top:100%;left:50%;width:1px;height:30px;background:linear-gradient(var(--line-strong),transparent)}
1091
+ .single-report .event-chip span{color:var(--faint);font-size:8px;text-transform:uppercase}
1092
+ .single-report .event-chip b{font-size:10px;font-variant-numeric:tabular-nums}
1093
+ .single-report .tooltip{position:absolute;z-index:8;top:66px;min-width:205px;padding:11px 13px;border:1px solid var(--line-strong);border-radius:3px;background:color-mix(in srgb,var(--panel-raised) 96%,transparent);box-shadow:0 18px 48px rgba(0,0,0,.28);font-size:11px;pointer-events:none;backdrop-filter:blur(10px)}
1094
+ .single-report .tooltip .row{display:flex;justify-content:space-between;gap:22px;padding:3px 0}.single-report .tooltip span{color:var(--dim)}
1095
+ .single-report .chart-range{margin:0;padding:9px 2px 11px;color:var(--faint);font-size:9px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1096
+ .single-report .observatory-frame[data-mode=equity] .drawdown-track{opacity:.14;transform:scaleY(.72)}
1097
+ .single-report .observatory-frame[data-mode=equity] .trade-markers{opacity:0}
1098
+ .single-report .observatory-frame[data-mode=drawdown] .equity-track{opacity:.18;filter:saturate(.2)}
1099
+ .single-report .observatory-frame[data-mode=drawdown] .event-rail{opacity:.25}
1100
+ .single-report .observatory-frame[data-mode=drawdown] .drawdown-track{transform:scaleY(1.18);filter:drop-shadow(0 0 7px color-mix(in srgb,var(--down) 24%,transparent))}
1101
+ .single-report .observatory-frame[data-mode=trades] .equity-area{opacity:.28}
1102
+ .single-report .observatory-frame[data-mode=trades] .trade-markers{opacity:1}
1103
+ .single-report .observatory-frame[data-mode=trades] .trade-marker{filter:drop-shadow(0 0 4px currentColor)}
1104
+ .single-report .window-readouts{display:grid;grid-template-columns:minmax(220px,1.5fr) repeat(3,minmax(120px,1fr));border-top:1px solid var(--line-strong);background:var(--panel-soft)}
1105
+ .single-report .window-readouts>div{min-width:0;padding:13px 16px;border-left:1px solid var(--line)}
1106
+ .single-report .window-readouts>div:first-child{border-left:0}
1107
+ .single-report .window-readouts span{display:block;color:var(--faint);font-size:8px;font-weight:750;text-transform:uppercase}
1108
+ .single-report .window-readouts strong{display:block;margin-top:4px;font-size:15px;font-weight:750;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1109
+ .single-report .window-readouts .window-period strong{font-size:10px;color:var(--dim)}
1110
+ .single-report .telemetry-module{display:flex;flex-direction:column;min-width:0;background:var(--panel-soft)}
1111
+ .single-report .execution-orbit{padding:18px 20px 16px;border-bottom:1px solid var(--line-strong)}
1112
+ .single-report .orbit-heading,.single-report .map-heading{display:flex;align-items:center;justify-content:space-between;gap:16px}
1113
+ .single-report .orbit-heading span,.single-report .map-heading h3{color:var(--text);font-size:11px;font-weight:800;text-transform:uppercase}
1114
+ .single-report .orbit-heading small,.single-report .map-heading span{color:var(--faint);font:650 9px/1 ui-monospace,SFMono-Regular,Menlo,monospace}
1115
+ .single-report .orbit-visual{position:relative;width:min(250px,100%);aspect-ratio:1;margin:6px auto 0;contain:layout paint}
1116
+ .single-report .orbit-visual svg{display:block;width:100%;height:100%;transform:rotate(-90deg);overflow:visible}
1117
+ .single-report .orbit-visual circle{fill:none}
1118
+ .single-report .orbit-track{stroke:var(--line);stroke-width:11}
1119
+ .single-report .orbit-segment{stroke-width:11;stroke-linecap:butt;pointer-events:none;transition:opacity 180ms ease,stroke-width 220ms var(--ease-out),filter 220ms ease}
1120
+ .single-report .orbit-long{stroke:var(--accent)}.single-report .orbit-short{stroke:var(--orbit-short)}.single-report .orbit-win{stroke:var(--up)}.single-report .orbit-loss{stroke:var(--down)}.single-report .orbit-fee{stroke:var(--warning)}.single-report .orbit-retained{stroke:var(--line-strong)}
1121
+ .single-report .orbit-hit{stroke:transparent;stroke-width:28;pointer-events:stroke;cursor:pointer}
1122
+ .single-report .orbit-hit:focus{outline:none;stroke:color-mix(in srgb,var(--focus) 22%,transparent)}
1123
+ .single-report .orbit-visual.is-exploring [data-orbit-shape]{opacity:.18}
1124
+ .single-report .orbit-visual.is-exploring [data-orbit-shape].is-active{opacity:1;stroke-width:15;filter:drop-shadow(0 0 7px color-mix(in srgb,var(--focus) 34%,transparent))}
1125
+ .single-report .orbit-core{position:absolute;inset:50% auto auto 50%;display:grid;place-items:center;width:98px;text-align:center;pointer-events:none;transform:translate(-50%,-50%)}
1126
+ .single-report .orbit-core span{max-width:96px;color:var(--faint);font-size:8px;line-height:1.15;text-transform:uppercase}
1127
+ .single-report .orbit-core strong{margin-top:3px;font:800 28px/1 "Avenir Next Condensed","Arial Narrow",sans-serif;font-variant-numeric:tabular-nums}
1128
+ .single-report .orbit-core small{min-height:17px;margin-top:4px;color:var(--dim);font:650 7px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace}
1129
+ .single-report .orbit-legend{display:grid;grid-template-columns:1fr 1fr;gap:5px 14px}
1130
+ .single-report .orbit-legend span{display:grid;grid-template-columns:7px 1fr auto;align-items:center;gap:6px;padding:3px 4px;color:var(--dim);font-size:9px;transition:color 160ms ease,background-color 160ms ease}
1131
+ .single-report .orbit-legend span.is-active{color:var(--text);background:color-mix(in srgb,var(--focus) 9%,transparent)}
1132
+ .single-report .orbit-legend i{width:6px;height:6px;border-radius:50%;background:var(--line)}
1133
+ .single-report .orbit-legend i.long{background:var(--accent)}.single-report .orbit-legend i.short{background:var(--orbit-short)}.single-report .orbit-legend i.win{background:var(--up)}.single-report .orbit-legend i.loss{background:var(--down)}.single-report .orbit-legend i.fee{background:var(--warning)}.single-report .orbit-legend i.retained{background:var(--line-strong)}
1134
+ .single-report .orbit-legend b{color:var(--text);font-weight:700;font-variant-numeric:tabular-nums}
1135
+ .single-report .telemetry-readouts{display:grid;grid-template-columns:1fr 1fr;margin:0}
1136
+ .single-report .telemetry-readout{min-width:0;padding:13px 15px;border-right:1px solid var(--line);border-bottom:1px solid var(--line)}
1137
+ .single-report .telemetry-readout:nth-child(even){border-right:0}.single-report .telemetry-readout:last-child{grid-column:1/-1;border-bottom:0}
1138
+ .single-report .telemetry-readout dt{color:var(--faint);font-size:8px;font-weight:750;text-transform:uppercase}
1139
+ .single-report .telemetry-readout dd{margin:5px 0 0;font-size:17px;font-weight:800;font-variant-numeric:tabular-nums}
1140
+ .single-report .telemetry-readout dd small{display:block;margin-top:2px;color:var(--dim);font-size:9px}
1141
+ .single-report .telemetry-band{display:grid;grid-template-columns:1fr 1.35fr .9fr;margin-top:12px;border:1px solid var(--line-strong);background:var(--panel-soft)}
1142
+ .single-report .telemetry-channel{min-width:0;padding:18px 20px;border-left:1px solid var(--line)}
1143
+ .single-report .telemetry-channel:first-child{border-left:0}
1144
+ .single-report .channel-heading{display:flex;align-items:center;gap:9px;margin-bottom:12px}
1145
+ .single-report .channel-heading span{width:18px;height:2px;background:var(--accent)}
1146
+ .single-report .activity-block .channel-heading span{background:var(--up)}.single-report .cost-block .channel-heading span{background:var(--warning)}
1147
+ .single-report .channel-heading h3{font-size:10px}
1148
+ .single-report .metric-list{display:grid;grid-template-columns:1fr 1fr;gap:0 20px;margin:0}
1149
+ .single-report .detail-metric{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;padding:7px 0;border-bottom:1px solid var(--line)}
1150
+ .single-report .detail-metric dt{color:var(--dim);font-size:9px}.single-report .detail-metric dd{margin:0;font-size:10px;font-weight:750;text-align:right;font-variant-numeric:tabular-nums}
1151
+ .single-report .detail-metric dd small{display:block;color:var(--faint);font-size:8px}
1152
+ .single-report .erosion-meter{height:4px;margin-top:13px;background:var(--line);overflow:hidden}.single-report .erosion-meter span{display:block;width:var(--erosion);height:100%;background:var(--warning)}
1153
+ .single-report .report-section{scroll-margin-top:30px;margin-top:90px}
1154
+ .single-report .regime-layout{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(360px,.75fr);margin-top:18px;border:1px solid var(--line-strong);background:var(--panel)}
1155
+ .single-report .month-map,.single-report .distribution-map{min-width:0;padding:20px}
1156
+ .single-report .distribution-map{border-left:1px solid var(--line-strong)}
1157
+ .single-report .month-strip{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;margin-top:16px;background:var(--line)}
1158
+ .single-report .month-cell{position:relative;display:grid;grid-template-columns:1fr auto;gap:8px;padding:15px 14px;border:0;border-radius:0;background:color-mix(in srgb,var(--panel-raised) var(--month-fill),var(--up));color:var(--text);text-align:left;cursor:pointer;transition:transform 200ms var(--ease-out),filter 200ms ease,box-shadow 200ms ease}
1159
+ .single-report .month-cell.down{background:color-mix(in srgb,var(--panel-raised) var(--month-fill),var(--down));color:var(--text)}
1160
+ .single-report .month-cell:hover,.single-report .month-cell:focus-visible{z-index:2;transform:translateY(-3px);filter:saturate(1.25);box-shadow:0 14px 28px rgba(0,0,0,.2)}
1161
+ .single-report .month-cell[aria-pressed=true]{box-shadow:inset 0 0 0 2px var(--accent)}
1162
+ .single-report .month-cell>span{color:var(--dim);font-size:9px}.single-report .month-cell strong{font-size:16px;font-variant-numeric:tabular-nums;text-align:right}.single-report .month-cell small{color:var(--dim);font-size:9px}.single-report .month-cell em{color:var(--faint);font-size:8px;font-style:normal;text-align:right}
1163
+ .single-report .pnl-spectrum{position:relative;display:flex;align-items:end;gap:4px;height:252px;margin-top:16px;padding-top:44px;border-top:1px solid var(--line)}
1164
+ .single-report .spectrum-readout{position:absolute;z-index:3;top:8px;left:var(--spectrum-x,50%);display:flex;align-items:baseline;gap:8px;max-width:calc(100% - 12px);padding:5px 7px;border:1px solid var(--line-strong);border-radius:2px;background:var(--panel-raised);color:var(--text);box-shadow:0 8px 20px rgba(0,0,0,.14);font-size:8px;white-space:nowrap;transform:translateX(-50%);pointer-events:none}
1165
+ .single-report .spectrum-readout strong{font:700 8px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace}.single-report .spectrum-readout span{color:var(--dim)}
1166
+ .single-report .spectrum-bin{display:grid;grid-template-rows:18px 1fr 24px;justify-items:center;flex:1;height:100%;min-width:20px;padding:0;border:0;border-radius:0;background:transparent;color:inherit;font:inherit;cursor:pointer}
1167
+ .single-report .spectrum-bin>span{color:var(--dim);font-size:8px}.single-report .spectrum-bin i{align-self:end;width:100%;height:var(--bin-height);min-height:2px;background:var(--up);transform-origin:center bottom;transition:opacity 160ms ease,filter 160ms ease,transform 180ms var(--ease-out)}.single-report .spectrum-bin i.down{background:var(--down)}.single-report .spectrum-bin:hover i,.single-report .spectrum-bin:focus-visible i,.single-report .spectrum-bin.is-active i{filter:brightness(1.16);transform:scaleX(1.1)}.single-report .spectrum-bin:focus-visible{outline:1px solid var(--focus);outline-offset:2px}.single-report .pnl-spectrum.is-exploring .spectrum-bin i{opacity:.24}.single-report .pnl-spectrum.is-exploring .spectrum-bin.is-active i{opacity:1}.single-report .spectrum-bin small{align-self:end;color:var(--faint);font-size:7px;writing-mode:vertical-rl;transform:rotate(180deg)}
1168
+ .single-report .blueprint-grid{display:grid;grid-template-columns:1.35fr .85fr;gap:1px;margin-top:18px;border:1px solid var(--line-strong);background:var(--line)}
1169
+ .single-report .blueprint-panel{min-width:0;background:var(--panel)}
1170
+ .single-report .settings-panel{grid-row:span 2}
1171
+ .single-report .blueprint-panel summary{display:grid;grid-template-columns:32px 1fr 16px;align-items:center;gap:10px;min-height:54px;padding:8px 16px;border-bottom:1px solid var(--line);background:var(--panel-soft);cursor:pointer;list-style:none}
1172
+ .single-report .blueprint-panel summary::-webkit-details-marker{display:none}.single-report .blueprint-panel summary span{color:var(--accent);font:750 9px/1 ui-monospace,SFMono-Regular,Menlo,monospace}.single-report .blueprint-panel summary strong{font-size:10px;text-transform:uppercase}.single-report .blueprint-panel summary i{position:relative;width:12px;height:12px}.single-report .blueprint-panel summary i::before,.single-report .blueprint-panel summary i::after{content:"";position:absolute;top:5px;left:1px;width:10px;height:1px;background:var(--dim);transition:transform 180ms ease}.single-report .blueprint-panel summary i::after{transform:rotate(90deg)}.single-report .blueprint-panel[open] summary i::after{transform:rotate(0)}
1173
+ .single-report .settings-ledger{display:grid;grid-template-columns:1fr 1fr;margin:0;padding:5px 18px 18px}
1174
+ .single-report .settings-ledger>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;padding:10px 0;border-bottom:1px solid var(--line)}
1175
+ .single-report .settings-ledger>div:nth-child(odd){margin-right:18px}.single-report .settings-ledger dt{color:var(--dim);font-size:9px}.single-report .settings-ledger dd{margin:0;color:var(--text);font-size:10px;font-weight:700;text-align:right;font-variant-numeric:tabular-nums}
1176
+ .single-report .parameter-grid,.single-report .provenance-list{padding:14px 18px 18px}
1177
+ .single-report .parameter-grid>div,.single-report .provenance-list>div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;padding:9px 0;border-bottom:1px solid var(--line)}
1178
+ .single-report .parameter-grid code,.single-report .provenance-list dt{color:var(--dim);font-size:9px}.single-report .parameter-grid strong,.single-report .provenance-list dd{margin:0;color:var(--text);font-size:10px;text-align:right;overflow-wrap:anywhere}
1179
+ .single-report .parameter-grid .empty{padding:12px 0}
1180
+ .single-report .tape-heading{display:grid;grid-template-columns:minmax(240px,1fr) auto 48px;align-items:center}
1181
+ .single-report .tape-heading output{display:grid;place-items:center;width:48px;height:42px;border:1px solid var(--line-strong);color:var(--accent);font-weight:800;font-variant-numeric:tabular-nums}
1182
+ .single-report .trade-tape{border-top:1px solid var(--line-strong)}
1183
+ .single-report .tape-row{position:relative;display:grid;grid-template-columns:38px 100px 210px minmax(190px,1fr) 160px 190px;align-items:center;min-height:74px;border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--panel) 70%,transparent);transition:background-color 170ms ease,transform 170ms var(--ease-out)}
1184
+ .single-report .tape-row:hover{z-index:1;background:var(--panel-raised);transform:translateX(4px)}
1185
+ .single-report .tape-row>div,.single-report .tape-index{min-width:0;padding:11px 14px;border-left:1px solid var(--line)}
1186
+ .single-report .tape-index{border-left:0;color:var(--faint);font-size:9px;text-align:center}
1187
+ .single-report .tape-side strong{display:block;color:var(--text);font-size:11px;text-transform:uppercase}.single-report .tape-side small,.single-report .tape-result small{display:block;margin-top:3px;color:var(--faint);font-size:8px}
1188
+ .single-report .tape-prices{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:10px;font-size:10px;font-variant-numeric:tabular-nums}.single-report .tape-prices span{font-weight:700}.single-report .tape-prices small{display:block;color:var(--faint);font-size:8px}.single-report .tape-prices i{color:var(--faint);font-style:normal}
1189
+ .single-report .tape-result{position:relative;align-self:stretch;display:flex;flex-direction:column;justify-content:center;overflow:hidden}.single-report .tape-result .pnl-bar{position:absolute;left:0;bottom:0;width:var(--pnl-size);height:3px;background:currentColor;opacity:.86}.single-report .tape-result strong{font-size:15px;font-weight:800;font-variant-numeric:tabular-nums}
1190
+ .single-report .tape-meta{display:grid;gap:5px;color:var(--faint);font-size:8px}.single-report .tape-meta span{display:flex;justify-content:space-between;gap:8px}.single-report .tape-meta b{color:var(--text);font-weight:700}
1191
+ .single-report .tape-time{display:grid;gap:5px;color:var(--dim);font-size:8px}.single-report .tape-time small{display:inline-block;min-width:48px;color:var(--faint);font:700 7px/1.2 Inter,"Segoe UI","PingFang SC",system-ui,sans-serif;text-transform:uppercase}
1192
+ .single-report .report-footer{display:grid;grid-template-columns:minmax(0,1fr) auto auto;align-items:center;margin-top:90px}.single-report .type-credit{color:var(--faint);font-size:9px}
1193
+ @media(prefers-reduced-motion:no-preference){.single-report .event-chip{animation:event-in .45s var(--ease-out) both}.single-report .event-chip:nth-child(2){animation-delay:140ms}.single-report .spectrum-bin i{animation:spectrum-in .65s var(--ease-out) both}}
1194
+ @keyframes event-in{from{opacity:0;transform:translateX(-50%) translateY(-8px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
1195
+ @keyframes spectrum-in{from{transform:scaleY(0)}to{transform:scaleY(1)}}
1196
+ @media(max-width:1180px){.single-report main.report{width:calc(100% - 92px);padding-inline:24px}.single-report .observatory-frame{grid-template-columns:minmax(0,1fr) 300px}.single-report .mast-identity h1{font-size:52px}.single-report .telemetry-band{grid-template-columns:1fr 1fr}.single-report .cost-block{grid-column:1/-1;border-top:1px solid var(--line);border-left:0}.single-report .cost-block .metric-list{grid-template-columns:repeat(4,minmax(0,1fr))}.single-report .tape-row{grid-template-columns:34px 90px 190px minmax(170px,1fr) 140px 170px}}
1197
+ @media(max-width:980px){.single-report .command-rail{top:auto;bottom:12px;left:50%;flex-direction:row;transform:translateX(-50%)}.single-report .command-rail a{width:64px;height:48px;border-left:0;border-bottom:2px solid transparent}.single-report .command-rail a.active{border-left:0;border-bottom-color:var(--accent)}.single-report main.report{width:100%;padding:24px 24px 96px}.single-report .observatory-frame{grid-template-columns:1fr}.single-report .plot-module{border-right:0}.single-report .telemetry-module{display:grid;grid-template-columns:300px 1fr;border-top:1px solid var(--line-strong)}.single-report .execution-orbit{border-right:1px solid var(--line-strong);border-bottom:0}.single-report .regime-layout{grid-template-columns:1fr}.single-report .distribution-map{border-top:1px solid var(--line-strong);border-left:0}.single-report .tape-row{grid-template-columns:34px 88px minmax(180px,1fr) minmax(180px,1fr)}.single-report .tape-meta,.single-report .tape-time{grid-column:span 2;border-top:1px solid var(--line)}}
1198
+ @media(max-width:720px){body.single-report{font-size:16px}.single-report main.report{padding:18px 14px 96px}.single-report .mast-rail{grid-template-columns:auto 1fr auto;gap:10px 14px}.single-report .theme-toggle{grid-column:3;grid-row:1}.single-report .run-id{grid-column:1/-1;grid-row:2;justify-self:start}.single-report .mast-grid{grid-template-columns:1fr;gap:22px;padding:24px 0 18px}.single-report .mast-identity h1{font-size:38px;line-height:.98}.single-report .mast-outcome{align-items:flex-start;padding:16px 0 0;border-top:1px solid var(--line-strong);border-left:0;text-align:left}.single-report .mast-outcome strong{font-size:48px}.single-report .mast-grid .facts{overflow-x:auto}.single-report .facts div{min-width:128px;padding-right:18px}.single-report .observatory-heading{align-items:stretch;flex-direction:column}.single-report .mode-controls{display:grid;grid-template-columns:repeat(4,1fr)}.single-report .mode-controls button{min-height:44px;padding:0 8px}.single-report .section-heading h2{font-size:24px}.single-report .observatory-frame{border-inline:0}.single-report .range-toolbar{align-items:stretch;flex-direction:column;gap:8px}.single-report .range-toolbar>div{display:grid;grid-template-columns:repeat(4,1fr)}.single-report .range-toolbar button{min-height:44px}.single-report .chart-stack{grid-template-rows:260px 90px auto;padding-inline:7px}.single-report .event-rail{right:48px}.single-report .event-chip{padding:4px 5px}.single-report .event-chip b{display:none}.single-report .window-readouts{grid-template-columns:1fr 1fr}.single-report .window-readouts .window-period{grid-column:1/-1}.single-report .window-readouts>div:nth-child(3){border-left:0}.single-report .window-readouts strong{font-size:14px}.single-report .telemetry-module{display:block}.single-report .execution-orbit{border-right:0;border-bottom:1px solid var(--line-strong)}.single-report .orbit-visual{width:230px}.single-report .telemetry-band{grid-template-columns:1fr}.single-report .telemetry-channel,.single-report .telemetry-channel:first-child{border-top:1px solid var(--line);border-left:0}.single-report .telemetry-channel:first-child{border-top:0}.single-report .cost-block{grid-column:auto}.single-report .cost-block .metric-list{grid-template-columns:1fr 1fr}.single-report .report-section{margin-top:70px}.single-report .regime-layout{border-inline:0}.single-report .month-strip{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(155px,68vw);grid-template-columns:none;overflow-x:auto;scroll-snap-type:x mandatory}.single-report .month-cell{min-height:114px;scroll-snap-align:start}.single-report .pnl-spectrum{height:220px;overflow-x:auto}.single-report .spectrum-bin{min-width:26px}.single-report .blueprint-grid{grid-template-columns:1fr;border-inline:0}.single-report .settings-panel{grid-row:auto}.single-report .settings-ledger{grid-template-columns:1fr}.single-report .settings-ledger>div:nth-child(odd){margin-right:0}.single-report .blueprint-panel summary{min-height:56px}.single-report .tape-heading{grid-template-columns:1fr 48px}.single-report .tape-heading .section-heading{grid-column:1/-1}.single-report .tape-controls{grid-column:1/-1;overflow-x:auto}.single-report .tape-controls button{min-width:max-content;min-height:44px}.single-report .tape-row{grid-template-columns:38px 1fr auto;align-items:start}.single-report .tape-prices{grid-column:2/-1;border-top:1px solid var(--line)}.single-report .tape-result{grid-column:2/-1;min-height:62px;border-top:1px solid var(--line)}.single-report .tape-meta,.single-report .tape-time{grid-column:2/-1}.single-report .report-footer{display:block}.single-report .report-footer p+ p{margin-top:8px}.single-report .command-rail{width:calc(100% - 24px);justify-content:space-between}.single-report .command-rail a{flex:1}}
1199
+ @media(max-width:390px){.single-report .mast-identity h1{font-size:34px}.single-report .section-heading{grid-template-columns:40px 1fr}.single-report .section-heading .section-index{width:40px;height:40px}.single-report .metric-list{grid-template-columns:1fr}.single-report .cost-block .metric-list{grid-template-columns:1fr}.single-report .window-readouts>div{padding:11px 12px}.single-report .orbit-legend{grid-template-columns:1fr 1fr}.single-report .tape-row>div,.single-report .tape-index{padding:10px}}
1200
+ @media print{.single-report{background:#fff!important;color:#111}.single-report .command-rail,.single-report .scroll-progress,.single-report .mode-controls,.single-report .range-toolbar button,.single-report .tape-controls{display:none!important}.single-report main.report{width:100%;padding:0}.single-report .observatory-frame,.single-report .telemetry-band,.single-report .regime-layout,.single-report .blueprint-grid{break-inside:avoid;box-shadow:none}.single-report details>*{display:block!important}.single-report .report-section{margin-top:36px}}
621
1201
  `;
622
1202
  /**
623
1203
  * Styles for the comparison document.
@@ -626,16 +1206,14 @@ footer p{margin:4px 0}
626
1206
  * second copy: the two documents should not drift apart visually, and a duplicated
627
1207
  * sheet is exactly how that happens.
628
1208
  */
629
- export const COMPARE_STYLES = `${STYLES}
630
- .legend{display:flex;gap:20px;flex-wrap:wrap;margin:20px 0 0;font-size:13px}
631
- .legend div{display:flex;align-items:center;gap:7px}
632
- .legend i,th i{display:inline-block;width:10px;height:10px;border-radius:2px;flex-shrink:0}
633
- th i{margin-right:6px;vertical-align:middle}
634
- table.matrix th[scope=row]{background:var(--panel);color:var(--dim);text-transform:none;letter-spacing:0;font-size:12px;font-weight:500;text-align:left;position:sticky;left:0}
635
- table.matrix thead th{text-align:right}
636
- table.matrix thead th span.dim{display:block;font-weight:400;text-transform:none;letter-spacing:0;font-size:10px}
637
- td.best{color:var(--up);font-weight:600}
638
- .chart .tooltip .swatch{display:inline-block;width:8px;height:8px;border-radius:2px;margin-right:6px}
639
- .chart .tooltip .row.head{border-bottom:1px solid var(--line);margin-bottom:3px;padding-bottom:3px}
1209
+ export const COMPARE_STYLES = String.raw `
1210
+ :root{color-scheme:light;--bg:#f3f4f7;--panel:#fbfbfd;--panel-raised:#fff;--text:#17181d;--dim:#60636d;--faint:#9296a1;--line:#d8dae1;--line-strong:#b8bbc4;--accent:#6f3ff5;--focus:#8f6bff;--up:#007f64;--down:#c4384e;--warning:#9a6700;--shadow:0 20px 60px rgba(21,18,32,.12);--ease-out:cubic-bezier(.16,1,.3,1)}
1211
+ *{box-sizing:border-box}html{scroll-behavior:smooth}body.comparison-report{margin:0;min-width:320px;background-color:var(--bg);background-image:linear-gradient(var(--line) 1px,transparent 1px),linear-gradient(90deg,var(--line) 1px,transparent 1px);background-size:88px 88px;color:var(--text);font:14px/1.5 "Segoe UI","PingFang SC","Microsoft YaHei",sans-serif;letter-spacing:0}body.comparison-report[data-theme=dark]{color-scheme:dark;--bg:#020305;--panel:#080a0e;--panel-raised:#0d1016;--text:#f5f7fb;--dim:#a7aab4;--faint:#6f737f;--line:#20242d;--line-strong:#3a3f4a;--accent:#955cff;--focus:#b89aff;--up:#0ecb81;--down:#f6465d;--warning:#d6a62d;--shadow:0 24px 72px rgba(0,0,0,.46)}
1212
+ button{font:inherit;color:inherit}button:focus-visible,summary:focus-visible,.compare-chart:focus-visible{outline:2px solid var(--focus);outline-offset:3px}.mono,code{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;font-variant-numeric:tabular-nums}.positive{color:var(--up)!important}.negative{color:var(--down)!important}.neutral{color:var(--text)!important}.compare-progress{position:fixed;z-index:30;top:0;left:0;width:100%;height:3px;background:var(--accent);transform:scaleX(0);transform-origin:left center}.compare-shell{width:min(1600px,calc(100% - 112px));margin:0 auto;padding:30px 0 96px}.compare-mast{border-bottom:1px solid var(--line-strong)}.compare-rail{display:grid;grid-template-columns:auto auto minmax(0,1fr) 38px;align-items:center;gap:22px;min-height:52px;border-bottom:1px solid var(--line)}.brand-mark{color:var(--accent);font:11px/1 "SFMono-Regular",Consolas,monospace}.compare-state{display:flex;align-items:center;gap:7px;color:var(--dim);font-size:10px;text-transform:uppercase}.compare-state i{width:7px;height:7px;border-radius:50%;background:var(--up);box-shadow:0 0 0 4px color-mix(in srgb,var(--up) 12%,transparent)}.run-signature{overflow:hidden;color:var(--faint);font-size:10px;text-align:right;text-overflow:ellipsis;white-space:nowrap}.compare-theme{display:grid;place-items:center;width:34px;height:34px;border:1px solid var(--line-strong);border-radius:50%;background:var(--text);color:var(--bg);cursor:pointer}.compare-identity{display:grid;grid-template-columns:minmax(0,1fr) 260px;align-items:end;gap:48px;padding:62px 0 34px}.instrument-line{display:block;margin-bottom:18px;color:var(--accent);font-size:11px}.compare-identity h1{max-width:1100px;margin:0;font:400 clamp(58px,8vw,128px)/.78 BubbledotICG-FinePos,"Arial Black",sans-serif;text-transform:uppercase;overflow-wrap:anywhere}.compare-identity p{margin:24px 0 0;color:var(--dim);font-size:15px}.run-total{display:flex;align-items:flex-end;justify-content:flex-end;gap:16px;padding:12px 0 6px;border-left:1px solid var(--line-strong)}.run-total span{padding-bottom:13px;color:var(--dim);font-size:10px;text-transform:uppercase}.run-total strong{font:400 86px/.75 BubbledotICG-FinePos,sans-serif}.compare-facts{display:grid;grid-template-columns:2fr repeat(3,1fr);margin:0;border-top:1px solid var(--line)}.compare-facts div{min-width:0;padding:16px 22px;border-right:1px solid var(--line)}.compare-facts div:last-child{border-right:0}.compare-facts dt{color:var(--faint);font-size:9px;text-transform:uppercase}.compare-facts dd{margin:5px 0 0;overflow:hidden;font-weight:750;text-overflow:ellipsis;white-space:nowrap}.warnings{margin:28px 0 0;border:1px solid color-mix(in srgb,var(--warning) 50%,var(--line));background:color-mix(in srgb,var(--warning) 7%,var(--panel))}.warnings div{display:grid;grid-template-columns:44px minmax(0,1fr) auto;align-items:center;border-bottom:1px solid color-mix(in srgb,var(--warning) 28%,var(--line))}.warnings div:last-child{border-bottom:0}.warnings span,.warnings code{padding:13px 15px;color:var(--warning);font-size:9px}.warnings p{margin:0;padding:13px 18px;border-inline:1px solid color-mix(in srgb,var(--warning) 28%,var(--line));color:var(--text)}.compare-section{margin-top:84px}.compare-heading{display:grid;grid-template-columns:54px minmax(0,1fr);align-items:center;margin-bottom:16px}.compare-heading>span{display:grid;place-items:center;width:54px;height:48px;border:1px solid var(--line-strong);color:var(--accent);font:11px/1 "SFMono-Regular",Consolas,monospace}.compare-heading h2{margin:0 0 0 16px;font-size:32px;line-height:1;letter-spacing:0}.run-deck{display:grid;grid-template-columns:repeat(var(--run-columns,2),minmax(0,1fr));border:1px solid var(--line-strong);border-bottom:0;background:var(--panel)}.run-track{position:relative;display:grid;grid-template-columns:38px minmax(130px,1fr) auto auto;align-items:center;gap:16px;min-width:0;min-height:78px;padding:12px 18px;border:0;border-right:1px solid var(--line);border-bottom:1px solid var(--line);background:transparent;text-align:left;cursor:pointer;transition:background-color 160ms ease,opacity 160ms ease}.run-track:nth-child(2n){border-right:0}.run-track::before{position:absolute;top:0;right:0;left:0;height:3px;background:var(--series);content:""}.run-track:hover{background:var(--panel-raised)}.run-track.is-muted{opacity:.38}.run-number{color:var(--series);font:10px/1 "SFMono-Regular",Consolas,monospace}.run-name{min-width:0}.run-name strong,.run-name small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.run-name strong{font-size:13px}.run-name small{margin-top:4px;color:var(--faint);font-size:9px}.run-metric{display:grid;gap:3px;min-width:78px;text-align:right}.run-metric small{color:var(--faint);font-size:8px;text-transform:uppercase}.run-metric b{font-size:14px;font-variant-numeric:tabular-nums}.compare-chart-frame{border:1px solid var(--line-strong);background:var(--panel);box-shadow:var(--shadow)}.compare-toolbar{display:flex;align-items:center;justify-content:space-between;min-height:58px;padding:8px 12px 8px 18px;border-bottom:1px solid var(--line-strong)}.compare-toolbar>span{color:var(--dim);font-size:10px;text-transform:uppercase}.compare-toolbar>div{display:flex;padding:4px;border-radius:999px;background:var(--panel-raised);box-shadow:0 5px 18px rgba(22,19,31,.1)}.compare-toolbar button{min-width:70px;height:38px;padding:0 16px;border:0;border-radius:999px;background:transparent;color:var(--dim);font-size:11px;font-weight:750;cursor:pointer}.compare-toolbar button[aria-pressed=true]{background:var(--text);color:var(--bg)}.compare-chart{position:relative;height:430px;overflow:hidden;cursor:crosshair;touch-action:pan-y}.compare-chart.is-dragging{cursor:grabbing}.compare-chart svg{display:block;width:100%;height:100%}.compare-grid line{stroke:var(--line);stroke-width:1;vector-effect:non-scaling-stroke}.compare-baseline{stroke:var(--faint);stroke-width:1;stroke-dasharray:5 7;vector-effect:non-scaling-stroke}.compare-series{stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;filter:drop-shadow(0 0 4px color-mix(in srgb,currentColor 22%,transparent));transition:opacity 180ms ease}.compare-series.is-hidden{opacity:0}.compare-crosshair{stroke:var(--faint);stroke-width:1;stroke-dasharray:3 4;vector-effect:non-scaling-stroke}.compare-marker{fill:var(--panel-raised);stroke-width:2.5;vector-effect:non-scaling-stroke}.compare-axis{position:absolute;inset:10px 8px 12px auto;display:flex;flex-direction:column;justify-content:space-between;color:var(--dim);font:700 9px/1 "SFMono-Regular",Consolas,monospace;pointer-events:none}.compare-tooltip{position:absolute;z-index:5;top:22px;width:min(340px,calc(100% - 28px));padding:12px 14px;border:1px solid var(--line-strong);border-radius:4px;background:var(--panel-raised);box-shadow:var(--shadow);pointer-events:none}.compare-tooltip .row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;padding:5px 0;color:var(--dim);font-size:11px}.compare-tooltip .row span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.compare-tooltip .row b{color:var(--text);font-variant-numeric:tabular-nums}.compare-tooltip .row.head{margin-bottom:5px;padding-bottom:8px;border-bottom:1px solid var(--line)}.compare-tooltip .swatch{display:inline-block;width:8px;height:8px;margin-right:7px;border-radius:2px}.compare-range{margin:0;padding:12px 18px;border-top:1px solid var(--line);color:var(--faint);font-size:9px}.compare-window-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));border-top:1px solid var(--line-strong)}.compare-window-strip>div{position:relative;display:grid;grid-template-columns:1fr auto;gap:4px 16px;padding:14px 18px;border-right:1px solid var(--line);transition:opacity 160ms ease}.compare-window-strip>div::before{position:absolute;top:0;right:0;left:0;height:2px;background:var(--series);content:""}.compare-window-strip>div:last-child{border-right:0}.compare-window-strip>div.is-hidden{opacity:.35}.compare-window-strip span{overflow:hidden;color:var(--faint);font:9px/1.3 "SFMono-Regular",Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.compare-window-strip strong{grid-row:span 2;font-size:18px;font-variant-numeric:tabular-nums}.compare-window-strip small{color:var(--dim);font-size:9px}.compare-window-strip small span{margin-right:6px}.matrix-shell{overflow:auto;border:1px solid var(--line-strong);background:var(--panel);box-shadow:var(--shadow)}table.matrix{width:100%;min-width:max(780px,calc(190px + var(--run-count,2)*210px));border-collapse:separate;border-spacing:0}table.matrix th,table.matrix td{height:54px;padding:10px 16px;border-right:1px solid var(--line);border-bottom:1px solid var(--line)}table.matrix tr>*:last-child{border-right:0}table.matrix tbody tr:last-child>*{border-bottom:0}table.matrix thead th{position:sticky;top:0;z-index:2;height:70px;background:var(--panel-raised);text-align:right}table.matrix thead th:first-child{text-align:left}table.matrix thead th.n{box-shadow:inset 0 3px var(--series)}table.matrix thead i{display:inline-block;width:8px;height:8px;margin-right:8px;border-radius:2px;background:var(--series)}table.matrix thead .dim{display:block;margin-top:5px;color:var(--faint);font-size:8px;font-weight:400}table.matrix th[scope=row]{position:sticky;left:0;z-index:1;min-width:190px;background:var(--panel);color:var(--dim);font-size:11px;font-weight:550;text-align:left}table.matrix td{text-align:right;font-variant-numeric:tabular-nums}table.matrix td span{display:inline-block;min-width:74px}table.matrix td.best{position:relative;background:color-mix(in srgb,var(--up) 8%,var(--panel));color:var(--up);font-weight:800}table.matrix td.best::after{position:absolute;top:8px;right:8px;width:5px;height:5px;border-radius:50%;background:var(--up);content:""}.assumption-matrix th code{color:var(--accent);font-size:10px}.assumption-matrix td.differs{background:color-mix(in srgb,var(--accent) 5%,var(--panel))}.shared-assumptions{margin-top:12px;border:1px solid var(--line-strong);background:var(--panel)}.shared-assumptions summary{padding:16px 18px;color:var(--dim);font-size:11px;cursor:pointer}.shared-assumptions dl{display:grid;grid-template-columns:1fr 1fr;margin:0;padding:0 18px 18px}.shared-assumptions dl div{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:18px;padding:9px 0;border-bottom:1px solid var(--line)}.shared-assumptions dl div:nth-child(odd){margin-right:24px}.shared-assumptions dt{min-width:0;color:var(--dim);overflow-wrap:anywhere}.shared-assumptions dd{margin:0;color:var(--text);font-size:11px;text-align:right;overflow-wrap:anywhere}.empty{margin:0;padding:28px;border:1px solid var(--line-strong);background:var(--panel);color:var(--dim)}.compare-footer{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;gap:30px;margin-top:90px;padding-top:22px;border-top:1px solid var(--line-strong);color:var(--faint);font-size:9px}.compare-footer p{margin:0}.type-credit{text-align:right}
1213
+ @media(max-width:1100px){.compare-shell{width:calc(100% - 48px)}.compare-identity{grid-template-columns:minmax(0,1fr) 190px}.compare-identity h1{font-size:72px}.run-total strong{font-size:66px}.run-track{grid-template-columns:32px minmax(100px,1fr) auto}.run-track .run-metric:last-child{display:none}.compare-facts{grid-template-columns:2fr repeat(3,1fr)}}
1214
+ @media(max-width:760px){body.comparison-report{background-size:64px 64px}.compare-shell{width:100%;padding:18px 14px 72px}.compare-rail{grid-template-columns:auto 1fr 36px;gap:10px}.compare-state{display:none}.run-signature{grid-column:1/-1;grid-row:2;text-align:left}.compare-identity{grid-template-columns:1fr;gap:22px;padding:38px 0 24px}.compare-identity h1{font-size:clamp(48px,17vw,72px)}.run-total{justify-content:flex-start;border-top:1px solid var(--line);border-left:0}.compare-facts{grid-template-columns:1fr 1fr}.compare-facts div:nth-child(2){border-right:0}.compare-facts div:first-child{grid-column:1/-1;border-right:0}.warnings div{grid-template-columns:38px minmax(0,1fr)}.warnings code{display:none}.compare-section{margin-top:54px}.compare-heading h2{font-size:24px}.run-deck{grid-template-columns:1fr}.run-track{border-right:0}.compare-toolbar{align-items:stretch;flex-direction:column;gap:8px}.compare-toolbar>div{display:grid;grid-template-columns:repeat(4,1fr)}.compare-toolbar button{min-width:0;padding:0 8px}.compare-chart{height:330px}.compare-window-strip{grid-template-columns:1fr 1fr}.compare-window-strip>div:nth-child(2n){border-right:0}.shared-assumptions dl{grid-template-columns:1fr}.shared-assumptions dl div:nth-child(odd){margin-right:0}.compare-footer{grid-template-columns:1fr}.type-credit{text-align:left}}
1215
+ @media(max-width:420px){.compare-shell{padding-inline:10px}.brand-mark{font-size:9px}.compare-facts{grid-template-columns:1fr}.compare-facts div,.compare-facts div:nth-child(2){grid-column:auto;border-right:0}.run-track{grid-template-columns:28px minmax(0,1fr) auto;padding-inline:12px}.run-track .run-metric{min-width:68px}.compare-window-strip{grid-template-columns:1fr}.compare-window-strip>div{border-right:0}.compare-tooltip{top:10px;padding:10px}.compare-tooltip .row{font-size:9px}}
1216
+ @media(prefers-reduced-motion:no-preference){.compare-mast{animation:compare-in .65s var(--ease-out) both}.compare-section{animation:compare-in .65s var(--ease-out) both}.compare-section:nth-of-type(2){animation-delay:80ms}.compare-section:nth-of-type(3){animation-delay:150ms}@keyframes compare-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}}
1217
+ @media print{body.comparison-report{background:#fff;color:#111}.compare-progress,.compare-theme,.compare-toolbar,.run-deck{display:none!important}.compare-shell{width:100%;padding:0}.compare-chart-frame,.matrix-shell{box-shadow:none}.compare-section{margin-top:36px}.compare-footer{margin-top:40px}}
640
1218
  `;
641
1219
  //# sourceMappingURL=html.js.map