juiceb0xc0de commited on
Commit
efb2fb6
·
verified ·
1 Parent(s): e97e5b7

Land Claude Design 3D convergence valley + polished UI pass

Browse files

Replaces the SAE debug placeholder with ConvergenceValley.jsx (3D Plotly surface over training_curve metrics, switchable EV / dead% / recon / L0 via tweaks panel). Polished pass across components and styles.

app.jsx CHANGED
@@ -48,7 +48,8 @@ const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
48
  "warmth": 1.0,
49
  "showIngestCard": true,
50
  "showSQL": true,
51
- "atlasDensity": "comfortable"
 
52
  }/*EDITMODE-END*/;
53
 
54
  function applyWarmth(w) {
@@ -254,6 +255,11 @@ function App({ onShowLanding }) {
254
  </div>
255
  </div>
256
 
 
 
 
 
 
257
  <div className="section-grid">
258
  <div className="card" data-screen-label="layer-deepdive">
259
  <LayerDeepDive
@@ -271,10 +277,6 @@ function App({ onShowLanding }) {
271
  <SQLBox />
272
  </div>
273
  )}
274
-
275
- <div className="card" data-screen-label="sae-debug">
276
- <SAEDebugPanel />
277
- </div>
278
  </div>
279
 
280
  <Footer />
@@ -305,6 +307,13 @@ function App({ onShowLanding }) {
305
  value={t.showSQL}
306
  onChange={v => setTweak('showSQL', v)}
307
  />
 
 
 
 
 
 
 
308
  </TweaksPanel>
309
  </div>
310
  );
 
48
  "warmth": 1.0,
49
  "showIngestCard": true,
50
  "showSQL": true,
51
+ "atlasDensity": "comfortable",
52
+ "saeMetric": "ev"
53
  }/*EDITMODE-END*/;
54
 
55
  function applyWarmth(w) {
 
255
  </div>
256
  </div>
257
 
258
+ <ConvergenceValley
259
+ metric={t.saeMetric || 'ev'}
260
+ onMetricChange={v => setTweak('saeMetric', v)}
261
+ />
262
+
263
  <div className="section-grid">
264
  <div className="card" data-screen-label="layer-deepdive">
265
  <LayerDeepDive
 
277
  <SQLBox />
278
  </div>
279
  )}
 
 
 
 
280
  </div>
281
 
282
  <Footer />
 
307
  value={t.showSQL}
308
  onChange={v => setTweak('showSQL', v)}
309
  />
310
+ <TweakSection label="Convergence valley" />
311
+ <TweakRadio
312
+ label="Surface metric"
313
+ value={t.saeMetric || 'ev'}
314
+ options={['ev', 'dead_pct', 'recon_loss', 'mean_l0']}
315
+ onChange={v => setTweak('saeMetric', v)}
316
+ />
317
  </TweaksPanel>
318
  </div>
319
  );
components/ConvergenceValley.jsx ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Convergence Valley — a 3D pastel terrain of every SAE's training trajectory.
2
+ // x = layer · y = training progress · z = chosen metric (default: EV).
3
+ //
4
+ // Drape on top: dashed scatter3d ribbons for the *stable* L2 / L3 runs we
5
+ // chose NOT to use, so the L3 stable failure reads as a visible canyon below
6
+ // the converged rolling surface. Hover, orbit (Plotly), scrub through training
7
+ // time, swap the driving metric.
8
+
9
+ const {
10
+ useState: useStateCV,
11
+ useEffect: useEffectCV,
12
+ useRef: useRefCV,
13
+ useMemo: useMemoCV,
14
+ } = React;
15
+
16
+ const CV_METRICS = {
17
+ ev: {
18
+ label: 'EV',
19
+ sub: 'explained variance',
20
+ fmt: v => (v == null || !isFinite(v)) ? '—' : v.toFixed(3),
21
+ zRange: [0, 1.05],
22
+ rising: true,
23
+ ramp: 'dawn',
24
+ story: 'rises as the SAE learns to reconstruct the layer',
25
+ },
26
+ dead_pct: {
27
+ label: 'dead %',
28
+ sub: 'features that never fire',
29
+ fmt: v => (v == null || !isFinite(v)) ? '—' : v.toFixed(1) + '%',
30
+ zRange: null,
31
+ rising: false,
32
+ ramp: 'rose',
33
+ story: 'over-sparsification — rising while EV plateaus is bad',
34
+ },
35
+ recon_loss: {
36
+ label: 'recon loss',
37
+ sub: 'reconstruction error',
38
+ fmt: v => (v == null || !isFinite(v)) ? '—' : v.toFixed(4),
39
+ zRange: null,
40
+ rising: false,
41
+ ramp: 'sage',
42
+ story: 'reconstruction error — drops as training progresses',
43
+ },
44
+ mean_l0: {
45
+ label: 'mean L0',
46
+ sub: 'active features / token',
47
+ fmt: v => (v == null || !isFinite(v)) ? '—' : Math.round(v).toLocaleString(),
48
+ zRange: null,
49
+ rising: false,
50
+ ramp: 'lavender',
51
+ story: 'sparsity — how many features fire per token',
52
+ },
53
+ };
54
+
55
+ // Soft, watercolor-grade ramps — all land on cream so the surface feels lit.
56
+ const CV_RAMPS = {
57
+ dawn: [
58
+ [0.00, '#eee6f3'],
59
+ [0.18, '#d8c4dc'],
60
+ [0.36, '#c9a8c2'],
61
+ [0.54, '#d8a3b2'],
62
+ [0.72, '#e8aea0'],
63
+ [0.88, '#f0c890'],
64
+ [1.00, '#f4dfa0'],
65
+ ],
66
+ rose: [
67
+ [0.00, '#f6ecdf'],
68
+ [0.22, '#f5d6c6'],
69
+ [0.44, '#eeb6b9'],
70
+ [0.66, '#dba8b0'],
71
+ [0.85, '#cf8d96'],
72
+ [1.00, '#b87280'],
73
+ ],
74
+ sage: [
75
+ [0.00, '#f2eedb'],
76
+ [0.22, '#dde2c2'],
77
+ [0.44, '#bcd4ad'],
78
+ [0.66, '#9cc59a'],
79
+ [0.85, '#7da272'],
80
+ [1.00, '#5b8460'],
81
+ ],
82
+ lavender: [
83
+ [0.00, '#f1eaf4'],
84
+ [0.22, '#dcc8e4'],
85
+ [0.44, '#c4a8d8'],
86
+ [0.66, '#aa90c8'],
87
+ [0.85, '#9a87cf'],
88
+ [1.00, '#7d6cb0'],
89
+ ],
90
+ };
91
+
92
+ const CV_LAYER_LABELS = ['L0','L1','L2','L3','L4','L5','L6','L7','L8'];
93
+ const CV_N_PROG = 56; // resampled training-progress steps per layer
94
+ const CV_HOST = 'https://huggingface.co/datasets';
95
+ const CV_GHOST_REPO = 'juiceb0xc0de/gemma-4-e2b-saes'; // the stable run (rolling lives elsewhere)
96
+ const CV_GHOST_LAYERS = [2, 3]; // these are the swapped-out ones
97
+
98
+ // Smooth, monotonic resample of a per-checkpoint curve onto a uniform grid.
99
+ function cvResample(arr, N) {
100
+ if (!arr || arr.length === 0) return null;
101
+ if (arr.length === 1) return Array(N).fill(arr[0]);
102
+ const out = new Array(N);
103
+ for (let i = 0; i < N; i++) {
104
+ const t = i / (N - 1);
105
+ const pos = t * (arr.length - 1);
106
+ const a = Math.floor(pos);
107
+ const b = Math.min(arr.length - 1, a + 1);
108
+ const f = pos - a;
109
+ out[i] = arr[a] * (1 - f) + arr[b] * f;
110
+ }
111
+ return out;
112
+ }
113
+
114
+ async function cvFetchGhost(layer) {
115
+ const suffix = (window.AtlasData?.SAE?._manifest?.feature_suffix) || 's0';
116
+ const url = `${CV_HOST}/${CV_GHOST_REPO}/resolve/main/layer_${String(layer).padStart(2,'0')}_${suffix}/meta.json`;
117
+ try {
118
+ const r = await fetch(url);
119
+ if (!r.ok) return null;
120
+ return await r.json();
121
+ } catch { return null; }
122
+ }
123
+
124
+ // ───────────────────────────── main component ─────────────────────────────
125
+
126
+ function ConvergenceValley({ metric, onMetricChange }) {
127
+ const [tick, setTick] = useStateCV(0); // re-render on sae-data-ready
128
+ const [progress, setProgress] = useStateCV(1); // 0..1 of training
129
+ const [playing, setPlaying] = useStateCV(false);
130
+ const [showGhost, setShowGhost] = useStateCV(true);
131
+ const [ghosts, setGhosts] = useStateCV({}); // {2: meta, 3: meta}
132
+ const [cameraTag, setCameraTag] = useStateCV(0); // bump to apply preset
133
+ const plotRef = useRefCV(null);
134
+
135
+ // Pick up SAE data when it lands.
136
+ useEffectCV(() => {
137
+ const fn = () => setTick(t => t + 1);
138
+ window.addEventListener('sae-data-ready', fn);
139
+ return () => window.removeEventListener('sae-data-ready', fn);
140
+ }, []);
141
+
142
+ // Fetch the two stable-run ghosts (L2 + L3 from the saes repo) once.
143
+ useEffectCV(() => {
144
+ let dead = false;
145
+ (async () => {
146
+ const results = await Promise.all(CV_GHOST_LAYERS.map(cvFetchGhost));
147
+ if (dead) return;
148
+ const out = {};
149
+ CV_GHOST_LAYERS.forEach((L, i) => { if (results[i]) out[L] = results[i]; });
150
+ setGhosts(out);
151
+ })();
152
+ return () => { dead = true; };
153
+ }, []);
154
+
155
+ // Playback (scrub the surface forward through training time).
156
+ useEffectCV(() => {
157
+ if (!playing) return;
158
+ const DURATION = 6500;
159
+ const start = performance.now() - progress * DURATION;
160
+ let raf;
161
+ const step = (now) => {
162
+ const t = (now - start) / DURATION;
163
+ if (t >= 1) { setProgress(1); setPlaying(false); return; }
164
+ setProgress(t);
165
+ raf = requestAnimationFrame(step);
166
+ };
167
+ raf = requestAnimationFrame(step);
168
+ return () => cancelAnimationFrame(raf);
169
+ // eslint-disable-next-line react-hooks/exhaustive-deps
170
+ }, [playing]);
171
+
172
+ const sae = window.AtlasData?.SAE;
173
+ const loaded = !!sae && Array.isArray(sae.layers) && sae.layers.length > 0;
174
+
175
+ // Build the full [9 × CV_N_PROG] grid for the chosen metric.
176
+ const grid = useMemoCV(() => {
177
+ if (!loaded) return null;
178
+ const z = [];
179
+ for (let L = 0; L < 9; L++) {
180
+ const rec = sae.byLayer[L];
181
+ const curve = rec?.training_curve?.[metric];
182
+ const r = cvResample(curve, CV_N_PROG);
183
+ z.push(r || Array(CV_N_PROG).fill(NaN));
184
+ }
185
+ return z;
186
+ }, [loaded, metric, tick]);
187
+
188
+ // Scrub: NaN out checkpoints past the playhead so the surface "grows".
189
+ const zScrubbed = useMemoCV(() => {
190
+ if (!grid) return null;
191
+ const cutoff = Math.max(1, Math.round(progress * CV_N_PROG));
192
+ return grid.map(row => row.map((v, i) => (i < cutoff ? v : NaN)));
193
+ }, [grid, progress]);
194
+
195
+ // Ghost (stable-run) trajectories for L2 and L3.
196
+ const ghostTraces = useMemoCV(() => {
197
+ if (!showGhost) return [];
198
+ const traces = [];
199
+ const cutoff = Math.max(1, Math.round(progress * CV_N_PROG));
200
+ for (const L of CV_GHOST_LAYERS) {
201
+ const g = ghosts[L];
202
+ const curve = g?.training_curve?.[metric];
203
+ const r = cvResample(curve, CV_N_PROG);
204
+ if (!r) continue;
205
+ const isFailed = (L === 3); // stable L3 == the famous failed run
206
+ const xs = [], ys = [], zs = [];
207
+ for (let i = 0; i < cutoff; i++) {
208
+ xs.push(L);
209
+ ys.push(i / (CV_N_PROG - 1));
210
+ zs.push(r[i]);
211
+ }
212
+ traces.push({
213
+ type: 'scatter3d', mode: 'lines',
214
+ x: xs, y: ys, z: zs,
215
+ line: {
216
+ color: isFailed ? '#cf8d96' : '#9a87cf',
217
+ width: isFailed ? 7 : 5,
218
+ dash: 'dash',
219
+ },
220
+ opacity: 0.95,
221
+ name: `L${L} · stable run${isFailed ? ' · failed' : ''}`,
222
+ hovertemplate:
223
+ `<b>L${L} · stable run${isFailed ? ' · failed' : ''}</b><br>` +
224
+ `progress %{y:.0%}<br>` +
225
+ `${CV_METRICS[metric].label}: %{z:.4f}<extra></extra>`,
226
+ showlegend: false,
227
+ });
228
+ }
229
+ return traces;
230
+ }, [ghosts, metric, progress, showGhost]);
231
+
232
+ // Z-axis annotations: a small "failed run" callout pinned to L3 stable.
233
+ const sceneAnnotations = useMemoCV(() => {
234
+ const out = [];
235
+ if (showGhost && ghosts[3]?.final_metrics) {
236
+ const z = ghosts[3].final_metrics[metric];
237
+ if (z != null && isFinite(z) && progress > 0.6) {
238
+ out.push({
239
+ x: 3, y: 0.98, z,
240
+ text: 'L3 stable · failed run',
241
+ font: { family: 'Inter Tight, sans-serif', size: 10.5, color: '#a25462' },
242
+ bgcolor: 'rgba(255,247,243,0.92)',
243
+ bordercolor: '#cf8d96',
244
+ borderwidth: 0.8,
245
+ borderpad: 5,
246
+ arrowcolor: '#cf8d96',
247
+ arrowwidth: 1,
248
+ arrowsize: 0.9,
249
+ ax: 40, ay: -34,
250
+ });
251
+ }
252
+ }
253
+ return out;
254
+ }, [ghosts, metric, progress, showGhost]);
255
+
256
+ // Render to Plotly whenever inputs change.
257
+ useEffectCV(() => {
258
+ if (!plotRef.current || !zScrubbed || typeof Plotly === 'undefined') return;
259
+ const m = CV_METRICS[metric];
260
+ const ramp = CV_RAMPS[m.ramp];
261
+
262
+ // Plotly surface wants z as [y][x].
263
+ const ys = Array.from({length: CV_N_PROG}, (_, i) => i / (CV_N_PROG - 1));
264
+ const zT = ys.map((_, i) => zScrubbed.map(row => row[i]));
265
+
266
+ const surface = {
267
+ type: 'surface',
268
+ x: [0,1,2,3,4,5,6,7,8],
269
+ y: ys,
270
+ z: zT,
271
+ colorscale: ramp,
272
+ cmin: m.zRange ? m.zRange[0] : undefined,
273
+ cmax: m.zRange ? m.zRange[1] : undefined,
274
+ showscale: false,
275
+ opacity: 0.97,
276
+ contours: {
277
+ z: {
278
+ show: true,
279
+ usecolormap: false,
280
+ color: 'rgba(74,58,68,0.18)',
281
+ width: 1,
282
+ highlight: true,
283
+ highlightcolor: '#cf8d96',
284
+ highlightwidth: 2,
285
+ project: { z: false },
286
+ },
287
+ },
288
+ lighting: {
289
+ ambient: 0.78,
290
+ diffuse: 0.50,
291
+ specular: 0.05,
292
+ roughness: 0.92,
293
+ fresnel: 0.10,
294
+ },
295
+ lightposition: { x: 4, y: -1, z: 4 },
296
+ hoverlabel: {
297
+ bgcolor: 'rgba(255,247,235,0.95)',
298
+ bordercolor: '#cf8d96',
299
+ font: { family: 'Inter Tight, sans-serif', color: '#3a2a30', size: 12 },
300
+ },
301
+ hovertemplate:
302
+ '<b>L%{x}</b> · progress %{y:.0%}<br>' +
303
+ m.label + ': %{z:.4f}<extra></extra>',
304
+ };
305
+
306
+ const traces = [surface, ...ghostTraces];
307
+
308
+ const axisCommon = {
309
+ gridcolor: 'rgba(74,58,68,0.10)',
310
+ zerolinecolor: 'rgba(74,58,68,0.22)',
311
+ backgroundcolor: 'rgba(255,255,255,0)',
312
+ showbackground: true,
313
+ color: '#84686c',
314
+ titlefont: { family: 'Inter Tight, sans-serif', size: 11, color: '#84686c' },
315
+ tickfont: { family: 'JetBrains Mono, monospace', size: 10, color: '#84686c' },
316
+ };
317
+
318
+ const layout = {
319
+ paper_bgcolor: 'rgba(0,0,0,0)',
320
+ plot_bgcolor: 'rgba(0,0,0,0)',
321
+ margin: { l: 0, r: 0, t: 0, b: 0 },
322
+ showlegend: false,
323
+ hovermode: 'closest',
324
+ scene: {
325
+ xaxis: {
326
+ ...axisCommon,
327
+ title: { text: 'layer' },
328
+ tickvals: [0,1,2,3,4,5,6,7,8],
329
+ ticktext: CV_LAYER_LABELS,
330
+ },
331
+ yaxis: {
332
+ ...axisCommon,
333
+ title: { text: 'training →' },
334
+ tickvals: [0, 0.25, 0.5, 0.75, 1.0],
335
+ ticktext: ['start', '¼', '½', '¾', 'final'],
336
+ },
337
+ zaxis: {
338
+ ...axisCommon,
339
+ title: { text: m.label },
340
+ range: m.zRange || undefined,
341
+ },
342
+ camera: { eye: { x: 1.55, y: -1.65, z: 0.95 }, up: { x: 0, y: 0, z: 1 } },
343
+ aspectratio: { x: 1.5, y: 1.05, z: 0.7 },
344
+ annotations: sceneAnnotations,
345
+ },
346
+ };
347
+
348
+ const config = { displayModeBar: false, responsive: true, doubleClick: 'reset' };
349
+
350
+ // Only reset the camera on metric change / preset bump — not every frame
351
+ // of the scrub, which would yank the user out of an orbit.
352
+ const cameraKey = `${metric}::${cameraTag}`;
353
+ const last = plotRef.current.__cvCameraKey;
354
+ if (last !== cameraKey) {
355
+ plotRef.current.__cvCameraKey = cameraKey;
356
+ Plotly.react(plotRef.current, traces, layout, config);
357
+ } else {
358
+ // Preserve current camera.
359
+ const cur = plotRef.current.layout?.scene?.camera;
360
+ if (cur) layout.scene.camera = cur;
361
+ Plotly.react(plotRef.current, traces, layout, config);
362
+ }
363
+ }, [zScrubbed, ghostTraces, metric, sceneAnnotations, cameraTag]);
364
+
365
+ // Handle window resizes.
366
+ useEffectCV(() => {
367
+ const onResize = () => {
368
+ if (plotRef.current && typeof Plotly !== 'undefined') {
369
+ Plotly.Plots.resize(plotRef.current);
370
+ }
371
+ };
372
+ window.addEventListener('resize', onResize);
373
+ return () => window.removeEventListener('resize', onResize);
374
+ }, []);
375
+
376
+ // Polled live-text status while loading.
377
+ const [loaderState, setLoaderState] = useStateCV(window.SAELoader?.state || {});
378
+ useEffectCV(() => {
379
+ if (!window.SAELoader) return;
380
+ return window.SAELoader.subscribe(setLoaderState);
381
+ }, []);
382
+
383
+ const m = CV_METRICS[metric];
384
+ const ramp = CV_RAMPS[m.ramp];
385
+ const swatchCSS = `linear-gradient(90deg, ${ramp.map(s => s[1]).join(', ')})`;
386
+
387
+ return (
388
+ <div className="cv card" data-screen-label="convergence-valley">
389
+ <div className="cv-head">
390
+ <div className="cv-head-left">
391
+ <div className="cv-eyebrow">SAE training trajectory</div>
392
+ <div className="cv-title">
393
+ Convergence <em>valley</em>
394
+ </div>
395
+ <div className="cv-sub">
396
+ {loaded ? (
397
+ <>per-checkpoint progression across <b>{sae.layers.length}</b> sparse autoencoders ·
398
+ {' '}<span className="cv-sub-quiet">{m.story}</span></>
399
+ ) : (
400
+ <>fetching {loaderState.count ?? 0} / {loaderState.expected ?? 9} layers from HuggingFace…</>
401
+ )}
402
+ </div>
403
+ </div>
404
+ <CVMetricPicker value={metric} onChange={onMetricChange} />
405
+ </div>
406
+
407
+ <div className="cv-body">
408
+ <div className="cv-stage">
409
+ <div ref={plotRef} className="cv-plot" />
410
+ {!loaded && <div className="cv-loading-veil">
411
+ <div className="cv-loading-pulse" />
412
+ <div className="cv-loading-msg">
413
+ {loaderState.status === 'error'
414
+ ? <>couldn't reach the SAE dataset</>
415
+ : <>terrain materializing… <b>{loaderState.count ?? 0}</b> / <b>{loaderState.expected ?? 9}</b> layers</>}
416
+ </div>
417
+ </div>}
418
+ <div className="cv-stage-foot">
419
+ <div className="cv-legend">
420
+ <span className="cv-legend-label">low</span>
421
+ <span className="cv-legend-swatch" style={{ background: swatchCSS }} />
422
+ <span className="cv-legend-label">high</span>
423
+ <span className="cv-legend-metric">· {m.label}</span>
424
+ </div>
425
+ <div className="cv-hint">drag to orbit · scroll to zoom · double-click resets</div>
426
+ </div>
427
+ </div>
428
+
429
+ <CVSidebar
430
+ sae={sae}
431
+ ghosts={ghosts}
432
+ metric={metric}
433
+ showGhost={showGhost}
434
+ onToggleGhost={setShowGhost}
435
+ onResetCamera={() => setCameraTag(c => c + 1)}
436
+ />
437
+ </div>
438
+
439
+ <CVScrubber
440
+ progress={progress}
441
+ playing={playing}
442
+ onScrub={v => { setPlaying(false); setProgress(v); }}
443
+ onTogglePlay={() => setPlaying(p => !p)}
444
+ onReset={() => { setPlaying(false); setProgress(0); }}
445
+ cutoffPct={Math.round(progress * 100)}
446
+ />
447
+ </div>
448
+ );
449
+ }
450
+
451
+ // ───────────────────────────── pieces ─────────────────────────────
452
+
453
+ function CVMetricPicker({ value, onChange }) {
454
+ const keys = ['ev', 'dead_pct', 'recon_loss', 'mean_l0'];
455
+ return (
456
+ <div className="cv-metric-pick" role="tablist" aria-label="surface metric">
457
+ {keys.map(k => (
458
+ <button
459
+ key={k}
460
+ role="tab"
461
+ aria-selected={value === k}
462
+ className={'cv-metric-chip ' + (value === k ? 'is-active' : '')}
463
+ onClick={() => onChange(k)}
464
+ >
465
+ <span className="cv-metric-label">{CV_METRICS[k].label}</span>
466
+ <span className="cv-metric-sub">{CV_METRICS[k].sub}</span>
467
+ </button>
468
+ ))}
469
+ </div>
470
+ );
471
+ }
472
+
473
+ function CVSidebar({ sae, ghosts, metric, showGhost, onToggleGhost, onResetCamera }) {
474
+ const m = CV_METRICS[metric];
475
+ const layers = sae?.layers || [];
476
+ const ghostL3 = ghosts?.[3]?.final_metrics;
477
+ const ghostL2 = ghosts?.[2]?.final_metrics;
478
+
479
+ return (
480
+ <div className="cv-side">
481
+ <div className="cv-side-actions">
482
+ <label className="cv-toggle">
483
+ <input
484
+ type="checkbox"
485
+ checked={showGhost}
486
+ onChange={e => onToggleGhost(e.target.checked)}
487
+ />
488
+ <span className="cv-toggle-dot" />
489
+ <span className="cv-toggle-label">stable-run ghost</span>
490
+ </label>
491
+ <button className="cv-camera-reset" onClick={onResetCamera} title="reset camera">
492
+ ↺ camera
493
+ </button>
494
+ </div>
495
+
496
+ <div className="cv-side-head">
497
+ <div className="cv-side-eyebrow">per-layer ledger</div>
498
+ <div className="cv-side-sub">final checkpoint · all metrics</div>
499
+ </div>
500
+
501
+ <div className="cv-table" role="table">
502
+ <div className="cv-row cv-row-head" role="row">
503
+ <span>layer</span>
504
+ <span>source</span>
505
+ <span className="cv-num">EV</span>
506
+ <span className="cv-num">L0</span>
507
+ <span className="cv-num">dead</span>
508
+ <span className="cv-num">recon</span>
509
+ </div>
510
+ {layers.length === 0
511
+ ? <div className="cv-row cv-row-empty">awaiting data…</div>
512
+ : layers.map(L => {
513
+ const rolling = (L.repo || '').endsWith('rolling');
514
+ const isHighlight = (metric === 'ev' && L.layer === 3) ||
515
+ (metric === 'dead_pct' && L.layer === 2);
516
+ return (
517
+ <div
518
+ key={L.layer}
519
+ className={'cv-row' + (rolling ? ' is-rolling' : '') + (isHighlight ? ' is-highlight' : '')}
520
+ role="row"
521
+ >
522
+ <span className="cv-layer">L{L.layer}</span>
523
+ <span className="cv-src">
524
+ <span className={'cv-src-dot ' + (rolling ? 'rolling' : 'stable')} />
525
+ <span className="cv-src-name">{rolling ? 'rolling' : 'stable'}</span>
526
+ </span>
527
+ <span className="cv-num mono">{CV_METRICS.ev.fmt(L.ev)}</span>
528
+ <span className="cv-num mono">{CV_METRICS.mean_l0.fmt(L.mean_l0)}</span>
529
+ <span className="cv-num mono">{CV_METRICS.dead_pct.fmt(L.dead_pct)}</span>
530
+ <span className="cv-num mono">{CV_METRICS.recon_loss.fmt(L.recon_loss)}</span>
531
+ </div>
532
+ );
533
+ })
534
+ }
535
+ </div>
536
+
537
+ {(ghostL2 || ghostL3) && (
538
+ <div className="cv-ghost-card">
539
+ <div className="cv-ghost-eyebrow">why two of these are different</div>
540
+ <div className="cv-ghost-rows">
541
+ {ghostL2 && (
542
+ <div className="cv-ghost-row">
543
+ <span className="cv-ghost-layer">L2 · stable</span>
544
+ <span className="cv-ghost-stats mono">
545
+ EV {CV_METRICS.ev.fmt(ghostL2.ev)} · dead {CV_METRICS.dead_pct.fmt(ghostL2.dead_pct)}
546
+ </span>
547
+ <span className="cv-ghost-tag tone-amber">over-sparsified</span>
548
+ </div>
549
+ )}
550
+ {ghostL3 && (
551
+ <div className="cv-ghost-row">
552
+ <span className="cv-ghost-layer">L3 · stable</span>
553
+ <span className="cv-ghost-stats mono">
554
+ EV {CV_METRICS.ev.fmt(ghostL3.ev)} · L0 {CV_METRICS.mean_l0.fmt(ghostL3.mean_l0)}
555
+ </span>
556
+ <span className="cv-ghost-tag tone-rose">never converged</span>
557
+ </div>
558
+ )}
559
+ </div>
560
+ <div className="cv-ghost-note">
561
+ L2 &amp; L3 swap to the <b>rolling</b> run on the surface above. The dashed lines mark where the
562
+ <b> stable</b> run actually landed — the L3 dip is the failed run.
563
+ </div>
564
+ </div>
565
+ )}
566
+ </div>
567
+ );
568
+ }
569
+
570
+ function CVScrubber({ progress, playing, onScrub, onTogglePlay, onReset, cutoffPct }) {
571
+ return (
572
+ <div className="cv-scrub">
573
+ <button
574
+ className={'cv-play ' + (playing ? 'is-playing' : '')}
575
+ onClick={onTogglePlay}
576
+ aria-label={playing ? 'pause' : 'play'}
577
+ >
578
+ <span className="cv-play-glyph">{playing ? '❚❚' : '▶'}</span>
579
+ </button>
580
+ <div className="cv-range-wrap">
581
+ <div className="cv-range-track">
582
+ <div className="cv-range-fill" style={{ width: cutoffPct + '%' }} />
583
+ </div>
584
+ <input
585
+ type="range"
586
+ min={0}
587
+ max={1000}
588
+ step={1}
589
+ value={Math.round(progress * 1000)}
590
+ onChange={e => onScrub(Number(e.target.value) / 1000)}
591
+ className="cv-range"
592
+ aria-label="training progress"
593
+ />
594
+ <div className="cv-range-ticks">
595
+ <span>start</span>
596
+ <span>¼</span>
597
+ <span>½</span>
598
+ <span>¾</span>
599
+ <span>final</span>
600
+ </div>
601
+ </div>
602
+ <div className="cv-scrub-readout">
603
+ <span className="cv-scrub-num">{cutoffPct}%</span>
604
+ <span className="cv-scrub-label">of training</span>
605
+ </div>
606
+ <button className="cv-scrub-reset" onClick={onReset}>↺ reset</button>
607
+ </div>
608
+ );
609
+ }
610
+
611
+ window.ConvergenceValley = ConvergenceValley;
612
+ window.CV_METRICS = CV_METRICS;
components/DataSource.jsx CHANGED
@@ -1,21 +1,13 @@
1
  // Live data-source pill — shows status + per-config progress bars.
2
 
3
- const { useState: useStateDS, useEffect: useEffectDS, useRef: useRefDS } = React;
4
 
5
  function DataSource() {
6
  const [s, setS] = useStateDS(window.AtlasLoader.state);
7
  const [expanded, setExpanded] = useStateDS(false);
8
- const [panelPos, setPanelPos] = useStateDS({ top: 0, right: 0 });
9
- const pillRef = useRefDS(null);
10
 
11
  useEffectDS(() => window.AtlasLoader.subscribe(setS), []);
12
 
13
- useEffectDS(() => {
14
- if (!expanded || !pillRef.current) return;
15
- const r = pillRef.current.getBoundingClientRect();
16
- setPanelPos({ top: r.bottom + 10, right: window.innerWidth - r.right });
17
- }, [expanded]);
18
-
19
  const dot = {
20
  idle: { c: 'var(--cream-dim)', label: 'idle' },
21
  loading: { c: 'var(--gold)', label: 'streaming' },
@@ -32,9 +24,15 @@ function DataSource() {
32
  pillLabel = 'live · juiceb0xc0de';
33
  }
34
 
35
- const panel = expanded && ReactDOM.createPortal(
36
- <div className="ds-panel card" data-screen-label="datasource"
37
- style={{ position: 'fixed', top: panelPos.top, right: panelPos.right, zIndex: 9999 }}>
 
 
 
 
 
 
38
  <div className="ds-head">
39
  <div>
40
  <div className="bf-eyebrow">connected to</div>
@@ -88,20 +86,10 @@ function DataSource() {
88
 
89
  <div className="ds-note muted">
90
  Real-time fetch of the published configs from the HuggingFace datasets-server.
91
- <b> compliance_scores</b> (52 MB of raw per-feature vectors) is intentionally skipped to keep page load fast — query via SQL instead.
92
  </div>
93
- </div>,
94
- document.body
95
- );
96
-
97
- return (
98
- <div className={"ds " + (expanded ? 'is-open' : '')}>
99
- <button ref={pillRef} className="ds-pill" onClick={() => setExpanded(!expanded)}>
100
- <span className="ds-led" style={{ background: dot.c, boxShadow: `0 0 10px ${dot.c}` }} />
101
- <span className="ds-label">{pillLabel}</span>
102
- <span className="ds-chev">{expanded ? '▾' : '▸'}</span>
103
- </button>
104
- {panel}
105
  </div>
106
  );
107
  }
 
1
  // Live data-source pill — shows status + per-config progress bars.
2
 
3
+ const { useState: useStateDS, useEffect: useEffectDS } = React;
4
 
5
  function DataSource() {
6
  const [s, setS] = useStateDS(window.AtlasLoader.state);
7
  const [expanded, setExpanded] = useStateDS(false);
 
 
8
 
9
  useEffectDS(() => window.AtlasLoader.subscribe(setS), []);
10
 
 
 
 
 
 
 
11
  const dot = {
12
  idle: { c: 'var(--cream-dim)', label: 'idle' },
13
  loading: { c: 'var(--gold)', label: 'streaming' },
 
24
  pillLabel = 'live · juiceb0xc0de';
25
  }
26
 
27
+ return (
28
+ <div className={"ds " + (expanded ? 'is-open' : '')}>
29
+ <button className="ds-pill" onClick={() => setExpanded(!expanded)}>
30
+ <span className="ds-led" style={{ background: dot.c, boxShadow: `0 0 10px ${dot.c}` }} />
31
+ <span className="ds-label">{pillLabel}</span>
32
+ <span className="ds-chev">{expanded ? '▾' : '▸'}</span>
33
+ </button>
34
+ {expanded && (
35
+ <div className="ds-panel card" data-screen-label="datasource">
36
  <div className="ds-head">
37
  <div>
38
  <div className="bf-eyebrow">connected to</div>
 
86
 
87
  <div className="ds-note muted">
88
  Real-time fetch of the published configs from the HuggingFace datasets-server.
89
+ <b> bouncer_scores</b> (52 MB of raw per-feature vectors) is intentionally skipped to keep page load fast — query via SQL instead.
90
  </div>
91
+ </div>
92
+ )}
 
 
 
 
 
 
 
 
 
 
93
  </div>
94
  );
95
  }
components/Heatmap.jsx CHANGED
@@ -121,7 +121,7 @@ function Heatmap({ matrix, behaviorMatrix, behaviorLabel, onCellClick, selectedC
121
  <>
122
  <span className="mono">L{hover.layer} · {hover.component}</span>
123
  <span className="dot">·</span>
124
- <span>F-stat <b>{Number.isFinite(hover.v) ? hover.v.toFixed(2) : '—'}</b></span>
125
  {behaviorLabel && (<><span className="dot">·</span><span>{behaviorLabel} overlay</span></>)}
126
  </>
127
  ) : (
 
121
  <>
122
  <span className="mono">L{hover.layer} · {hover.component}</span>
123
  <span className="dot">·</span>
124
+ <span>F-stat <b>{hover.v.toFixed(2)}</b></span>
125
  {behaviorLabel && (<><span className="dot">·</span><span>{behaviorLabel} overlay</span></>)}
126
  </>
127
  ) : (
components/Landing.jsx CHANGED
@@ -66,11 +66,11 @@ function Landing({ onEnter }) {
66
  />
67
  <Step
68
  n="ii"
69
- title="Compliance probe"
70
  tint="rose"
71
- body={<>A second corpus pairs "corporate voice" stems against "authentic voice" completions. A per-feature F-test against those labels yields the <b>compliance F-statistic</b> and a signed Δ — how strongly, and which direction, the feature leans on this single axis.</>}
72
- tag="compliance_features · 24.6 k rows"
73
- icon={<IconCompliance />}
74
  />
75
  <Step
76
  n="iii"
@@ -199,7 +199,7 @@ function IconCensus() {
199
  );
200
  }
201
 
202
- function IconCompliance() {
203
  // two clouds of dots — corp vs auth — with a decision boundary
204
  return (
205
  <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
 
66
  />
67
  <Step
68
  n="ii"
69
+ title="Bouncer probe"
70
  tint="rose"
71
+ body={<>A second corpus pairs "corporate voice" stems against "authentic voice" completions. A per-feature F-test against those labels yields the <b>bouncer F-statistic</b> and a signed Δ — how strongly, and which direction, the feature leans on this single axis.</>}
72
+ tag="bouncer_analysis · 24.6 k rows"
73
+ icon={<IconBouncer />}
74
  />
75
  <Step
76
  n="iii"
 
199
  );
200
  }
201
 
202
+ function IconBouncer() {
203
  // two clouds of dots — corp vs auth — with a decision boundary
204
  return (
205
  <svg width="40" height="40" viewBox="0 0 40 40" fill="none">
components/LayerDeepDive.jsx CHANGED
@@ -94,11 +94,11 @@ function LayerDeepDive({ selected, onPickCell }) {
94
  </div>
95
 
96
  <div className="ld-section">
97
- <div className="ld-eyebrow">Top compliance features at L{layer} · {component}</div>
98
  {feats.length === 0 ? (
99
  <div className="ld-empty muted">
100
- No compliance features for L{layer} · {component} — likely not in the analyzed component set
101
- (compliance analysis runs on <span className="mono">mlp</span> and <span className="mono">gate</span> right now).
102
  </div>
103
  ) : (
104
  <div className="ld-feats">
 
94
  </div>
95
 
96
  <div className="ld-section">
97
+ <div className="ld-eyebrow">Top bouncer features at L{layer} · {component}</div>
98
  {feats.length === 0 ? (
99
  <div className="ld-empty muted">
100
+ No bouncer features for L{layer} · {component} — likely not in the analyzed component set
101
+ (bouncer analysis runs on <span className="mono">mlp</span> and <span className="mono">gate</span> right now).
102
  </div>
103
  ) : (
104
  <div className="ld-feats">
components/SAEDebugPanel.jsx DELETED
@@ -1,90 +0,0 @@
1
- // ───────────────────────────────────────────────────────────────────────────
2
- // PLACEHOLDER / SCAFFOLDING — intentionally unstyled.
3
- // This is a raw verification table for the SAE data layer (window.AtlasData.SAE,
4
- // populated by sae-loader.js). It exists so we can confirm the metrics are wired
5
- // correctly before visual design. Claude Design: feel free to restyle or replace
6
- // this entirely. The interesting data for visualization is each layer's
7
- // `training_curve` (per-checkpoint EV / L0 / dead% arrays) — see SAE_CONTRACT.md.
8
- // ───────────────────────────────────────────────────────────────────────────
9
-
10
- function SAEDebugPanel() {
11
- const [st, setSt] = React.useState(window.SAELoader ? window.SAELoader.state : { status: 'idle' });
12
- const [, tick] = React.useReducer(x => x + 1, 0);
13
-
14
- React.useEffect(() => {
15
- if (!window.SAELoader) return;
16
- const unsub = window.SAELoader.subscribe(setSt);
17
- const onReady = () => tick();
18
- window.addEventListener('sae-data-ready', onReady);
19
- return () => { unsub && unsub(); window.removeEventListener('sae-data-ready', onReady); };
20
- }, []);
21
-
22
- const sae = (window.AtlasData && window.AtlasData.SAE) || null;
23
- const layers = sae ? sae.layers : [];
24
- const fmt = (v, d = 3) => (typeof v === 'number' ? v.toFixed(d) : '—');
25
- const repoShort = r => (r ? r.split('/').pop().replace('gemma-4-e2b-', '') : '—');
26
-
27
- return (
28
- <div data-screen-label="sae-debug" style={{ fontFamily: 'var(--mono, monospace)', fontSize: 12 }}>
29
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6 }}>
30
- <div>
31
- <div className="bf-eyebrow">SAE training metrics</div>
32
- <div className="bf-sub">
33
- placeholder scaffolding · {st.status}
34
- {st.status === 'loading' ? ` · ${Math.round((st.progress || 0) * 100)}%` : ''}
35
- {sae ? ` · ${sae.meta.loaded}/${sae.meta.expected} layers` : ''}
36
- </div>
37
- </div>
38
- <span style={{ opacity: 0.6, fontStyle: 'italic' }}>raw data check — restyle me</span>
39
- </div>
40
-
41
- {st.status === 'error' && (
42
- <div style={{ color: '#a33', padding: 8 }}>SAE load failed: {st.error}</div>
43
- )}
44
-
45
- {layers.length > 0 ? (
46
- <table style={{ borderCollapse: 'collapse', width: '100%' }}>
47
- <thead>
48
- <tr style={{ textAlign: 'right', opacity: 0.7 }}>
49
- <th style={{ textAlign: 'left', padding: '3px 8px' }}>layer</th>
50
- <th style={{ textAlign: 'left', padding: '3px 8px' }}>source</th>
51
- <th style={{ padding: '3px 8px' }}>EV</th>
52
- <th style={{ padding: '3px 8px' }}>mean L0</th>
53
- <th style={{ padding: '3px 8px' }}>dead %</th>
54
- <th style={{ padding: '3px 8px' }}>recon</th>
55
- <th style={{ padding: '3px 8px' }}>steps</th>
56
- <th style={{ padding: '3px 8px' }}>ckpts</th>
57
- </tr>
58
- </thead>
59
- <tbody>
60
- {layers.map(L => (
61
- <tr key={L.layer} style={{ borderTop: '1px solid rgba(0,0,0,0.08)', textAlign: 'right' }}
62
- title={L.note || ''}>
63
- <td style={{ textAlign: 'left', padding: '3px 8px' }}>L{L.layer}</td>
64
- <td style={{ textAlign: 'left', padding: '3px 8px' }}>
65
- {repoShort(L.repo)}{L.note ? ' *' : ''}
66
- </td>
67
- <td style={{ padding: '3px 8px' }}>{fmt(L.ev)}</td>
68
- <td style={{ padding: '3px 8px' }}>{fmt(L.mean_l0, 1)}</td>
69
- <td style={{ padding: '3px 8px' }}>{fmt(L.dead_pct, 1)}</td>
70
- <td style={{ padding: '3px 8px' }}>{fmt(L.recon_loss, 5)}</td>
71
- <td style={{ padding: '3px 8px' }}>{L.n_steps ?? '—'}</td>
72
- <td style={{ padding: '3px 8px' }}>{(L.training_curve && L.training_curve.ev.length) || 0}</td>
73
- </tr>
74
- ))}
75
- </tbody>
76
- </table>
77
- ) : (
78
- st.status !== 'error' && <div style={{ opacity: 0.6, padding: 8 }}>loading SAE metrics…</div>
79
- )}
80
-
81
- {layers.some(L => L.note) && (
82
- <div style={{ opacity: 0.6, marginTop: 6, fontSize: 11 }}>
83
- * source switched for this layer — hover the row for why.
84
- </div>
85
- )}
86
- </div>
87
- );
88
- }
89
-
90
- window.SAEDebugPanel = SAEDebugPanel;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/SQLBox.jsx CHANGED
@@ -1,5 +1,5 @@
1
  // SQL query box — accepts SELECT statements against the live atlas tables.
2
- // Tables: features (compliance features), per_head, prompts, layers, components, behaviors
3
 
4
  const { useState: useStateSQL, useMemo: useMemoSQL } = React;
5
 
@@ -25,15 +25,6 @@ function buildTables() {
25
  };
26
  });
27
 
28
- const ov_circuits = (raw.ovCircuits || []).map(r => ({
29
- layer: r.layer, head: r.head, kv_head: r.kv_head,
30
- top_singular_val: round(r.top_singular_val, 3),
31
- spectral_conc: round(r.spectral_conc, 4),
32
- eff_rank: round(r.eff_rank, 2),
33
- compliance_score: round(r.compliance_score, 4),
34
- layer_comp_strength: round(r.layer_comp_strength, 4),
35
- }));
36
-
37
  const per_head = phRaw.map(r => ({
38
  layer: r.layer, component: r.component, head: r.head,
39
  dims: r.dims, non_zero_var: r.non_zero_var,
@@ -64,17 +55,15 @@ function buildTables() {
64
  position: round(x.layer / Math.max(1, (window.AtlasData?.N_LAYERS || 1) - 1), 3),
65
  }));
66
 
67
- // components table: prefer component_summary (has tax/fstat data) over COMPONENTS list
68
- const csRaw = raw.componentSummary || [];
69
- const components = csRaw.length > 0
70
- ? csRaw.map(r => ({ ...r }))
71
- : (window.AtlasData?.COMPONENTS || []).map(c => ({ component: c.id, kind: c.kind, label: c.label }));
72
 
73
  const behaviors = (window.AtlasData?.BEHAVIORS || []).map(b => ({
74
  behavior: b.id, label: b.label, prompt_count: b.count,
75
  }));
76
 
77
- return { features, per_head, prompts, layers, components, behaviors, ov_circuits };
78
  }
79
 
80
  function round(v, p) {
@@ -148,22 +137,18 @@ function parseAndRun(sql, tables) {
148
  }
149
 
150
  const EXAMPLES = [
151
- { label: 'sharpest compliance features',
152
  sql: "SELECT layer, component, feature, fstat, delta\nFROM features\nWHERE leaning = 'corp'\nORDER BY fstat DESC\nLIMIT 20" },
153
- { label: 'gate dormancy by layer',
154
- sql: "SELECT layer, component, tax_non_activated, n_features,\n ROUND(100.0 * tax_non_activated / n_features, 1) AS dead_pct\nFROM components\nWHERE component = 'gate'\nORDER BY dead_pct DESC\nLIMIT 20" },
155
- { label: 'sharpest separator heads',
156
  sql: "SELECT layer, component, head, top_sep_score, top_code_spec\nFROM per_head\nORDER BY top_sep_score DESC\nLIMIT 25" },
157
- { label: 'L0 entanglement vs selectivity',
158
- sql: "SELECT layer, component, code_entangled, code_selective,\n ROUND(1.0 * code_selective / MAX(code_entangled, 1), 2) AS sel_ratio\nFROM components\nORDER BY layer ASC, sel_ratio DESC\nLIMIT 30" },
159
  { label: 'richest layers',
160
  sql: "SELECT layer, mean_fstat, max_fstat, n_features FROM layers\nORDER BY mean_fstat DESC\nLIMIT 12" },
161
  { label: 'creative-writing prompts',
162
  sql: "SELECT idx, subcategory, prompt FROM prompts\nWHERE category = 'Creative Writing'\nLIMIT 15" },
163
- { label: 'sharpest OV circuits',
164
- sql: "SELECT layer, head, spectral_conc, eff_rank, compliance_score\nFROM ov_circuits\nORDER BY spectral_conc DESC\nLIMIT 20" },
165
- { label: 'compliance-aligned heads',
166
- sql: "SELECT layer, head, compliance_score, spectral_conc, eff_rank\nFROM ov_circuits\nORDER BY compliance_score DESC\nLIMIT 20" },
167
  ];
168
 
169
  function SQLBox() {
 
1
  // SQL query box — accepts SELECT statements against the live atlas tables.
2
+ // Tables: features (bouncer_analysis), per_head, prompts, layers, components, behaviors
3
 
4
  const { useState: useStateSQL, useMemo: useMemoSQL } = React;
5
 
 
25
  };
26
  });
27
 
 
 
 
 
 
 
 
 
 
28
  const per_head = phRaw.map(r => ({
29
  layer: r.layer, component: r.component, head: r.head,
30
  dims: r.dims, non_zero_var: r.non_zero_var,
 
55
  position: round(x.layer / Math.max(1, (window.AtlasData?.N_LAYERS || 1) - 1), 3),
56
  }));
57
 
58
+ const components = (window.AtlasData?.COMPONENTS || []).map(c => ({
59
+ component: c.id, kind: c.kind, label: c.label,
60
+ }));
 
 
61
 
62
  const behaviors = (window.AtlasData?.BEHAVIORS || []).map(b => ({
63
  behavior: b.id, label: b.label, prompt_count: b.count,
64
  }));
65
 
66
+ return { features, per_head, prompts, layers, components, behaviors };
67
  }
68
 
69
  function round(v, p) {
 
137
  }
138
 
139
  const EXAMPLES = [
140
+ { label: 'sharpest corp-leaning features',
141
  sql: "SELECT layer, component, feature, fstat, delta\nFROM features\nWHERE leaning = 'corp'\nORDER BY fstat DESC\nLIMIT 20" },
142
+ { label: 'auth/corp split by layer',
143
+ sql: "SELECT layer, leaning, fstat FROM features\nWHERE rank < 3\nORDER BY layer ASC\nLIMIT 50" },
144
+ { label: 'sharpest heads',
145
  sql: "SELECT layer, component, head, top_sep_score, top_code_spec\nFROM per_head\nORDER BY top_sep_score DESC\nLIMIT 25" },
146
+ { label: 'highly-specialized dims',
147
+ sql: "SELECT layer, component, head, top_code_dim, top_code_spec\nFROM per_head\nWHERE top_code_spec > 0.3\nORDER BY top_code_spec DESC\nLIMIT 20" },
148
  { label: 'richest layers',
149
  sql: "SELECT layer, mean_fstat, max_fstat, n_features FROM layers\nORDER BY mean_fstat DESC\nLIMIT 12" },
150
  { label: 'creative-writing prompts',
151
  sql: "SELECT idx, subcategory, prompt FROM prompts\nWHERE category = 'Creative Writing'\nLIMIT 15" },
 
 
 
 
152
  ];
153
 
154
  function SQLBox() {
components/SurgicalTargets.jsx CHANGED
@@ -64,7 +64,7 @@ function SurgicalTargets({ rows, onPick }) {
64
  <div className="st-head">
65
  <div>
66
  <div className="bf-eyebrow">Feature inventory</div>
67
- <div className="bf-sub">Every catalogued compliance feature, sorted by how cleanly it separates the corporate/authentic axis.</div>
68
  </div>
69
  <div className="st-filters">
70
  {splits.length > 1 && (
 
64
  <div className="st-head">
65
  <div>
66
  <div className="bf-eyebrow">Feature inventory</div>
67
+ <div className="bf-sub">Every catalogued feature, sorted by how cleanly it separates the bouncer-axis labels.</div>
68
  </div>
69
  <div className="st-filters">
70
  {splits.length > 1 && (
index.html CHANGED
@@ -31,7 +31,7 @@
31
  <script type="text/babel" src="components/LayerDeepDive.jsx"></script>
32
  <script type="text/babel" src="components/SQLBox.jsx"></script>
33
  <script type="text/babel" src="components/DataSource.jsx"></script>
34
- <script type="text/babel" src="components/SAEDebugPanel.jsx"></script>
35
  <script type="text/babel" src="app.jsx"></script>
36
 
37
  <script type="text/babel">
 
31
  <script type="text/babel" src="components/LayerDeepDive.jsx"></script>
32
  <script type="text/babel" src="components/SQLBox.jsx"></script>
33
  <script type="text/babel" src="components/DataSource.jsx"></script>
34
+ <script type="text/babel" src="components/ConvergenceValley.jsx"></script>
35
  <script type="text/babel" src="app.jsx"></script>
36
 
37
  <script type="text/babel">
loader.js CHANGED
@@ -4,13 +4,11 @@
4
  // already on window.AtlasData if anything fails.
5
 
6
  (function () {
7
- const REPO = 'juiceb0xc0de/gemma-4-e2b-atlas';
8
- const TOKEN = window.HF_TOKEN || '';
9
- const HEADERS = TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {};
10
- const ROWS = (cfg, split, off, len) =>
11
  `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(REPO)}&config=${cfg}&split=${split}&offset=${off}&length=${len}`;
12
- const SPLITS = `https://datasets-server.huggingface.co/splits?dataset=${encodeURIComponent(REPO)}`;
13
- const SIZE = `https://datasets-server.huggingface.co/size?dataset=${encodeURIComponent(REPO)}`;
14
 
15
  const state = {
16
  status: 'idle', // idle | loading | live | error
@@ -28,7 +26,7 @@
28
  // /splits gives us configs+splits but NOT row counts; /size gives counts.
29
  let splits = [];
30
  try {
31
- const r = await fetch(SPLITS, { headers: HEADERS });
32
  if (r.ok) {
33
  const j = await r.json();
34
  splits = (j.splits || []).map(s => ({ config: s.config, split: s.split, num_rows: null }));
@@ -36,7 +34,7 @@
36
  } catch {}
37
  // augment with row counts from /size
38
  try {
39
- const r = await fetch(SIZE, { headers: HEADERS });
40
  if (r.ok) {
41
  const j = await r.json();
42
  const sizes = (j.size && j.size.splits) || [];
@@ -51,26 +49,7 @@
51
  }
52
  async function fetchSize() { return null; /* folded into fetchSplits */ }
53
 
54
- // Fetch one page with up to `retries` retries and exponential back-off.
55
- async function fetchPage(url, retries = 4) {
56
- let delay = 800;
57
- for (let attempt = 0; attempt <= retries; attempt++) {
58
- try {
59
- const r = await fetch(url, { headers: HEADERS });
60
- if (r.ok) return r;
61
- if (r.status === 429 || r.status >= 500) {
62
- if (attempt < retries) { await new Promise(res => setTimeout(res, delay)); delay *= 2; continue; }
63
- }
64
- throw new Error(`HTTP ${r.status}`);
65
- } catch (e) {
66
- if (attempt < retries) { await new Promise(res => setTimeout(res, delay)); delay *= 2; }
67
- else throw e;
68
- }
69
- }
70
- }
71
-
72
  // Paginate /rows. If `total` is null, pages until an empty page comes back.
73
- // Returns whatever rows were fetched even if a page fails mid-way.
74
  async function fetchAllRows(cfg, split, total, onProgress) {
75
  const PAGE = 100;
76
  const out = [];
@@ -79,20 +58,15 @@
79
  while (true) {
80
  const want = total != null ? Math.min(PAGE, total - off) : PAGE;
81
  if (want <= 0) break;
82
- let r;
83
- try {
84
- r = await fetchPage(ROWS(cfg, split, off, want));
85
- } catch (e) {
86
- console.warn(`[AtlasLoader] ${cfg} @${off} failed after retries:`, e.message, '— using partial data');
87
- break;
88
- }
89
  const j = await r.json();
90
  const rows = j.rows || [];
91
  if (rows.length === 0) break;
92
  for (const row of rows) out.push(row.row);
93
  done += rows.length;
94
  off += rows.length;
95
- const denom = total != null ? total : (done + PAGE);
96
  onProgress && onProgress(done, denom);
97
  if (rows.length < want) break;
98
  }
@@ -250,7 +224,7 @@
250
  return { matrix, covered };
251
  }
252
 
253
- // ---- Feature inventory from real compliance features ----------------------
254
  function buildSurgicalTargets(features) {
255
  // Selectivity = log(1 + F-stat) × (1 - |Δ|), scaled to ~0..100.
256
  // High = the feature separates the labeled axes cleanly with a moderate
@@ -305,13 +279,7 @@
305
  meanFstat: r.mean_sep_score,
306
  topCodeDim: r.top_code_dim,
307
  topCodeSpec: parseFloat(r.top_code_spec),
308
- taxonomy: {
309
- all_shared: r.tax_all_shared ?? 0,
310
- broadly_shared:r.tax_broadly_shared?? 0,
311
- partial_shared:r.tax_partial_shared?? 0,
312
- specific: r.tax_specific ?? 0,
313
- non_activated: r.tax_non_activated ?? 0,
314
- },
315
  dims: r.dims, nonZeroVar: r.non_zero_var,
316
  })).sort((a, b) => b.fstat - a.fstat);
317
  };
@@ -319,15 +287,9 @@
319
 
320
  // Derive an interpretable role from taxonomy + spec.
321
  function pickRole(r) {
322
- const dims = r.dims || 1;
323
- const tax = {
324
- non_activated: r.tax_non_activated ?? 0,
325
- all_shared: r.tax_all_shared ?? 0,
326
- broadly_shared:r.tax_broadly_shared?? 0,
327
- partial_shared:r.tax_partial_shared?? 0,
328
- specific: r.tax_specific ?? 0,
329
- };
330
  const spec = parseFloat(r.top_code_spec) || 0;
 
331
  const nonAct = (tax.non_activated || 0) / dims;
332
  const allShared = (tax.all_shared || 0) / dims;
333
  const broadly = (tax.broadly_shared || 0) / dims;
@@ -379,9 +341,6 @@
379
  const phSplit = findSplit('per_head');
380
  const bSplit = findSplit('bouncer_analysis');
381
  const pSplit = findSplit('prompts');
382
- const csSplit = findSplit('component_summary');
383
- const caSplit = findSplit('code_analysis');
384
- const ovSplit = findSplit('ov_circuits');
385
  if (!phSplit || !bSplit || !pSplit) {
386
  throw new Error('expected configs missing — has per_head / bouncer_analysis / prompts been published?');
387
  }
@@ -392,13 +351,10 @@
392
  };
393
  notify();
394
 
395
- const [perHead, bouncer, prompts, componentSummary, codeAnalysis, ovCircuits] = await Promise.all([
396
- fetchAllRows('per_head', 'train', phSplit.num_rows ?? null, (d, t) => { state.progress.perHead = t ? d/t : 0; notify(); }),
397
- fetchAllRows('bouncer_analysis', 'train', bSplit.num_rows ?? null, (d, t) => { state.progress.bouncer = t ? d/t : 0; notify(); }),
398
- fetchAllRows('prompts', 'train', pSplit.num_rows ?? null, (d, t) => { state.progress.prompts = t ? d/t : 0; notify(); }),
399
- csSplit ? fetchAllRows('component_summary', 'train', csSplit.num_rows ?? null, null) : Promise.resolve([]),
400
- caSplit ? fetchAllRows('code_analysis', 'train', caSplit.num_rows ?? null, null) : Promise.resolve([]),
401
- ovSplit ? fetchAllRows('ov_circuits', 'train', ovSplit.num_rows ?? null, null) : Promise.resolve([]),
402
  ]);
403
 
404
  // ---- assemble ----
@@ -468,7 +424,7 @@
468
  authorHandle: 'juiceb0xc0de',
469
  source: 'live',
470
  },
471
- _RAW: { perHead, bouncer: bn.features, prompts, componentSummary, codeAnalysis, ovCircuits }, // for SQL box
472
  };
473
 
474
  state.status = 'live';
 
4
  // already on window.AtlasData if anything fails.
5
 
6
  (function () {
7
+ const REPO = 'juiceb0xc0de/gemma-4-e2b-atlas';
8
+ const ROWS = (cfg, split, off, len) =>
 
 
9
  `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(REPO)}&config=${cfg}&split=${split}&offset=${off}&length=${len}`;
10
+ const SPLITS = `https://datasets-server.huggingface.co/splits?dataset=${encodeURIComponent(REPO)}`;
11
+ const SIZE = `https://datasets-server.huggingface.co/size?dataset=${encodeURIComponent(REPO)}`;
12
 
13
  const state = {
14
  status: 'idle', // idle | loading | live | error
 
26
  // /splits gives us configs+splits but NOT row counts; /size gives counts.
27
  let splits = [];
28
  try {
29
+ const r = await fetch(SPLITS);
30
  if (r.ok) {
31
  const j = await r.json();
32
  splits = (j.splits || []).map(s => ({ config: s.config, split: s.split, num_rows: null }));
 
34
  } catch {}
35
  // augment with row counts from /size
36
  try {
37
+ const r = await fetch(SIZE);
38
  if (r.ok) {
39
  const j = await r.json();
40
  const sizes = (j.size && j.size.splits) || [];
 
49
  }
50
  async function fetchSize() { return null; /* folded into fetchSplits */ }
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  // Paginate /rows. If `total` is null, pages until an empty page comes back.
 
53
  async function fetchAllRows(cfg, split, total, onProgress) {
54
  const PAGE = 100;
55
  const out = [];
 
58
  while (true) {
59
  const want = total != null ? Math.min(PAGE, total - off) : PAGE;
60
  if (want <= 0) break;
61
+ const r = await fetch(ROWS(cfg, split, off, want));
62
+ if (!r.ok) throw new Error(`rows ${cfg} @${off}: ${r.status}`);
 
 
 
 
 
63
  const j = await r.json();
64
  const rows = j.rows || [];
65
  if (rows.length === 0) break;
66
  for (const row of rows) out.push(row.row);
67
  done += rows.length;
68
  off += rows.length;
69
+ const denom = total != null ? total : (done + PAGE); // jittered estimate while exploring
70
  onProgress && onProgress(done, denom);
71
  if (rows.length < want) break;
72
  }
 
224
  return { matrix, covered };
225
  }
226
 
227
+ // ---- Feature inventory from real bouncer features ----------------------
228
  function buildSurgicalTargets(features) {
229
  // Selectivity = log(1 + F-stat) × (1 - |Δ|), scaled to ~0..100.
230
  // High = the feature separates the labeled axes cleanly with a moderate
 
279
  meanFstat: r.mean_sep_score,
280
  topCodeDim: r.top_code_dim,
281
  topCodeSpec: parseFloat(r.top_code_spec),
282
+ taxonomy: parsePyDict(r.taxonomy),
 
 
 
 
 
 
283
  dims: r.dims, nonZeroVar: r.non_zero_var,
284
  })).sort((a, b) => b.fstat - a.fstat);
285
  };
 
287
 
288
  // Derive an interpretable role from taxonomy + spec.
289
  function pickRole(r) {
290
+ const tax = parsePyDict(r.taxonomy) || {};
 
 
 
 
 
 
 
291
  const spec = parseFloat(r.top_code_spec) || 0;
292
+ const dims = r.dims || 1;
293
  const nonAct = (tax.non_activated || 0) / dims;
294
  const allShared = (tax.all_shared || 0) / dims;
295
  const broadly = (tax.broadly_shared || 0) / dims;
 
341
  const phSplit = findSplit('per_head');
342
  const bSplit = findSplit('bouncer_analysis');
343
  const pSplit = findSplit('prompts');
 
 
 
344
  if (!phSplit || !bSplit || !pSplit) {
345
  throw new Error('expected configs missing — has per_head / bouncer_analysis / prompts been published?');
346
  }
 
351
  };
352
  notify();
353
 
354
+ const [perHead, bouncer, prompts] = await Promise.all([
355
+ fetchAllRows('per_head', 'train', phSplit.num_rows ?? null, (d, t) => { state.progress.perHead = t ? d/t : 0; notify(); }),
356
+ fetchAllRows('bouncer_analysis', 'train', bSplit.num_rows ?? null, (d, t) => { state.progress.bouncer = t ? d/t : 0; notify(); }),
357
+ fetchAllRows('prompts', 'train', pSplit.num_rows ?? null, (d, t) => { state.progress.prompts = t ? d/t : 0; notify(); }),
 
 
 
358
  ]);
359
 
360
  // ---- assemble ----
 
424
  authorHandle: 'juiceb0xc0de',
425
  source: 'live',
426
  },
427
+ _RAW: { perHead, bouncer: bn.features, prompts }, // for SQL box
428
  };
429
 
430
  state.status = 'live';
sae-loader.js CHANGED
@@ -6,7 +6,8 @@
6
  // same way. No token required — the SAE datasets are public.
7
 
8
  (function () {
9
- const MANIFEST = '/assets/sae_manifest.json';
 
10
  const RESOLVE = (repo, rev, layer, suffix) =>
11
  `https://huggingface.co/datasets/${repo}/resolve/${rev}/layer_${String(layer).padStart(2, '0')}_${suffix}/meta.json`;
12
 
@@ -38,6 +39,15 @@
38
  }
39
  }
40
 
 
 
 
 
 
 
 
 
 
41
  // Normalize one meta.json into the SAE layer record the UI consumes.
42
  function buildLayer(entry, meta) {
43
  const fm = meta.final_metrics || {};
@@ -84,7 +94,7 @@
84
  notify();
85
 
86
  try {
87
- const manifest = await fetchJSON(MANIFEST);
88
  const entries = manifest.layers || [];
89
  const suffix = manifest.feature_suffix || 's0';
90
  state.expected = entries.length;
 
6
  // same way. No token required — the SAE datasets are public.
7
 
8
  (function () {
9
+ // Try the HF Space path first (deployed), fall back to project-relative (local).
10
+ const MANIFEST_CANDIDATES = ['./sae_manifest.json', '/assets/sae_manifest.json'];
11
  const RESOLVE = (repo, rev, layer, suffix) =>
12
  `https://huggingface.co/datasets/${repo}/resolve/${rev}/layer_${String(layer).padStart(2, '0')}_${suffix}/meta.json`;
13
 
 
39
  }
40
  }
41
 
42
+ async function loadManifest() {
43
+ let lastErr;
44
+ for (const url of MANIFEST_CANDIDATES) {
45
+ try { return await fetchJSON(url, 0); }
46
+ catch (e) { lastErr = e; }
47
+ }
48
+ throw lastErr || new Error('manifest not found');
49
+ }
50
+
51
  // Normalize one meta.json into the SAE layer record the UI consumes.
52
  function buildLayer(entry, meta) {
53
  const fm = meta.final_metrics || {};
 
94
  notify();
95
 
96
  try {
97
+ const manifest = await loadManifest();
98
  const entries = manifest.layers || [];
99
  const suffix = manifest.feature_suffix || 's0';
100
  state.expected = entries.length;
styles.css CHANGED
@@ -107,8 +107,6 @@ body {
107
  box-shadow: var(--shadow-soft);
108
  margin-bottom: 24px;
109
  backdrop-filter: blur(4px);
110
- position: relative;
111
- z-index: 100;
112
  }
113
  .brand { display: flex; align-items: center; gap: 14px; }
114
  .brand-mark {
@@ -1043,6 +1041,474 @@ body {
1043
  .landing-mark, .landing-meta, .landing-process, .landing-foot { opacity: 1; transform: none; }
1044
  }
1045
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1046
  /* === Responsive === */
1047
  @media (max-width: 1180px) {
1048
  .hero-band { grid-template-columns: 1fr; }
@@ -1051,4 +1517,12 @@ body {
1051
  .ld-comps { grid-template-columns: repeat(2, 1fr); }
1052
  .bf { min-height: auto; }
1053
  .hm-card { min-height: 620px; }
 
 
 
 
 
 
 
 
1054
  }
 
107
  box-shadow: var(--shadow-soft);
108
  margin-bottom: 24px;
109
  backdrop-filter: blur(4px);
 
 
110
  }
111
  .brand { display: flex; align-items: center; gap: 14px; }
112
  .brand-mark {
 
1041
  .landing-mark, .landing-meta, .landing-process, .landing-foot { opacity: 1; transform: none; }
1042
  }
1043
 
1044
+ /* ============================================================ */
1045
+ /* === CONVERGENCE VALLEY — 3D SAE training-trajectory hero === */
1046
+ /* ============================================================ */
1047
+
1048
+ .cv {
1049
+ margin-bottom: 22px;
1050
+ padding: 26px 28px 22px;
1051
+ display: flex; flex-direction: column;
1052
+ gap: 18px;
1053
+ position: relative;
1054
+ overflow: hidden;
1055
+ background:
1056
+ linear-gradient(160deg, rgba(255,255,255,0.45), rgba(255,255,255,0.18)),
1057
+ linear-gradient(180deg, var(--bg-3), var(--bg-2));
1058
+ }
1059
+ .cv::before {
1060
+ content: ''; position: absolute;
1061
+ top: -120px; right: -120px;
1062
+ width: 380px; height: 380px;
1063
+ border-radius: 50%;
1064
+ background: radial-gradient(circle, rgba(201,184,230,0.32), transparent 70%);
1065
+ pointer-events: none;
1066
+ z-index: 0;
1067
+ }
1068
+ .cv::after {
1069
+ content: ''; position: absolute;
1070
+ bottom: -160px; left: -120px;
1071
+ width: 420px; height: 420px;
1072
+ border-radius: 50%;
1073
+ background: radial-gradient(circle, rgba(244,217,158,0.28), transparent 70%);
1074
+ pointer-events: none;
1075
+ z-index: 0;
1076
+ }
1077
+ .cv > * { position: relative; z-index: 1; }
1078
+
1079
+ /* — header — */
1080
+
1081
+ .cv-head {
1082
+ display: flex; justify-content: space-between; align-items: flex-start;
1083
+ gap: 28px;
1084
+ flex-wrap: wrap;
1085
+ }
1086
+ .cv-head-left { display: flex; flex-direction: column; gap: 4px; }
1087
+ .cv-eyebrow {
1088
+ font-size: 10.5px; letter-spacing: 0.22em; text-transform: uppercase;
1089
+ color: var(--lavender-2); font-weight: 600;
1090
+ }
1091
+ .cv-title {
1092
+ font-family: var(--serif); font-weight: 400;
1093
+ font-size: 30px; line-height: 1.0;
1094
+ letter-spacing: -0.015em;
1095
+ color: var(--ink);
1096
+ margin-top: 2px;
1097
+ }
1098
+ .cv-title em { font-style: italic; color: var(--rose-2); }
1099
+ .cv-sub {
1100
+ font-family: var(--serif); font-style: italic;
1101
+ font-size: 14px; color: var(--ink-2);
1102
+ margin-top: 6px;
1103
+ max-width: 56ch;
1104
+ line-height: 1.5;
1105
+ }
1106
+ .cv-sub b { font-style: normal; font-family: var(--sans); font-weight: 600; color: var(--ink); }
1107
+ .cv-sub-quiet { color: var(--ink-3); }
1108
+
1109
+ .cv-metric-pick {
1110
+ display: flex; gap: 6px;
1111
+ padding: 4px;
1112
+ background: rgba(255,255,255,0.55);
1113
+ border: 1px solid var(--hairline);
1114
+ border-radius: 14px;
1115
+ box-shadow: var(--shadow-soft);
1116
+ }
1117
+ .cv-metric-chip {
1118
+ appearance: none; background: transparent; border: none;
1119
+ padding: 8px 14px 9px;
1120
+ border-radius: 10px;
1121
+ cursor: pointer;
1122
+ font-family: var(--sans);
1123
+ display: flex; flex-direction: column; align-items: flex-start;
1124
+ gap: 2px;
1125
+ transition: background 140ms ease, color 140ms ease, box-shadow 140ms ease;
1126
+ }
1127
+ .cv-metric-chip:hover { background: rgba(201,184,230,0.16); }
1128
+ .cv-metric-chip.is-active {
1129
+ background: linear-gradient(170deg, rgba(201,184,230,0.55), rgba(236,181,190,0.30));
1130
+ box-shadow: 0 1px 0 rgba(255,255,255,0.7) inset, 0 4px 12px rgba(154,135,207,0.25);
1131
+ }
1132
+ .cv-metric-label {
1133
+ font-size: 12.5px; font-weight: 600;
1134
+ color: var(--ink-2); letter-spacing: 0.02em;
1135
+ }
1136
+ .cv-metric-chip.is-active .cv-metric-label { color: var(--ink); }
1137
+ .cv-metric-sub {
1138
+ font-size: 10px; color: var(--ink-3);
1139
+ letter-spacing: 0.02em;
1140
+ }
1141
+ .cv-metric-chip.is-active .cv-metric-sub { color: var(--rose-2); }
1142
+
1143
+ /* — body grid: 3D stage + ledger sidebar — */
1144
+
1145
+ .cv-body {
1146
+ display: grid;
1147
+ grid-template-columns: minmax(0, 1fr) 360px;
1148
+ gap: 22px;
1149
+ }
1150
+
1151
+ .cv-stage {
1152
+ position: relative;
1153
+ display: flex; flex-direction: column;
1154
+ background:
1155
+ radial-gradient(ellipse 80% 60% at 50% 100%, rgba(244,217,158,0.20), transparent 60%),
1156
+ linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0.30));
1157
+ border: 1px solid var(--hairline);
1158
+ border-radius: var(--radius-lg);
1159
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.5), 0 1px 0 rgba(255,255,255,0.7) inset;
1160
+ min-height: 560px;
1161
+ overflow: hidden;
1162
+ }
1163
+ .cv-plot {
1164
+ width: 100%; flex: 1;
1165
+ min-height: 500px;
1166
+ }
1167
+ .cv-stage-foot {
1168
+ display: flex; justify-content: space-between; align-items: center;
1169
+ padding: 10px 18px 14px;
1170
+ border-top: 1px dashed var(--hairline);
1171
+ gap: 14px;
1172
+ flex-wrap: wrap;
1173
+ }
1174
+ .cv-legend {
1175
+ display: flex; align-items: center; gap: 8px;
1176
+ font-size: 11px; color: var(--ink-3);
1177
+ }
1178
+ .cv-legend-label { font-family: var(--mono); font-size: 10.5px; }
1179
+ .cv-legend-swatch {
1180
+ width: 140px; height: 8px;
1181
+ border-radius: 4px;
1182
+ box-shadow: inset 0 0 0 1px var(--hairline);
1183
+ }
1184
+ .cv-legend-metric {
1185
+ font-family: var(--mono); font-size: 10.5px;
1186
+ color: var(--lavender-2);
1187
+ margin-left: 4px;
1188
+ }
1189
+ .cv-hint {
1190
+ font-size: 10.5px; letter-spacing: 0.04em;
1191
+ color: var(--ink-3); font-family: var(--mono);
1192
+ }
1193
+
1194
+ /* loading veil over the plot area */
1195
+ .cv-loading-veil {
1196
+ position: absolute; inset: 0;
1197
+ display: flex; flex-direction: column;
1198
+ align-items: center; justify-content: center;
1199
+ gap: 18px;
1200
+ background: linear-gradient(180deg, rgba(255,255,255,0.40), rgba(255,255,255,0.10));
1201
+ backdrop-filter: blur(2px);
1202
+ pointer-events: none;
1203
+ z-index: 2;
1204
+ }
1205
+ .cv-loading-pulse {
1206
+ width: 64px; height: 64px;
1207
+ border-radius: 50%;
1208
+ background:
1209
+ radial-gradient(circle at 30% 30%, #fff, var(--lavender) 50%, var(--rose) 100%);
1210
+ box-shadow: 0 0 40px rgba(201,184,230,0.45);
1211
+ animation: cv-pulse 2.2s ease-in-out infinite;
1212
+ }
1213
+ @keyframes cv-pulse {
1214
+ 0%, 100% { transform: scale(0.92); opacity: 0.85; }
1215
+ 50% { transform: scale(1.06); opacity: 1.0; }
1216
+ }
1217
+ .cv-loading-msg {
1218
+ font-family: var(--mono); font-size: 11.5px;
1219
+ color: var(--ink-2);
1220
+ background: rgba(255,255,255,0.7);
1221
+ padding: 6px 14px;
1222
+ border-radius: 999px;
1223
+ border: 1px solid var(--hairline);
1224
+ }
1225
+ .cv-loading-msg b { color: var(--rose-2); font-weight: 600; }
1226
+
1227
+ /* — sidebar / per-layer ledger — */
1228
+
1229
+ .cv-side {
1230
+ display: flex; flex-direction: column;
1231
+ gap: 14px;
1232
+ min-width: 0;
1233
+ }
1234
+ .cv-side-actions {
1235
+ display: flex; align-items: center; justify-content: space-between;
1236
+ gap: 10px;
1237
+ }
1238
+ .cv-toggle {
1239
+ display: inline-flex; align-items: center; gap: 9px;
1240
+ cursor: pointer;
1241
+ font-size: 11.5px; color: var(--ink-2);
1242
+ user-select: none;
1243
+ }
1244
+ .cv-toggle input { position: absolute; opacity: 0; pointer-events: none; }
1245
+ .cv-toggle-dot {
1246
+ width: 30px; height: 17px;
1247
+ border-radius: 999px;
1248
+ background: rgba(74,58,68,0.12);
1249
+ position: relative;
1250
+ transition: background 180ms ease;
1251
+ }
1252
+ .cv-toggle-dot::after {
1253
+ content: '';
1254
+ position: absolute; top: 2px; left: 2px;
1255
+ width: 13px; height: 13px;
1256
+ border-radius: 50%;
1257
+ background: #fff;
1258
+ box-shadow: 0 1px 2px rgba(74,58,68,0.25);
1259
+ transition: transform 180ms cubic-bezier(.4,0,.2,1);
1260
+ }
1261
+ .cv-toggle input:checked + .cv-toggle-dot {
1262
+ background: linear-gradient(180deg, var(--rose), var(--rose-2));
1263
+ }
1264
+ .cv-toggle input:checked + .cv-toggle-dot::after { transform: translateX(13px); }
1265
+ .cv-toggle-label { font-family: var(--sans); letter-spacing: 0.01em; }
1266
+
1267
+ .cv-camera-reset {
1268
+ appearance: none;
1269
+ background: rgba(255,255,255,0.6);
1270
+ border: 1px solid var(--hairline);
1271
+ border-radius: 999px;
1272
+ padding: 5px 11px 6px;
1273
+ font-family: var(--mono); font-size: 10.5px;
1274
+ color: var(--ink-2); letter-spacing: 0.04em;
1275
+ cursor: pointer;
1276
+ transition: all 140ms ease;
1277
+ }
1278
+ .cv-camera-reset:hover {
1279
+ background: rgba(201,184,230,0.20);
1280
+ border-color: var(--lavender-2);
1281
+ color: var(--ink);
1282
+ }
1283
+
1284
+ .cv-side-head { display: flex; flex-direction: column; gap: 2px; padding-top: 4px; }
1285
+ .cv-side-eyebrow {
1286
+ font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase;
1287
+ color: var(--lavender-2); font-weight: 600;
1288
+ }
1289
+ .cv-side-sub {
1290
+ font-family: var(--serif); font-style: italic;
1291
+ font-size: 13px; color: var(--ink-2);
1292
+ }
1293
+
1294
+ /* table */
1295
+ .cv-table {
1296
+ display: flex; flex-direction: column;
1297
+ background: rgba(255,255,255,0.55);
1298
+ border: 1px solid var(--hairline);
1299
+ border-radius: var(--radius);
1300
+ overflow: hidden;
1301
+ box-shadow: var(--shadow-soft);
1302
+ }
1303
+ .cv-row {
1304
+ display: grid;
1305
+ grid-template-columns: 28px 64px 1fr 1fr 1fr 1fr;
1306
+ align-items: center;
1307
+ gap: 8px;
1308
+ padding: 8px 12px;
1309
+ font-size: 11.5px;
1310
+ color: var(--ink-2);
1311
+ border-top: 1px solid var(--hairline);
1312
+ transition: background 120ms ease;
1313
+ }
1314
+ .cv-row:first-child { border-top: none; }
1315
+ .cv-row-head {
1316
+ background: var(--bg-2);
1317
+ color: var(--ink-3);
1318
+ font-size: 9.5px;
1319
+ letter-spacing: 0.14em;
1320
+ text-transform: uppercase;
1321
+ font-weight: 600;
1322
+ padding-top: 9px; padding-bottom: 9px;
1323
+ }
1324
+ .cv-row-empty {
1325
+ padding: 14px 12px; color: var(--ink-3); font-family: var(--mono); font-size: 11px;
1326
+ grid-template-columns: 1fr;
1327
+ }
1328
+ .cv-row.is-rolling { background: linear-gradient(180deg, rgba(201,184,230,0.10), rgba(201,184,230,0.04)); }
1329
+ .cv-row.is-highlight {
1330
+ background: linear-gradient(180deg, rgba(236,181,190,0.20), rgba(236,181,190,0.08));
1331
+ box-shadow: inset 3px 0 0 var(--rose-2);
1332
+ }
1333
+ .cv-row:hover:not(.cv-row-head):not(.cv-row-empty) { background: rgba(201,184,230,0.08); }
1334
+
1335
+ .cv-layer {
1336
+ font-family: var(--serif); font-size: 14px; color: var(--ink);
1337
+ font-weight: 500;
1338
+ }
1339
+ .cv-src {
1340
+ display: inline-flex; align-items: center; gap: 6px;
1341
+ font-family: var(--mono); font-size: 10px;
1342
+ color: var(--ink-2);
1343
+ }
1344
+ .cv-src-dot {
1345
+ width: 6px; height: 6px; border-radius: 50%;
1346
+ flex: 0 0 auto;
1347
+ }
1348
+ .cv-src-dot.rolling { background: var(--rose-2); box-shadow: 0 0 0 2px rgba(207,141,150,0.20); }
1349
+ .cv-src-dot.stable { background: var(--lavender-2); box-shadow: 0 0 0 2px rgba(154,135,207,0.20); }
1350
+ .cv-src-name { letter-spacing: 0.04em; }
1351
+ .cv-num { text-align: right; font-size: 11.5px; color: var(--ink); }
1352
+ .cv-num.mono { font-size: 11px; }
1353
+
1354
+ /* ghost-run info card */
1355
+ .cv-ghost-card {
1356
+ background:
1357
+ linear-gradient(160deg, rgba(236,181,190,0.18), rgba(244,217,158,0.10));
1358
+ border: 1px solid var(--hairline);
1359
+ border-radius: var(--radius);
1360
+ padding: 12px 14px 13px;
1361
+ display: flex; flex-direction: column; gap: 10px;
1362
+ }
1363
+ .cv-ghost-eyebrow {
1364
+ font-size: 9.5px; letter-spacing: 0.22em; text-transform: uppercase;
1365
+ color: var(--rose-2); font-weight: 600;
1366
+ }
1367
+ .cv-ghost-rows { display: flex; flex-direction: column; gap: 6px; }
1368
+ .cv-ghost-row {
1369
+ display: grid;
1370
+ grid-template-columns: 78px 1fr auto;
1371
+ align-items: center;
1372
+ gap: 8px;
1373
+ font-size: 11px;
1374
+ color: var(--ink-2);
1375
+ }
1376
+ .cv-ghost-layer {
1377
+ font-family: var(--serif); font-size: 12.5px;
1378
+ color: var(--ink);
1379
+ }
1380
+ .cv-ghost-stats { font-size: 10.5px; color: var(--ink-2); }
1381
+ .cv-ghost-tag {
1382
+ font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.04em;
1383
+ padding: 2px 7px; border-radius: 999px;
1384
+ font-weight: 500;
1385
+ }
1386
+ .cv-ghost-tag.tone-rose { background: rgba(207,141,150,0.30); color: #8a4756; }
1387
+ .cv-ghost-tag.tone-amber { background: rgba(240,201,138,0.40); color: #8a6a32; }
1388
+ .cv-ghost-note {
1389
+ font-size: 11px; line-height: 1.55;
1390
+ color: var(--ink-3);
1391
+ padding-top: 6px;
1392
+ border-top: 1px dashed var(--hairline);
1393
+ }
1394
+ .cv-ghost-note b { color: var(--ink); font-weight: 600; }
1395
+
1396
+ /* — scrubber — */
1397
+
1398
+ .cv-scrub {
1399
+ display: grid;
1400
+ grid-template-columns: auto 1fr auto auto;
1401
+ align-items: center;
1402
+ gap: 16px;
1403
+ padding: 14px 18px;
1404
+ background:
1405
+ linear-gradient(180deg, rgba(255,255,255,0.55), rgba(255,255,255,0.30));
1406
+ border: 1px solid var(--hairline);
1407
+ border-radius: var(--radius);
1408
+ box-shadow: var(--shadow-soft);
1409
+ }
1410
+ .cv-play {
1411
+ appearance: none;
1412
+ width: 38px; height: 38px;
1413
+ border-radius: 50%;
1414
+ border: 1px solid var(--lavender-2);
1415
+ background: linear-gradient(180deg, var(--lavender), var(--lavender-2));
1416
+ color: #fff;
1417
+ cursor: pointer;
1418
+ display: flex; align-items: center; justify-content: center;
1419
+ box-shadow: 0 4px 12px rgba(154,135,207,0.30), inset 0 1px 0 rgba(255,255,255,0.5);
1420
+ transition: transform 120ms ease, box-shadow 140ms ease;
1421
+ }
1422
+ .cv-play:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(154,135,207,0.45), inset 0 1px 0 rgba(255,255,255,0.5); }
1423
+ .cv-play.is-playing { background: linear-gradient(180deg, var(--rose), var(--rose-2)); border-color: var(--rose-2); box-shadow: 0 4px 12px rgba(207,141,150,0.40), inset 0 1px 0 rgba(255,255,255,0.5); }
1424
+ .cv-play-glyph {
1425
+ font-size: 12px;
1426
+ letter-spacing: 0;
1427
+ line-height: 1;
1428
+ margin-left: 2px; /* optical alignment of the play triangle */
1429
+ }
1430
+ .cv-play.is-playing .cv-play-glyph { margin-left: 0; font-size: 11px; }
1431
+
1432
+ .cv-range-wrap { position: relative; padding: 4px 4px 0; min-width: 0; }
1433
+ .cv-range-track {
1434
+ height: 8px; border-radius: 999px;
1435
+ background: rgba(74,58,68,0.08);
1436
+ overflow: hidden;
1437
+ position: relative;
1438
+ }
1439
+ .cv-range-fill {
1440
+ height: 100%;
1441
+ background: linear-gradient(90deg, var(--lavender), var(--rose), var(--coral), var(--gold));
1442
+ border-radius: 999px;
1443
+ box-shadow: 0 0 14px rgba(245,185,163,0.40);
1444
+ transition: width 80ms linear;
1445
+ }
1446
+ .cv-range {
1447
+ position: absolute; inset: 0;
1448
+ appearance: none;
1449
+ width: 100%; height: 100%;
1450
+ background: transparent;
1451
+ margin: 0; padding: 0;
1452
+ cursor: pointer;
1453
+ }
1454
+ .cv-range::-webkit-slider-thumb {
1455
+ appearance: none;
1456
+ width: 18px; height: 18px;
1457
+ border-radius: 50%;
1458
+ background: #fff;
1459
+ border: 2px solid var(--rose-2);
1460
+ box-shadow: 0 2px 6px rgba(74,58,68,0.22);
1461
+ cursor: grab;
1462
+ transition: transform 120ms ease;
1463
+ }
1464
+ .cv-range::-webkit-slider-thumb:active { transform: scale(1.12); cursor: grabbing; }
1465
+ .cv-range::-moz-range-thumb {
1466
+ width: 18px; height: 18px;
1467
+ border-radius: 50%;
1468
+ background: #fff;
1469
+ border: 2px solid var(--rose-2);
1470
+ box-shadow: 0 2px 6px rgba(74,58,68,0.22);
1471
+ cursor: grab;
1472
+ }
1473
+ .cv-range-ticks {
1474
+ display: flex; justify-content: space-between;
1475
+ font-family: var(--mono); font-size: 9.5px;
1476
+ color: var(--ink-3);
1477
+ padding: 6px 2px 0;
1478
+ letter-spacing: 0.04em;
1479
+ }
1480
+
1481
+ .cv-scrub-readout {
1482
+ display: flex; flex-direction: column; align-items: flex-end;
1483
+ line-height: 1.0;
1484
+ }
1485
+ .cv-scrub-num {
1486
+ font-family: var(--serif); font-weight: 500;
1487
+ font-size: 20px; color: var(--ink);
1488
+ letter-spacing: -0.01em;
1489
+ }
1490
+ .cv-scrub-label {
1491
+ font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase;
1492
+ color: var(--ink-3); margin-top: 4px;
1493
+ }
1494
+ .cv-scrub-reset {
1495
+ appearance: none;
1496
+ background: rgba(255,255,255,0.6);
1497
+ border: 1px solid var(--hairline);
1498
+ border-radius: 999px;
1499
+ padding: 6px 12px 7px;
1500
+ font-family: var(--mono); font-size: 10.5px;
1501
+ color: var(--ink-2);
1502
+ cursor: pointer;
1503
+ letter-spacing: 0.04em;
1504
+ transition: all 140ms ease;
1505
+ }
1506
+ .cv-scrub-reset:hover {
1507
+ background: rgba(201,184,230,0.18);
1508
+ border-color: var(--lavender-2);
1509
+ color: var(--ink);
1510
+ }
1511
+
1512
  /* === Responsive === */
1513
  @media (max-width: 1180px) {
1514
  .hero-band { grid-template-columns: 1fr; }
 
1517
  .ld-comps { grid-template-columns: repeat(2, 1fr); }
1518
  .bf { min-height: auto; }
1519
  .hm-card { min-height: 620px; }
1520
+ .cv-body { grid-template-columns: 1fr; }
1521
+ .cv-stage { min-height: 460px; }
1522
+ .cv-plot { min-height: 420px; }
1523
+ }
1524
+ @media (max-width: 760px) {
1525
+ .cv-scrub { grid-template-columns: auto 1fr; row-gap: 10px; }
1526
+ .cv-scrub-readout, .cv-scrub-reset { grid-column: 1 / -1; }
1527
+ .cv-scrub-readout { flex-direction: row; gap: 10px; align-items: baseline; }
1528
  }