Land Claude Design 3D convergence valley + polished UI pass
Browse filesReplaces 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 +14 -5
- components/ConvergenceValley.jsx +612 -0
- components/DataSource.jsx +13 -25
- components/Heatmap.jsx +1 -1
- components/Landing.jsx +5 -5
- components/LayerDeepDive.jsx +3 -3
- components/SAEDebugPanel.jsx +0 -90
- components/SQLBox.jsx +11 -26
- components/SurgicalTargets.jsx +1 -1
- index.html +1 -1
- loader.js +18 -62
- sae-loader.js +12 -2
- styles.css +476 -2
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 & 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
|
| 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 |
-
|
| 36 |
-
<div className="ds
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|
| 92 |
</div>
|
| 93 |
-
</div>
|
| 94 |
-
|
| 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>{
|
| 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="
|
| 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>
|
| 72 |
-
tag="
|
| 73 |
-
icon={<
|
| 74 |
/>
|
| 75 |
<Step
|
| 76 |
n="iii"
|
|
@@ -199,7 +199,7 @@ function IconCensus() {
|
|
| 199 |
);
|
| 200 |
}
|
| 201 |
|
| 202 |
-
function
|
| 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
|
| 98 |
{feats.length === 0 ? (
|
| 99 |
<div className="ld-empty muted">
|
| 100 |
-
No
|
| 101 |
-
(
|
| 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 (
|
| 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 |
-
|
| 68 |
-
|
| 69 |
-
|
| 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
|
| 78 |
}
|
| 79 |
|
| 80 |
function round(v, p) {
|
|
@@ -148,22 +137,18 @@ function parseAndRun(sql, tables) {
|
|
| 148 |
}
|
| 149 |
|
| 150 |
const EXAMPLES = [
|
| 151 |
-
{ label: 'sharpest
|
| 152 |
sql: "SELECT layer, component, feature, fstat, delta\nFROM features\nWHERE leaning = 'corp'\nORDER BY fstat DESC\nLIMIT 20" },
|
| 153 |
-
{ label: '
|
| 154 |
-
sql: "SELECT layer,
|
| 155 |
-
{ label: 'sharpest
|
| 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: '
|
| 158 |
-
sql: "SELECT layer, component,
|
| 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
|
| 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/
|
| 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
|
| 8 |
-
const
|
| 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
|
| 13 |
-
const SIZE
|
| 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
|
| 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
|
| 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 |
-
|
| 83 |
-
|
| 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
|
| 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
|
| 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
|
| 396 |
-
fetchAllRows('per_head',
|
| 397 |
-
fetchAllRows('bouncer_analysis',
|
| 398 |
-
fetchAllRows('prompts',
|
| 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
|
| 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 |
-
|
|
|
|
| 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
|
| 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 |
}
|