- multi_model_metrics: publish gbt / mlp / cnn / knn_semi / gru / lstm / bert (knn handled by knn streamer); read both *_train.json and *_eval.json with macro_f1.point fallback - dashboard.css: add palette gradients for the four non-canonical names so the bars render with a fill colour - dashboard.js: open the bar's visible scale to the full 0–1 range so honest-low cross-host F1s show as a bar instead of clamping to 0% - ship lambda-live-detection-loop.py + dashboard request docs (scenes 7/8/12, sticky cache, lambda-inference-demo) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1510 lines
57 KiB
CSS
1510 lines
57 KiB
CSS
:root {
|
||
color-scheme: dark;
|
||
/* Theme machinery overrides --c1..c5, --accent, --accent-soft via
|
||
JS. The fallback values below match the previous hard-coded
|
||
palette so the page looks identical on a fresh load. */
|
||
--theme-l: 70%;
|
||
--theme-c: 0.15;
|
||
--theme-h: 250;
|
||
--c1: oklch(70% 0.15 250);
|
||
--c2: oklch(70% 0.15 220);
|
||
--c3: oklch(70% 0.15 280);
|
||
--c4: oklch(70% 0.15 340);
|
||
--c5: oklch(70% 0.15 100);
|
||
--accent: var(--c1);
|
||
--accent-soft: oklch(70% 0.15 250 / 0.15);
|
||
|
||
--bg: #07090d;
|
||
--bg-rgb: 7, 9, 13;
|
||
--bg-elev: #0d1117;
|
||
--bg-elev2: #161b22;
|
||
|
||
/* Text & line greys, theme-aware with a discontinuous jump.
|
||
|
||
A *linear* response of grey-L to theme-L is too easy to push
|
||
into unreadable territory at the slider extremes. So the L
|
||
dependence is a STEP at theme-L = 50%: clamp(0, (L - 50) ×
|
||
1000, 1) gives 0 below 50 and 1 above 50 with a vanishingly
|
||
small transition zone — effectively a step function expressed
|
||
in pure CSS. Both step values are inside their grey's safe
|
||
contrast band. Chroma tint stays linear (high C → more hue
|
||
in the greys) since chroma doesn't threaten contrast. Falls
|
||
back to mid-band values if --theme-l-num isn't set (briefly
|
||
during initial load before JS runs).
|
||
|
||
The step landing zones are calibrated so:
|
||
theme-L < 50 → cooler / dimmer greys (suits darker accents)
|
||
theme-L ≥ 50 → brighter / fuller greys (suits brighter accents)
|
||
and the difference is small enough not to wreck the design,
|
||
yet large enough that you SEE the click as you cross 50%. */
|
||
--l-step: clamp(0, calc((var(--theme-l-num, 70) - 50) * 1000), 1);
|
||
--c-tint: calc(var(--theme-c, 0.15) * 0.18);
|
||
|
||
--fg: oklch(
|
||
calc(89% + var(--l-step) * 5%)
|
||
var(--c-tint)
|
||
var(--theme-h, 250));
|
||
--fg-dim: oklch(
|
||
calc(56% + var(--l-step) * 9%)
|
||
calc(var(--c-tint) * 1.2)
|
||
var(--theme-h, 250));
|
||
--fg-mute: oklch(
|
||
calc(33% + var(--l-step) * 7%)
|
||
calc(var(--c-tint) * 0.9)
|
||
var(--theme-h, 250));
|
||
--line: oklch(
|
||
calc(16% + var(--l-step) * 4%)
|
||
calc(var(--c-tint) * 0.4)
|
||
var(--theme-h, 250));
|
||
--line-soft: oklch(
|
||
calc(20% + var(--l-step) * 4%)
|
||
calc(var(--c-tint) * 0.5)
|
||
var(--theme-h, 250));
|
||
--accent: #58a6ff;
|
||
--accent-soft: rgba(88, 166, 255, 0.15);
|
||
--warn: #f85149;
|
||
--ok: #3fb950;
|
||
--phase-clean: #3fb950;
|
||
--phase-armed: #d29922;
|
||
--phase-infecting: #db61a2;
|
||
--phase-running: #f85149;
|
||
--phase-dormant: #6e7681;
|
||
--topbar-h: 44px;
|
||
--prose-w: 36em;
|
||
--scene-fade-ms: 600ms;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
html, body {
|
||
margin: 0; padding: 0; background: var(--bg); color: var(--fg);
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||
-webkit-font-smoothing: antialiased; overflow-x: hidden;
|
||
}
|
||
|
||
/* ─── Background canvas (theme-driven) ─────────────────────────────── */
|
||
/* Animation timings respect --anim-speed (multiplier; 1 = nominal,
|
||
2 = twice as fast). Whole canvas can also be blurred via
|
||
--bg-blur. Both are set by the theme panel sliders. */
|
||
/* Minimal positioning, no layer-promotion tricks. Every stacking-
|
||
context-creating property here (filter, transform, isolation,
|
||
contain) caused some downstream compositor artifact in earlier
|
||
iterations. The browser will GPU-promote on its own when needed.
|
||
`filter` is only applied when the bg-blur slider is non-zero —
|
||
the JS sets `--bg-filter` to `blur(Npx)` then, and removes the
|
||
custom property when blur is 0 so the rule falls through to
|
||
`none`. */
|
||
.bg-canvas {
|
||
position: fixed; inset: 0; z-index: 0;
|
||
pointer-events: none; overflow: hidden;
|
||
background: var(--bg);
|
||
filter: var(--bg-filter, none);
|
||
overflow-anchor: none;
|
||
}
|
||
|
||
/* Subtle palette-tinted vignette — applies on every theme so even
|
||
"black" picks up palette character. Tint strength controlled by
|
||
--tint-strength via the panel. */
|
||
.bg-tint {
|
||
position: absolute; inset: 0; pointer-events: none;
|
||
background:
|
||
radial-gradient(ellipse 90% 70% at 50% 65%,
|
||
oklch(var(--theme-l) calc(var(--theme-c) * 0.6) var(--theme-h) / var(--tint-strength, 0.10)),
|
||
transparent 65%);
|
||
}
|
||
|
||
.bg-drift, .bg-lava, .bg-vaporwave, .bg-laser {
|
||
position: absolute; inset: 0; display: none;
|
||
}
|
||
body[data-theme="drift"] .bg-drift { display: block; }
|
||
body[data-theme="lava"] .bg-lava { display: block; }
|
||
body[data-theme="vaporwave"] .bg-vaporwave { display: block; }
|
||
body[data-theme="laser"] .bg-laser { display: block; }
|
||
|
||
/* drift — soft, blurred radial blobs floating up and back down.
|
||
The blobs are JS-generated; styling here uses theme variables
|
||
(--drift-blur, --drift-opacity) so live slider changes don't
|
||
need a DOM rebuild. */
|
||
.drift-blob {
|
||
position: absolute; border-radius: 50%;
|
||
filter: blur(var(--drift-blur, 70px));
|
||
opacity: var(--drift-opacity, 0.55);
|
||
mix-blend-mode: screen;
|
||
}
|
||
@keyframes drift-rise {
|
||
0%, 100% { transform: translateY(0) scale(1); }
|
||
50% { transform: translateY(-130vh) scale(1.4); }
|
||
}
|
||
|
||
/* lava — metaballs via SVG goo filter. The filter values
|
||
(stdDeviation, alpha-threshold matrix entries) are updated by
|
||
JS as the goo strength / blur sliders move. Bubbles are
|
||
JS-generated so their statistical-distribution sliders work. */
|
||
.goo-container { position: absolute; inset: 0; filter: url(#goo); }
|
||
.goo-bubble { position: absolute; border-radius: 50%; opacity: 0.95; }
|
||
@keyframes goo-rise {
|
||
0% { transform: translateY(0) scale(0.9); }
|
||
45% { transform: translateY(-65vh) scale(1.05); }
|
||
55% { transform: translateY(-75vh) scale(1.1); }
|
||
100% { transform: translateY(-130vh) scale(0.85); }
|
||
}
|
||
|
||
/* ─── Vaporwave (overhauled) ───────────────────────────────────────
|
||
Layered: gradient sky → palette-blended sun with venetian-blind
|
||
stripes → glowing horizon → perspective grid floor → CRT scanlines.
|
||
Knobs (CSS vars set by sliders): --vw-horizon, --vw-grid-size,
|
||
--vw-perspective, --vw-sun-size, --vw-blind. */
|
||
.bg-vaporwave {
|
||
position: absolute; inset: 0;
|
||
background: radial-gradient(
|
||
ellipse 100% 70% at 50% var(--vw-horizon, 55%),
|
||
transparent 0%, transparent 60%,
|
||
rgba(0, 0, 0, 0.6) 100%);
|
||
}
|
||
.vw-sky {
|
||
position: absolute; left: 0; right: 0; top: 0;
|
||
height: var(--vw-horizon, 55%);
|
||
background: linear-gradient(
|
||
to bottom,
|
||
oklch(15% 0.20 var(--theme-h, 250)) 0%,
|
||
var(--c1) 60%,
|
||
var(--c2) 100%);
|
||
}
|
||
.vw-sun {
|
||
position: absolute; left: 50%;
|
||
top: var(--vw-horizon, 55%);
|
||
width: var(--vw-sun-size, 50vmin);
|
||
height: var(--vw-sun-size, 50vmin);
|
||
transform: translate(-50%, -78%);
|
||
border-radius: 50%;
|
||
background: linear-gradient(
|
||
to bottom,
|
||
var(--c3) 0%,
|
||
var(--c4) 50%,
|
||
var(--c5) 100%);
|
||
overflow: hidden;
|
||
box-shadow: 0 0 80px var(--c4), 0 0 160px var(--c3);
|
||
}
|
||
/* Venetian-blind stripes on the lower 50% — black-bg occluders, so
|
||
the sun's gradient peeks through the gaps. */
|
||
.vw-sun-blinds {
|
||
position: absolute; left: 0; right: 0; bottom: 0;
|
||
height: 50%;
|
||
background: repeating-linear-gradient(
|
||
to bottom,
|
||
transparent 0 calc(var(--vw-blind, 11px) - 4px),
|
||
rgba(0, 0, 0, 0.92) calc(var(--vw-blind, 11px) - 4px) var(--vw-blind, 11px));
|
||
}
|
||
.vw-horizon {
|
||
position: absolute; left: 0; right: 0;
|
||
top: var(--vw-horizon, 55%);
|
||
height: 1px; background: var(--c1);
|
||
box-shadow:
|
||
0 0 20px var(--c1),
|
||
0 0 40px var(--c1),
|
||
0 0 80px var(--c2);
|
||
}
|
||
/* The vaporwave floor is a <canvas> drawn frame-by-frame in JS via
|
||
requestAnimationFrame. Two earlier CSS-only attempts (perspective +
|
||
rotateX with animated background-position; then split rotate +
|
||
translate3d on a repeating-linear-gradient) both flickered when
|
||
the user scrolled — the perspective math kept getting recomputed
|
||
in lockstep with document scroll, even with isolation: isolate
|
||
and contain: paint. The canvas approach side-steps that entirely:
|
||
no perspective transform, lines drawn at exact pixel positions
|
||
each frame, palette colors read from CSS custom properties. */
|
||
/* CSS perspective grid. Three layered elements so rotate (static)
|
||
and translate (animated) live on different layers — animating
|
||
transform on the rotated element directly was what re-rasterized
|
||
per frame in earlier attempts. Each element is also pushed into
|
||
its own compositor layer to keep the document scroll from
|
||
dirtying the bg. */
|
||
/* Collapsed structure: one perspective container, one animated grid
|
||
element. The rotation and translation live on the SAME transform
|
||
on the SAME element — no nested transform-style: preserve-3d, no
|
||
separately-promoted compositor layer for the translate. The
|
||
compositor has nothing to disagree about, so the phantom-orthogonal
|
||
3D-grid post artifact disappears. */
|
||
.vw-floor {
|
||
position: absolute; left: -50%; right: -50%;
|
||
top: var(--vw-horizon, 55%); bottom: -10%;
|
||
perspective: 800px;
|
||
overflow: hidden;
|
||
}
|
||
.vw-floor-grid {
|
||
position: absolute; left: 0; right: 0; top: 0; height: 200%;
|
||
background-image:
|
||
repeating-linear-gradient(
|
||
to bottom,
|
||
transparent 0 calc(var(--vw-grid-size, 80px) - 3px),
|
||
var(--c1) calc(var(--vw-grid-size, 80px) - 3px) var(--vw-grid-size, 80px)),
|
||
repeating-linear-gradient(
|
||
to right,
|
||
transparent 0 calc(var(--vw-grid-size, 80px) - 3px),
|
||
var(--c2) calc(var(--vw-grid-size, 80px) - 3px) var(--vw-grid-size, 80px));
|
||
transform-origin: top center;
|
||
animation: vw-floor-y calc(4s / var(--anim-speed, 1)) linear infinite;
|
||
}
|
||
/* Both keyframes carry the same rotateX, so it stays static; only
|
||
translateY interpolates. The grid never leaves its rotated plane
|
||
because its rotation is part of the same transform list. */
|
||
@keyframes vw-floor-y {
|
||
from { transform: rotateX(var(--vw-perspective, 62deg)) translateY(0); }
|
||
to { transform: rotateX(var(--vw-perspective, 62deg))
|
||
translateY(var(--vw-grid-size, 80px)); }
|
||
}
|
||
/* Scanlines: confined to the sky above the horizon and stacked
|
||
below the sun (DOM-ordered between .vw-sky and .vw-sun) so the
|
||
sun's solid disc occludes them in its area. The 5 px period is
|
||
off-resonance with the floor's perspective grid (which is below
|
||
the horizon and never overlaps with these anyway) and with the
|
||
sun's blinds (which the sun itself hides). Plain alpha
|
||
compositing — no mix-blend-mode multiply that previously
|
||
created an isolation group flattening 3D content beneath. */
|
||
.vw-scanlines {
|
||
position: absolute; left: 0; right: 0; top: 0;
|
||
height: var(--vw-horizon, 55%);
|
||
pointer-events: none;
|
||
background: repeating-linear-gradient(
|
||
to bottom,
|
||
transparent 0 3px,
|
||
rgba(0, 0, 0, 0.14) 3px 5px);
|
||
}
|
||
|
||
|
||
/* laser show — long beams rotating from screen center, palette colors.
|
||
Beams JS-generated so count/thickness/blur sliders work. */
|
||
.laser-beam {
|
||
position: absolute; left: 50%; top: 50%;
|
||
height: var(--laser-thickness, 3px);
|
||
width: 200vmax; transform-origin: left center;
|
||
filter: blur(var(--laser-blur, 2px));
|
||
opacity: var(--laser-opacity, 0.55);
|
||
mix-blend-mode: screen;
|
||
}
|
||
@keyframes laser-sweep {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
/* ─── References scene (PDF viewer + tab strip + description) ──────── */
|
||
.ref-stack { /* metric-stack-wide variant; let content area fill height */
|
||
height: 100%;
|
||
justify-content: flex-start;
|
||
}
|
||
.ref-tabs {
|
||
display: flex; flex-wrap: wrap; gap: 6px;
|
||
max-height: clamp(60px, 9vh, 110px);
|
||
overflow-y: auto;
|
||
}
|
||
.ref-tabs .awaiting {
|
||
color: var(--fg-mute); font-size: 12px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
padding: 4px 0;
|
||
}
|
||
.ref-tab {
|
||
background: transparent; color: var(--fg-dim);
|
||
border: 1px solid var(--line); border-radius: 16px;
|
||
padding: 4px 12px; font: inherit; font-size: 12px; cursor: pointer;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
transition: color 120ms, border-color 120ms, background 120ms;
|
||
max-width: 28em;
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.ref-tab:hover { color: var(--fg); border-color: var(--fg-mute); }
|
||
.ref-tab.active {
|
||
color: var(--accent); border-color: var(--accent);
|
||
background: var(--accent-soft);
|
||
}
|
||
/* Two-column layout: PDF viewer on the left taking the larger
|
||
share, description panel on the right. The viewer's column
|
||
uses minmax(0, …) so the iframe won't blow out the grid when
|
||
the PDF reports a wide intrinsic size. */
|
||
.ref-content {
|
||
flex: 1 1 auto; min-height: 0;
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.55fr);
|
||
gap: 14px;
|
||
}
|
||
.ref-viewer-wrap {
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
}
|
||
.ref-viewer {
|
||
width: 100%; height: 100%;
|
||
min-height: clamp(360px, 70vh, 900px);
|
||
border: 0; display: block; background: var(--bg-elev);
|
||
}
|
||
.ref-description {
|
||
background: var(--bg-elev);
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
overflow-y: auto;
|
||
padding: 18px 22px;
|
||
font-size: 14px; line-height: 1.6;
|
||
color: var(--fg);
|
||
min-height: 0;
|
||
}
|
||
.ref-description h1, .ref-description h2 {
|
||
font-size: 15px; font-weight: 600; margin: 0 0 10px;
|
||
color: var(--fg);
|
||
}
|
||
.ref-description h3 { font-size: 13px; font-weight: 600; margin: 12px 0 4px; }
|
||
.ref-description p { margin: 0 0 10px; }
|
||
.ref-description ul,
|
||
.ref-description ol { margin: 0 0 10px; padding-left: 20px; }
|
||
.ref-description li { margin: 0 0 4px; }
|
||
.ref-description code {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 0.9em; color: var(--accent);
|
||
background: var(--accent-soft); padding: 1px 5px; border-radius: 3px;
|
||
}
|
||
.ref-description strong { color: var(--fg); font-weight: 600; }
|
||
.ref-description em { color: var(--fg-dim); font-style: italic; }
|
||
.ref-description .awaiting {
|
||
color: var(--fg-mute); font-style: italic;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* On narrow viewports stack vertically: PDF on top, description
|
||
below, capped to a sensible height so the PDF still gets room. */
|
||
@media (max-width: 1100px) {
|
||
.ref-content { grid-template-columns: 1fr; }
|
||
.ref-description { max-height: 240px; }
|
||
}
|
||
|
||
/* References scene wants more horizontal room than the default
|
||
metric scenes — the PDF is the point. Drop the right padding
|
||
that reserves space for the prose column. The prose for this
|
||
scene is hidden anyway (see below) so we can use the full width
|
||
for the PDF + description grid. */
|
||
.stage-view[data-view="references"] {
|
||
padding-right: clamp(8px, 2vw, 48px);
|
||
}
|
||
/* Hide the prose card on the references scene — the description
|
||
panel inside the metric-stack already explains each PDF in
|
||
context, and freeing the right-side viewport gives the
|
||
description panel proper room. */
|
||
.scene[data-stage="references"] .prose { display: none; }
|
||
|
||
/* ─── Per-theme settings section ───────────────────────────────────── */
|
||
.theme-bg-section { display: none; }
|
||
.theme-bg-section.is-active { display: block; }
|
||
|
||
/* Continuous-harmony hint line */
|
||
.theme-harmony-hint {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px; color: var(--fg-mute); padding: 2px 0;
|
||
}
|
||
|
||
/* Theme-panel advanced section accordion */
|
||
.theme-advanced { border-top: 1px solid var(--line); padding-top: 6px; }
|
||
.theme-advanced > summary {
|
||
cursor: pointer; color: var(--fg-dim);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px; letter-spacing: 0.04em; padding: 4px 0;
|
||
list-style: none;
|
||
}
|
||
.theme-advanced > summary::-webkit-details-marker { display: none; }
|
||
.theme-advanced > summary::before { content: '▸ '; display: inline-block; transition: transform 150ms; }
|
||
.theme-advanced[open] > summary::before { transform: rotate(90deg); }
|
||
.theme-advanced > .theme-sliders { padding-top: 6px; }
|
||
|
||
/* ─── Theme panel ──────────────────────────────────────────────────── */
|
||
/* Right-half sidebar that slides in from the right when open.
|
||
Always rendered (no hidden attribute) so the transform animation
|
||
works; visibility gated by the .is-open class. Pointer-events
|
||
are turned off when closed so the hidden panel doesn't intercept
|
||
clicks on the page beneath. */
|
||
.theme-panel {
|
||
position: fixed;
|
||
top: var(--topbar-h); right: 0; bottom: 0;
|
||
width: 50vw;
|
||
z-index: 60;
|
||
background: rgba(13, 17, 23, 0.95);
|
||
backdrop-filter: blur(12px);
|
||
border-left: 1px solid var(--line);
|
||
padding: 20px 28px 28px;
|
||
color: var(--fg); font-size: 12px;
|
||
display: flex; flex-direction: column; gap: 14px;
|
||
overflow-y: auto;
|
||
box-shadow: -20px 0 60px rgba(0, 0, 0, 0.5);
|
||
transform: translateX(100%);
|
||
transition: transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
||
pointer-events: none;
|
||
}
|
||
.theme-panel.is-open {
|
||
transform: translateX(0);
|
||
pointer-events: auto;
|
||
}
|
||
@media (max-width: 880px) {
|
||
.theme-panel { width: 100vw; }
|
||
}
|
||
.theme-panel-header {
|
||
position: sticky; top: 0;
|
||
display: flex; align-items: center; gap: 8px;
|
||
background: inherit;
|
||
padding-bottom: 10px;
|
||
margin: -20px -28px 0;
|
||
padding: 14px 28px;
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
.theme-panel-header .theme-title { flex: 1; font-weight: 600; letter-spacing: 0.04em; }
|
||
.theme-panel select, .theme-panel input[type="range"] {
|
||
background: var(--bg-elev); color: var(--fg);
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
font: inherit; font-size: 12px;
|
||
}
|
||
.theme-panel select { padding: 4px 8px; }
|
||
.theme-row {
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.theme-row > span:first-child {
|
||
flex: 0 0 90px; color: var(--fg-dim);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px; letter-spacing: 0.04em; text-transform: lowercase;
|
||
}
|
||
.theme-row > select { flex: 1; }
|
||
|
||
.theme-wheel-block {
|
||
display: grid; grid-template-columns: 200px 1fr; gap: 14px; align-items: center;
|
||
}
|
||
.theme-wheel {
|
||
position: relative; width: 200px; height: 200px;
|
||
user-select: none; touch-action: none;
|
||
}
|
||
.wheel-disc {
|
||
position: absolute; inset: 0; border-radius: 50%;
|
||
/* Conic starts from 0deg = 12 o'clock (top). Was previously
|
||
`from -90deg` which puts the gradient origin at 9 o'clock —
|
||
a 90° offset from where the JS marker code expects it
|
||
(positionMarker uses (H - 90)°, so H=0 lands at the top). */
|
||
background: conic-gradient(
|
||
from 0deg,
|
||
oklch(var(--theme-l) var(--theme-c) 0),
|
||
oklch(var(--theme-l) var(--theme-c) 60),
|
||
oklch(var(--theme-l) var(--theme-c) 120),
|
||
oklch(var(--theme-l) var(--theme-c) 180),
|
||
oklch(var(--theme-l) var(--theme-c) 240),
|
||
oklch(var(--theme-l) var(--theme-c) 300),
|
||
oklch(var(--theme-l) var(--theme-c) 360)
|
||
);
|
||
}
|
||
.wheel-rim {
|
||
position: absolute; inset: 25%; border-radius: 50%;
|
||
background: var(--bg-elev); border: 1px solid var(--line);
|
||
}
|
||
.wheel-markers { position: absolute; inset: 0; pointer-events: none; }
|
||
.wheel-marker {
|
||
position: absolute; width: 18px; height: 18px;
|
||
border-radius: 50%; border: 2px solid #fff;
|
||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
|
||
transform: translate(-50%, -50%); cursor: grab;
|
||
pointer-events: auto; touch-action: none;
|
||
}
|
||
.wheel-marker.primary {
|
||
width: 24px; height: 24px;
|
||
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.6), 0 0 12px rgba(255, 255, 255, 0.3);
|
||
}
|
||
.wheel-marker:active, .wheel-marker.dragging { cursor: grabbing; }
|
||
|
||
.theme-sliders {
|
||
display: flex; flex-direction: column; gap: 8px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px; color: var(--fg-dim);
|
||
}
|
||
.theme-sliders label { display: flex; flex-direction: column; gap: 3px; }
|
||
.theme-sliders input[type="range"] { width: 100%; }
|
||
|
||
.theme-swatches {
|
||
flex: 1; display: flex; gap: 4px; height: 28px;
|
||
}
|
||
.theme-swatch {
|
||
flex: 1; border-radius: 3px; border: 1px solid var(--line);
|
||
}
|
||
|
||
.theme-meta-row {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
border-top: 1px solid var(--line); padding-top: 10px; margin-top: 2px;
|
||
}
|
||
.theme-meta-row code {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px; color: var(--accent);
|
||
background: var(--accent-soft); padding: 2px 6px; border-radius: 3px;
|
||
}
|
||
.theme-meta-row button.ghost {
|
||
background: transparent; color: var(--fg-dim); border: 1px solid var(--line);
|
||
font: inherit; font-size: 11px; padding: 3px 8px; border-radius: 3px; cursor: pointer;
|
||
}
|
||
.theme-meta-row button.ghost:hover { color: var(--fg); border-color: var(--fg-mute); }
|
||
|
||
/* ─── Topbar ───────────────────────────────────────────────────────── */
|
||
.topbar {
|
||
position: fixed; top: 0; left: 0; right: 0; height: var(--topbar-h); z-index: 50;
|
||
display: flex; align-items: center; gap: 10px; padding: 0 16px;
|
||
background: rgba(7, 9, 13, 0.85); backdrop-filter: blur(8px);
|
||
border-bottom: 1px solid var(--line); font-size: 12px;
|
||
}
|
||
.topbar .brand { font-weight: 700; letter-spacing: 0.04em; }
|
||
.topbar .spacer { flex: 1; }
|
||
.topbar .status { color: var(--fg-dim); }
|
||
.topbar .status.ok { color: var(--ok); }
|
||
.topbar .status.bad { color: var(--warn); }
|
||
.topbar .counter {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--fg-dim); font-variant-numeric: tabular-nums; padding: 0 6px;
|
||
}
|
||
.topbar button.ghost {
|
||
background: transparent; color: var(--fg-dim); border: 1px solid var(--line);
|
||
font: inherit; padding: 4px 10px; border-radius: 4px; cursor: pointer;
|
||
transition: color 120ms, border-color 120ms;
|
||
}
|
||
.topbar button.ghost:hover { color: var(--fg); border-color: var(--fg-mute); }
|
||
.topbar button.ghost:disabled { opacity: 0.35; cursor: not-allowed; }
|
||
.topbar button.ghost.icon { padding: 4px 8px; min-width: 28px; }
|
||
.topbar button.ghost.active { color: var(--accent); border-color: var(--accent); }
|
||
|
||
/* ─── Layout ───────────────────────────────────────────────────────── */
|
||
.layout { position: relative; }
|
||
.canvas-wrapper {
|
||
position: fixed;
|
||
top: var(--topbar-h); left: 0; right: 0;
|
||
height: calc(100vh - var(--topbar-h));
|
||
z-index: 1; overflow: hidden;
|
||
/* Layer-isolation properties (transform: translateZ, will-change,
|
||
contain: paint) were tried here during the scroll-flicker
|
||
investigation but caused glitchy cutout-mask artifacts on the
|
||
foreground stage views — paint containment plus a
|
||
GPU-promoted layer breaks compositor ordering of children's
|
||
opacity transitions. Layer promotion stays only on .bg-canvas,
|
||
which is enough to isolate bg from scroll. */
|
||
}
|
||
html, body { overflow-anchor: none; }
|
||
.article { overflow-anchor: none; }
|
||
.article {
|
||
position: relative; z-index: 2;
|
||
padding-top: var(--topbar-h); pointer-events: none;
|
||
}
|
||
.article .prose { pointer-events: auto; }
|
||
|
||
/* ─── Stage views ──────────────────────────────────────────────────── */
|
||
.stage { position: absolute; inset: 0; cursor: pointer; }
|
||
.stage-view {
|
||
position: absolute; inset: 0;
|
||
display: flex; align-items: center; justify-content: flex-start;
|
||
/* Reserve full prose-w of right-side space so the metric stack
|
||
ends exactly where the prose column starts (was prose-w - 1.5em,
|
||
which let prose's feathered left edge sit over interactive
|
||
widgets and block clicks). */
|
||
padding-right: clamp(0px, var(--prose-w), 42em);
|
||
/* No opacity transition: snapping scenes in/out instantly. The
|
||
transition was the source of the grid-shape artifact that
|
||
appeared over metric content during scene changes. While
|
||
stage-view's opacity was animating between 0 and 1, the
|
||
compositor sampled the perspective floor (bg-canvas's animated
|
||
grid) into the stage-view's intermediate bitmap, and the
|
||
grid pattern leaked into the metric content area for the
|
||
duration of the transition. Removing the transition removes
|
||
that compositor work entirely. */
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
.stage-view[data-active] { opacity: 1; pointer-events: auto; }
|
||
|
||
/* Intro stage */
|
||
.bg-grid {
|
||
position: absolute; inset: 0;
|
||
background-image:
|
||
linear-gradient(rgba(88,166,255,0.07) 1px, transparent 1px),
|
||
linear-gradient(90deg, rgba(88,166,255,0.07) 1px, transparent 1px);
|
||
background-size: 48px 48px;
|
||
mask-image: radial-gradient(ellipse at center, #000 0%, transparent 75%);
|
||
animation: drift 18s linear infinite;
|
||
}
|
||
/* DIAGNOSTIC: hide .bg-grid unconditionally (was previously hidden
|
||
only on non-black themes). The grid-shape artifact reported as
|
||
"appearing over metric content during scene transitions" matches
|
||
the intro scene's .bg-grid being visible-through-transparency
|
||
when both intro and the next scene's stage-view are
|
||
simultaneously partially-opaque during the IntersectionObserver-
|
||
driven cross-fade. .bg-grid is in the intro stage-view; the next
|
||
scene's metric-stack has a transparent background, so the grid
|
||
shows through during the transition window. Hiding it
|
||
unconditionally for now to verify, then we'll reintroduce it on
|
||
the black theme if it's actually wanted there. */
|
||
.bg-grid { display: none; }
|
||
@keyframes drift {
|
||
from { background-position: 0 0, 0 0; }
|
||
to { background-position: 48px 48px, 48px 48px; }
|
||
}
|
||
.intro-block {
|
||
position: relative; z-index: 1; text-align: left;
|
||
padding: 0 clamp(32px, 5vw, 80px); width: 100%;
|
||
}
|
||
.intro-eyebrow {
|
||
font-size: 12px; letter-spacing: 0.18em; text-transform: uppercase;
|
||
color: var(--fg-dim); margin-bottom: 18px;
|
||
}
|
||
.intro-title {
|
||
font-size: clamp(56px, 9vw, 168px); line-height: 0.95; font-weight: 700;
|
||
letter-spacing: -0.04em;
|
||
background: linear-gradient(180deg, var(--fg) 0%, var(--fg-dim) 100%);
|
||
-webkit-background-clip: text; background-clip: text; color: transparent;
|
||
}
|
||
|
||
/* ─── Metric stack — calculated, viewport-relative sizing ──────────── */
|
||
/* Wide-by-default. Inside a stage-view the right padding already
|
||
reserves room for prose, so width:100% means "use everything left
|
||
of the prose column." Backdrop is a horizontal gradient that
|
||
fades at both edges so the card dissolves into the bg instead
|
||
of meeting it with a hard rectangle. The right fade is wider
|
||
(78%→100%) than the left (0%→8%) because the right edge meets
|
||
the prose column's feathered left edge — letting the two
|
||
feathers overlap produces a continuous transition from
|
||
metric-card → bg → prose-card. */
|
||
.metric-stack {
|
||
text-align: left;
|
||
/* Asymmetric horizontal padding: less on the left (so the
|
||
interactive widgets sit further left, closer to the viewport
|
||
edge) and the existing larger value on the right (which holds
|
||
the gradient fade and the prose column behind it).
|
||
Vertical padding unchanged. */
|
||
padding:
|
||
clamp(20px, 2.5vh, 36px)
|
||
clamp(40px, 5vw, 88px)
|
||
clamp(20px, 2.5vh, 36px)
|
||
clamp(20px, 2.5vw, 48px);
|
||
width: 100%; max-width: none;
|
||
display: flex; flex-direction: column;
|
||
gap: clamp(10px, 1.4vh, 22px);
|
||
background: linear-gradient(
|
||
to right,
|
||
rgba(var(--bg-rgb), 0) 0%,
|
||
rgba(var(--bg-rgb), var(--content-backdrop, 0.30)) 8%,
|
||
rgba(var(--bg-rgb), var(--content-backdrop, 0.30)) 78%,
|
||
rgba(var(--bg-rgb), 0) 100%
|
||
);
|
||
}
|
||
.metric-stack-wide {
|
||
/* Slightly less right padding than other stages so the table can
|
||
stretch into the gradient zone of the prose column. */
|
||
padding-right: clamp(24px, 3vw, 56px);
|
||
}
|
||
.metric-eyebrow {
|
||
font-size: clamp(11px, 1vw, 14px);
|
||
letter-spacing: 0.18em; text-transform: uppercase; color: var(--fg-dim);
|
||
}
|
||
.metric-big {
|
||
font-size: clamp(72px, 13vw, 240px);
|
||
line-height: 0.95; font-weight: 700;
|
||
letter-spacing: -0.04em; font-variant-numeric: tabular-nums;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.metric-sub { color: var(--fg-dim); font-size: clamp(13px, 1vw, 16px);
|
||
line-height: 1.55; font-variant-numeric: tabular-nums; }
|
||
.metric-sub code {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 0.9em; color: var(--accent);
|
||
background: var(--accent-soft); padding: 1px 5px; border-radius: 3px;
|
||
}
|
||
|
||
.awaiting {
|
||
color: var(--fg-mute); font-size: clamp(12px, 0.95vw, 14px);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
padding: 16px 0; font-style: italic;
|
||
}
|
||
|
||
/* Sparkline */
|
||
.sparkline {
|
||
width: 100%; height: clamp(140px, 28vh, 360px); margin-top: 6px;
|
||
}
|
||
.sparkline path { fill: none; stroke: var(--accent); stroke-width: 1.5;
|
||
vector-effect: non-scaling-stroke; }
|
||
.sparkline #ingest-spark-fill { fill: var(--accent-soft); stroke: none; }
|
||
|
||
/* Per-host bars */
|
||
.bars { display: flex; flex-direction: column; gap: clamp(8px, 1.1vh, 14px); }
|
||
.bar-row {
|
||
display: grid;
|
||
grid-template-columns: minmax(140px, 18ch) 1fr minmax(72px, 10ch);
|
||
gap: clamp(10px, 1vw, 18px); align-items: center;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.bar-host { color: var(--fg); font-size: clamp(13px, 1vw, 15px);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||
.bar-track { height: clamp(24px, 3vh, 40px); background: var(--bg-elev);
|
||
border: 1px solid var(--line); border-radius: 3px; overflow: hidden; }
|
||
.bar-fill { height: 100%; background: var(--accent);
|
||
transition: width 600ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
||
.bar-count { color: var(--fg-dim); font-size: clamp(13px, 1vw, 15px); text-align: right;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||
|
||
/* Phase mix */
|
||
.phase-stack {
|
||
display: flex; height: clamp(48px, 7vh, 96px);
|
||
border-radius: 4px; overflow: hidden;
|
||
background: var(--bg-elev); border: 1px solid var(--line);
|
||
}
|
||
.phase-seg { transition: flex-grow 600ms ease; flex-grow: 0; min-width: 0; }
|
||
.phase-seg.clean { background: var(--phase-clean); }
|
||
.phase-seg.armed { background: var(--phase-armed); }
|
||
.phase-seg.infecting { background: var(--phase-infecting); }
|
||
.phase-seg.infected_running { background: var(--phase-running); }
|
||
.phase-seg.dormant { background: var(--phase-dormant); }
|
||
.phase-legend { display: flex; flex-wrap: wrap; gap: 14px;
|
||
font-size: 12px; color: var(--fg-dim);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||
.phase-legend > span { display: inline-flex; align-items: center; }
|
||
.phase-legend .swatch { display: inline-block; width: 10px; height: 10px;
|
||
border-radius: 2px; margin-right: 6px; }
|
||
|
||
/* ─── Code cards (stack scene) ─────────────────────────────────────── */
|
||
.code-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||
gap: clamp(12px, 1.4vw, 22px);
|
||
align-items: start;
|
||
}
|
||
.code-card {
|
||
background: var(--bg-elev2);
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: clamp(11px, 0.92vw, 14px);
|
||
line-height: 1.6;
|
||
}
|
||
.code-card-header {
|
||
padding: 7px 14px;
|
||
background: var(--bg-elev);
|
||
border-bottom: 1px solid var(--line);
|
||
color: var(--fg-dim);
|
||
font-size: 11px;
|
||
letter-spacing: 0.06em;
|
||
text-transform: lowercase;
|
||
}
|
||
.code-card pre.code {
|
||
margin: 0;
|
||
padding: 14px 18px;
|
||
white-space: pre;
|
||
overflow-x: auto;
|
||
color: var(--fg);
|
||
max-height: clamp(260px, 50vh, 560px);
|
||
}
|
||
/* Syntax-highlighting colors derive from the theme. Each token
|
||
type sits at a fixed hue offset from --theme-h so the relative
|
||
distinguishability between kw / str / fn / ty / num is preserved
|
||
regardless of where the user moves the H slider — they all
|
||
rotate together. Lightness fixed at 75% (readable on dark bg
|
||
without straining), chroma fixed at 0.18 (saturated enough that
|
||
the offsets read as distinct colors). Comments go through
|
||
--fg-mute so they pick up the L-step grey transitions. */
|
||
.code-card .kw { color: oklch(75% 0.18 calc(var(--theme-h, 250) + 0)); }
|
||
.code-card .str { color: oklch(75% 0.18 calc(var(--theme-h, 250) + 220)); }
|
||
.code-card .com { color: var(--fg-mute); font-style: italic; }
|
||
.code-card .fn { color: oklch(75% 0.18 calc(var(--theme-h, 250) + 280)); }
|
||
.code-card .ty { color: oklch(75% 0.18 calc(var(--theme-h, 250) + 40)); }
|
||
.code-card .num { color: oklch(75% 0.18 calc(var(--theme-h, 250) + 200)); }
|
||
|
||
/* ─── Database explorer ────────────────────────────────────────────── */
|
||
.db-header {
|
||
display: flex; align-items: baseline; gap: 16px; flex-wrap: wrap;
|
||
}
|
||
.db-count { color: var(--fg-mute); font-size: 12px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-variant-numeric: tabular-nums; }
|
||
.db-controls {
|
||
display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
|
||
}
|
||
.db-tabs { display: flex; gap: 6px; flex-wrap: wrap; }
|
||
.db-tab {
|
||
background: transparent; color: var(--fg-dim);
|
||
border: 1px solid var(--line); border-radius: 16px;
|
||
padding: 4px 12px; font: inherit; font-size: 12px; cursor: pointer;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
transition: color 120ms, border-color 120ms, background 120ms;
|
||
}
|
||
.db-tab:hover { color: var(--fg); border-color: var(--fg-mute); }
|
||
.db-tab.active { color: var(--accent); border-color: var(--accent);
|
||
background: var(--accent-soft); }
|
||
.db-search {
|
||
flex: 1; min-width: 200px;
|
||
background: var(--bg-elev); color: var(--fg);
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
padding: 6px 10px; font: inherit; font-size: 13px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.db-search:focus { outline: none; border-color: var(--accent); }
|
||
.db-table-wrap {
|
||
flex: 1 1 auto; min-height: 0;
|
||
max-height: clamp(280px, 56vh, 720px);
|
||
overflow: auto;
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
background: var(--bg-elev);
|
||
}
|
||
.db-table {
|
||
width: 100%; border-collapse: collapse;
|
||
font-size: clamp(12px, 0.92vw, 14px);
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.db-table thead th {
|
||
position: sticky; top: 0; z-index: 1;
|
||
background: var(--bg-elev2); color: var(--fg-dim);
|
||
text-align: left; padding: 8px 12px;
|
||
font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase;
|
||
font-weight: 500; border-bottom: 1px solid var(--line);
|
||
}
|
||
.db-table tbody tr {
|
||
border-bottom: 1px solid var(--line-soft); cursor: pointer;
|
||
transition: background 80ms;
|
||
}
|
||
.db-table tbody tr:hover { background: rgba(88, 166, 255, 0.06); }
|
||
.db-table tbody tr.selected { background: var(--accent-soft); }
|
||
.db-table td {
|
||
padding: 7px 12px; color: var(--fg);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.db-table td.db-size { color: var(--fg-dim); text-align: right; }
|
||
.db-host { color: var(--fg); }
|
||
.db-id { color: var(--fg-dim); }
|
||
.db-detail {
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
background: var(--bg-elev);
|
||
overflow: hidden;
|
||
display: flex; flex-direction: column;
|
||
}
|
||
.db-detail[hidden] { display: none; }
|
||
.db-detail-meta {
|
||
padding: 8px 14px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 12px; color: var(--fg-dim);
|
||
border-bottom: 1px solid var(--line-soft);
|
||
display: flex; gap: 14px; flex-wrap: wrap;
|
||
}
|
||
.db-detail-meta .db-id { color: var(--fg); }
|
||
.db-detail-chart-wrap {
|
||
background: var(--bg-elev2);
|
||
width: 100%;
|
||
position: relative;
|
||
}
|
||
.db-detail-chart {
|
||
display: block;
|
||
width: 100%;
|
||
height: clamp(220px, 32vh, 420px);
|
||
}
|
||
.db-detail-chart .axis { stroke: var(--line); stroke-width: 1; }
|
||
.db-detail-chart .tick {
|
||
fill: var(--fg-mute); font-size: 10px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.db-detail-chart .metric-line {
|
||
fill: none; stroke-width: 1.5;
|
||
vector-effect: non-scaling-stroke;
|
||
}
|
||
.db-detail-chart .phase-band { opacity: 0.18; }
|
||
.db-detail-chart .phase-band.clean { fill: var(--phase-clean); }
|
||
.db-detail-chart .phase-band.armed { fill: var(--phase-armed); }
|
||
.db-detail-chart .phase-band.infecting { fill: var(--phase-infecting); }
|
||
.db-detail-chart .phase-band.infected_running { fill: var(--phase-running); }
|
||
.db-detail-chart .phase-band.dormant { fill: var(--phase-dormant); }
|
||
.db-detail-chart .placeholder {
|
||
fill: var(--fg-mute); font-size: 12px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.db-detail-legend {
|
||
display: flex; flex-wrap: wrap; gap: 14px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 11px; color: var(--fg-dim);
|
||
padding: 8px 14px;
|
||
border-top: 1px solid var(--line-soft);
|
||
}
|
||
.db-detail-legend > span { display: inline-flex; align-items: center; }
|
||
.db-detail-legend .swatch {
|
||
display: inline-block; width: 10px; height: 10px;
|
||
border-radius: 2px; margin-right: 6px; vertical-align: middle;
|
||
}
|
||
|
||
/* ─── Attack envelope thumbnails ───────────────────────────────────── */
|
||
.profile-grid {
|
||
display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: clamp(12px, 1.4vw, 22px);
|
||
}
|
||
.profile-card {
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
padding: clamp(12px, 1.2vw, 18px);
|
||
background: rgba(13, 17, 23, 0.6);
|
||
}
|
||
.profile-name {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: clamp(13px, 1vw, 15px); color: var(--fg); margin-bottom: 4px;
|
||
}
|
||
.profile-shape {
|
||
font-size: clamp(11px, 0.85vw, 13px); color: var(--fg-dim);
|
||
margin-bottom: 8px; line-height: 1.4;
|
||
}
|
||
.profile-card svg {
|
||
display: block; width: 100%;
|
||
height: clamp(56px, 9vh, 120px);
|
||
}
|
||
.profile-card svg path { fill: none; stroke: var(--accent); stroke-width: 1.4;
|
||
vector-effect: non-scaling-stroke; }
|
||
|
||
/* ─── Chunking timeline ────────────────────────────────────────────── */
|
||
.chunk-rule, .chunk-row, .chunk-axis { display: flex; gap: 4px; }
|
||
.chunk-row { height: clamp(56px, 9vh, 120px); }
|
||
.chunk-cell {
|
||
flex: 1; border-radius: 3px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: clamp(11px, 0.95vw, 14px); color: var(--fg);
|
||
}
|
||
.chunk-cell.clean { background: var(--phase-clean); }
|
||
.chunk-cell.armed { background: var(--phase-armed); }
|
||
.chunk-cell.infecting { background: var(--phase-infecting); }
|
||
.chunk-cell.infected_running { background: var(--phase-running); }
|
||
.chunk-cell.dormant { background: var(--phase-dormant); }
|
||
.chunk-rule { height: 8px; background: var(--bg-elev);
|
||
border: 1px solid var(--line); border-radius: 2px; padding: 1px; }
|
||
.chunk-rule .tick { flex: 1; border-right: 1px solid var(--line); }
|
||
.chunk-rule .tick:last-child { border-right: none; }
|
||
.chunk-axis { height: 16px; font-size: 10px; color: var(--fg-mute);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||
.chunk-axis span { flex: 1; text-align: center; }
|
||
|
||
/* ─── Model bars ───────────────────────────────────────────────────── */
|
||
.model-bars { display: flex; flex-direction: column; gap: clamp(10px, 1.5vh, 18px); }
|
||
.model-row {
|
||
display: grid; grid-template-columns: minmax(80px, 12ch) 1fr minmax(64px, 9ch);
|
||
gap: clamp(10px, 1vw, 18px); align-items: center; font-variant-numeric: tabular-nums;
|
||
}
|
||
.model-name { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: clamp(13px, 1vw, 15px); color: var(--fg); }
|
||
.model-track { height: clamp(28px, 4vh, 48px); background: var(--bg-elev);
|
||
border: 1px solid var(--line); border-radius: 3px; overflow: hidden; }
|
||
.model-fill { height: 100%; transition: width 600ms cubic-bezier(0.2, 0.8, 0.2, 1); }
|
||
.model-fill.lstm { background: linear-gradient(90deg, #58a6ff, #1f6feb); }
|
||
.model-fill.gru { background: linear-gradient(90deg, #db61a2, #a8327f); }
|
||
.model-fill.rnn { background: linear-gradient(90deg, #d29922, #8a6a17); }
|
||
.model-fill.bert { background: linear-gradient(90deg, #f85149, #b22e2a); }
|
||
.model-fill.knn { background: linear-gradient(90deg, #3fb950, #1a7f37); }
|
||
/* Producer-side additions (see docs/dashboard-request-scenes-7-8-12.md):
|
||
gbt / mlp / cnn / knn_semi are also published as ModelMetric so that
|
||
scene 9 shows the full trained zoo, not just the canonical sequence
|
||
models. Same gradient shape, different hues. */
|
||
.model-fill.gbt { background: linear-gradient(90deg, #ff8c42, #c2410c); }
|
||
.model-fill.mlp { background: linear-gradient(90deg, #a371f7, #6e40c9); }
|
||
.model-fill.cnn { background: linear-gradient(90deg, #34d399, #047857); }
|
||
.model-fill.knn_semi { background: linear-gradient(90deg, #2dd4bf, #115e59); }
|
||
.model-acc { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: clamp(13px, 1vw, 15px); color: var(--fg-dim); text-align: right; }
|
||
|
||
/* ─── KNN 3-D scatter (canvas) ─────────────────────────────────────── */
|
||
.scatter3d-controls {
|
||
display: flex; flex-wrap: wrap; gap: 12px;
|
||
align-items: center; justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
.scatter3d-modes { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
.scatter3d-mode, .scatter3d-reset {
|
||
background: transparent; color: var(--fg-dim);
|
||
border: 1px solid var(--line); border-radius: 16px;
|
||
padding: 4px 12px; font-size: 12px; cursor: pointer;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
transition: color 120ms, border-color 120ms, background 120ms;
|
||
}
|
||
.scatter3d-mode:hover, .scatter3d-reset:hover {
|
||
color: var(--fg); border-color: var(--fg-mute);
|
||
}
|
||
.scatter3d-mode.active {
|
||
color: var(--accent); border-color: var(--accent);
|
||
background: var(--accent-soft, rgba(80, 140, 220, 0.12));
|
||
}
|
||
.scatter3d-wrap {
|
||
position: relative;
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
width: 100%;
|
||
height: clamp(320px, 56vh, 640px);
|
||
overflow: hidden;
|
||
cursor: grab;
|
||
touch-action: none;
|
||
}
|
||
.scatter3d-wrap:active { cursor: grabbing; }
|
||
.scatter3d {
|
||
display: block;
|
||
width: 100%; height: 100%;
|
||
}
|
||
|
||
/* ─── Motivation cards (scene: motivation) ─────────────────────────── */
|
||
.motivation-stack { gap: clamp(12px, 1.8vh, 22px); }
|
||
.motivation-cards {
|
||
display: flex; flex-direction: column;
|
||
gap: clamp(10px, 1.4vh, 18px);
|
||
}
|
||
.motivation-card {
|
||
display: grid;
|
||
grid-template-columns: 6px 1fr;
|
||
gap: clamp(12px, 1.4vw, 18px);
|
||
padding: clamp(12px, 1.6vh, 20px) clamp(14px, 1.6vw, 22px);
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
align-items: stretch;
|
||
}
|
||
.motivation-card-marker {
|
||
border-radius: 2px;
|
||
}
|
||
.motivation-card-marker.mc-trust { background: var(--accent); }
|
||
.motivation-card-marker.mc-contain { background: var(--phase-armed); }
|
||
.motivation-card-marker.mc-recover { background: var(--phase-clean); }
|
||
.motivation-card-body {
|
||
display: flex; flex-direction: column;
|
||
gap: clamp(4px, 0.6vh, 8px);
|
||
min-width: 0;
|
||
}
|
||
.motivation-card-title {
|
||
font: 600 clamp(14px, 1.2vw, 18px)
|
||
ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--fg);
|
||
letter-spacing: 0.02em;
|
||
}
|
||
.motivation-card-text {
|
||
font-size: clamp(13px, 1vw, 15px);
|
||
line-height: 1.5;
|
||
color: var(--fg-dim);
|
||
}
|
||
|
||
/* ─── Problem statement (scene: problem-statement) ─────────────────── */
|
||
.problem-claim {
|
||
padding: clamp(16px, 2vh, 28px) clamp(18px, 2vw, 28px);
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-left: 4px solid var(--accent);
|
||
border-radius: 4px;
|
||
}
|
||
.problem-claim-text {
|
||
font-size: clamp(16px, 1.5vw, 22px);
|
||
line-height: 1.45;
|
||
color: var(--fg);
|
||
font-weight: 500;
|
||
}
|
||
.problem-stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: clamp(10px, 1.4vw, 18px);
|
||
}
|
||
.problem-stat {
|
||
padding: clamp(14px, 1.8vh, 22px);
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
display: flex; flex-direction: column; gap: 4px;
|
||
align-items: flex-start;
|
||
}
|
||
.problem-stat-num {
|
||
font: 700 clamp(28px, 3.4vw, 44px)
|
||
ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--accent);
|
||
line-height: 1;
|
||
}
|
||
.problem-stat-lbl {
|
||
font-size: clamp(12px, 0.95vw, 14px);
|
||
color: var(--fg-dim);
|
||
line-height: 1.35;
|
||
}
|
||
.problem-task {
|
||
padding: 12px 16px;
|
||
background: var(--bg);
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
font-size: clamp(13px, 1vw, 15px);
|
||
color: var(--fg-dim);
|
||
line-height: 1.5;
|
||
}
|
||
.problem-task-label { color: var(--fg-mute); margin-right: 6px; }
|
||
.problem-task-value { color: var(--fg); font-weight: 600; }
|
||
.problem-task-detail { color: var(--fg-dim); }
|
||
|
||
/* ─── Research questions (scene: research-questions) ───────────────── */
|
||
.research-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: clamp(12px, 1.6vw, 22px);
|
||
}
|
||
.research-col {
|
||
padding: clamp(14px, 1.8vh, 22px);
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
}
|
||
.research-col-title {
|
||
font: 600 clamp(13px, 1.05vw, 15px)
|
||
ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--accent);
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
margin-bottom: 12px;
|
||
}
|
||
.research-list {
|
||
list-style: none; padding: 0; margin: 0;
|
||
display: flex; flex-direction: column; gap: 10px;
|
||
font-size: clamp(13px, 1vw, 15px);
|
||
line-height: 1.45;
|
||
color: var(--fg-dim);
|
||
}
|
||
.research-list li::before {
|
||
content: '·'; color: var(--accent); margin-right: 8px;
|
||
}
|
||
.research-list strong { color: var(--fg); }
|
||
|
||
/* ─── Solution overview (scene: solution-overview) ─────────────────── */
|
||
.pipeline-svg {
|
||
width: 100%;
|
||
height: clamp(360px, 60vh, 640px);
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
padding: 12px;
|
||
box-sizing: border-box;
|
||
}
|
||
.pipeline-stage rect {
|
||
fill: var(--bg);
|
||
stroke: var(--accent);
|
||
stroke-width: 1.5;
|
||
}
|
||
.pipeline-stage-models rect {
|
||
fill: var(--accent-soft, rgba(80, 140, 220, 0.08));
|
||
stroke-width: 2;
|
||
}
|
||
.pipeline-stage-final rect {
|
||
stroke: var(--phase-clean);
|
||
}
|
||
.pipeline-stage text {
|
||
fill: var(--fg);
|
||
font: 600 14px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.pipeline-stage-title {
|
||
font-size: 16px !important;
|
||
}
|
||
.pipeline-detail {
|
||
fill: var(--fg-dim) !important;
|
||
font-weight: 400 !important;
|
||
font-size: 11px !important;
|
||
}
|
||
.pipeline-detail-mini {
|
||
fill: var(--fg-mute) !important;
|
||
font-weight: 400 !important;
|
||
font-size: 10px !important;
|
||
}
|
||
.pipeline-arrow path {
|
||
stroke: var(--fg-mute);
|
||
stroke-width: 1.5;
|
||
stroke-linecap: round;
|
||
marker-end: url(#pipe-arrow);
|
||
}
|
||
|
||
/* ─── Evaluation setup (scene: evaluation-setup) ───────────────────── */
|
||
.eval-blocks {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: clamp(10px, 1.4vw, 18px);
|
||
}
|
||
.eval-block {
|
||
padding: clamp(12px, 1.6vh, 18px);
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
display: flex; flex-direction: column; gap: 8px;
|
||
}
|
||
.eval-block-title {
|
||
font: 600 clamp(12px, 0.95vw, 14px)
|
||
ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--accent);
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
}
|
||
.eval-block-body {
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
font-size: clamp(13px, 1vw, 15px);
|
||
color: var(--fg-dim);
|
||
line-height: 1.45;
|
||
}
|
||
.eval-block-body strong { color: var(--fg); }
|
||
.eval-detail {
|
||
margin-top: 4px;
|
||
color: var(--fg-mute);
|
||
font-size: clamp(12px, 0.9vw, 13px);
|
||
font-style: italic;
|
||
}
|
||
|
||
/* ─── Conclusion + future (scene: conclusion-future) ───────────────── */
|
||
.conclusion-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: clamp(12px, 1.6vw, 22px);
|
||
}
|
||
.conclusion-col {
|
||
padding: clamp(14px, 1.8vh, 22px);
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
}
|
||
.conclusion-col-title {
|
||
font: 600 clamp(13px, 1.05vw, 15px)
|
||
ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--accent);
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
margin-bottom: 12px;
|
||
}
|
||
.conclusion-list {
|
||
list-style: none; padding: 0; margin: 0;
|
||
display: flex; flex-direction: column; gap: 10px;
|
||
font-size: clamp(13px, 1vw, 15px);
|
||
line-height: 1.45;
|
||
color: var(--fg-dim);
|
||
}
|
||
.conclusion-list li::before {
|
||
content: '·'; color: var(--accent); margin-right: 8px;
|
||
}
|
||
.conclusion-list strong { color: var(--fg); }
|
||
|
||
/* ─── Limitations card uses the motivation-card pattern with an
|
||
armed-phase marker for the "warning" feel. ─── */
|
||
.motivation-card-marker.mc-armed { background: var(--phase-armed); }
|
||
|
||
/* ─── Live detections (scene: live) ────────────────────────────────── */
|
||
.live-stack { gap: clamp(10px, 1.6vh, 20px); }
|
||
|
||
.live-stats {
|
||
display: flex; flex-wrap: wrap; gap: 12px; align-items: center;
|
||
padding: 8px 14px;
|
||
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--fg-dim);
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
}
|
||
.live-stats-eye { color: var(--accent); font-weight: 600; }
|
||
.live-stats-dot::before { content: '·'; margin-right: 12px; opacity: 0.5; }
|
||
|
||
.live-lanes {
|
||
display: flex; flex-direction: column;
|
||
gap: clamp(4px, 0.8vh, 8px);
|
||
padding: 12px;
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
min-height: 220px;
|
||
}
|
||
.live-lanes:empty::before {
|
||
content: 'no hosts reporting yet';
|
||
align-self: center;
|
||
margin: auto;
|
||
color: var(--fg-mute);
|
||
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
}
|
||
.live-lane {
|
||
display: grid;
|
||
grid-template-columns: minmax(120px, 16ch) 1fr;
|
||
gap: 12px; align-items: center;
|
||
}
|
||
.live-lane-host {
|
||
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--fg);
|
||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||
}
|
||
.live-lane-cells {
|
||
display: flex; gap: 1px;
|
||
height: clamp(24px, 4vh, 36px);
|
||
overflow: hidden;
|
||
background: var(--bg);
|
||
border: 1px solid var(--line);
|
||
border-radius: 2px;
|
||
}
|
||
.live-cell {
|
||
flex: 1 1 0;
|
||
position: relative;
|
||
min-width: 4px;
|
||
}
|
||
.live-cell.clean { background: var(--phase-clean); }
|
||
.live-cell.armed { background: var(--phase-armed); }
|
||
.live-cell.infecting { background: var(--phase-infecting); }
|
||
.live-cell.infected_running { background: var(--phase-running); }
|
||
.live-cell.dormant { background: var(--phase-dormant); }
|
||
.live-cell.miss::after {
|
||
content: ''; position: absolute; inset: 0;
|
||
background: repeating-linear-gradient(
|
||
-45deg, transparent 0 3px, rgba(0, 0, 0, 0.55) 3px 5px
|
||
);
|
||
}
|
||
|
||
.live-latest {
|
||
padding: 14px 16px;
|
||
background: var(--bg-elev, rgba(255, 255, 255, 0.03));
|
||
border: 1px solid var(--line);
|
||
border-radius: 4px;
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto;
|
||
gap: 16px; align-items: center;
|
||
}
|
||
.live-latest-empty {
|
||
grid-column: 1 / -1;
|
||
text-align: center;
|
||
color: var(--fg-mute);
|
||
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
padding: 12px 0;
|
||
}
|
||
.live-phase-block {
|
||
width: clamp(80px, 9vw, 110px);
|
||
aspect-ratio: 1;
|
||
border-radius: 4px;
|
||
display: grid; place-items: center;
|
||
font: clamp(12px, 1vw, 14px) ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-weight: 700;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
text-align: center; padding: 8px;
|
||
line-height: 1.15;
|
||
}
|
||
.live-phase-block.clean { background: var(--phase-clean); }
|
||
.live-phase-block.armed { background: var(--phase-armed); }
|
||
.live-phase-block.infecting { background: var(--phase-infecting); }
|
||
.live-phase-block.infected_running { background: var(--phase-running); }
|
||
.live-phase-block.dormant { background: var(--phase-dormant); }
|
||
|
||
.live-meta {
|
||
display: flex; flex-direction: column; gap: 4px;
|
||
font: 13px ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--fg-dim);
|
||
min-width: 0;
|
||
}
|
||
.live-meta-host { color: var(--fg); font-weight: 600; }
|
||
.live-meta-line code { color: var(--fg); }
|
||
.live-conf {
|
||
text-align: right;
|
||
font: clamp(20px, 2vw, 28px) ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
color: var(--fg);
|
||
white-space: nowrap;
|
||
}
|
||
.live-conf-label {
|
||
display: block;
|
||
font-size: 11px; color: var(--fg-mute);
|
||
font-weight: normal;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
/* ─── Scatter plots (KNN, perf) ────────────────────────────────────── */
|
||
.scatter {
|
||
width: 100%;
|
||
height: clamp(320px, 60vh, 640px);
|
||
}
|
||
.scatter .axis { stroke: var(--line); stroke-width: 1; }
|
||
.scatter .axis-label { fill: var(--fg-mute); font-size: 12px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||
.scatter .point { transition: r 200ms; }
|
||
.scatter .point.clean { fill: var(--phase-clean); }
|
||
.scatter .point.armed { fill: var(--phase-armed); }
|
||
.scatter .point.infecting { fill: var(--phase-infecting); }
|
||
.scatter .point.infected_running { fill: var(--phase-running); }
|
||
.scatter .point.dormant { fill: var(--phase-dormant); }
|
||
.scatter .perf-point { fill: var(--accent); stroke: #1f6feb; stroke-width: 1.5; }
|
||
.scatter .perf-label { fill: var(--fg); font-size: 13px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
|
||
|
||
/* ─── Floating advance button ──────────────────────────────────────── */
|
||
.fab {
|
||
position: absolute; right: 24px; bottom: 24px; z-index: 5;
|
||
width: 44px; height: 44px; border-radius: 50%;
|
||
background: rgba(13, 17, 23, 0.85); color: var(--fg-dim);
|
||
border: 1px solid var(--line); font-size: 16px; cursor: pointer;
|
||
backdrop-filter: blur(8px);
|
||
display: flex; align-items: center; justify-content: center;
|
||
transition: color 150ms, border-color 150ms, transform 150ms;
|
||
}
|
||
.fab:hover { color: var(--accent); border-color: var(--accent); transform: translateY(-1px); }
|
||
.fab:disabled { opacity: 0.25; cursor: not-allowed; transform: none; }
|
||
|
||
/* ─── Article (prose, overlaid right) ──────────────────────────────── */
|
||
.scene {
|
||
display: flex; align-items: center; justify-content: flex-end;
|
||
min-height: 100vh;
|
||
/* 0.5rem right padding instead of 2rem — shifts the prose card
|
||
~1.5rem further right so it sits cleanly past the interactive
|
||
widgets in the metric stack. Bottom/top still 4rem; left 2rem
|
||
stays as scroll-margin for the prose column. */
|
||
padding: 4rem 0.5rem 4rem 2rem;
|
||
}
|
||
|
||
/* ─── Scene virtualization (mount window: active ± 1) ─────────────────
|
||
Only three scenes are "mounted" at a time — the active one, plus its
|
||
immediate neighbours. Far scenes get content-visibility: hidden which
|
||
skips paint and rasterization while preserving layout (so the
|
||
scrollbar position and IntersectionObserver geometry stay correct).
|
||
contain-intrinsic-size declares the placeholder height so the
|
||
document doesn't reflow as scenes mount/unmount on scroll.
|
||
|
||
For the absolutely-positioned stage views (which all overlap in the
|
||
same box), far ones can use display: none outright — they don't
|
||
contribute to layout flow either way. */
|
||
.scene[data-stage]:not([data-mounted]) {
|
||
content-visibility: hidden;
|
||
contain-intrinsic-size: 1px 100vh;
|
||
}
|
||
.stage-view[data-view]:not([data-mounted]) {
|
||
display: none;
|
||
}
|
||
.scene .prose {
|
||
width: var(--prose-w); max-width: calc(100vw - 4rem);
|
||
padding: 2.25rem 2.5rem 2.25rem 5rem;
|
||
font-size: 17px; line-height: 1.65; color: var(--fg);
|
||
/* Two background layers: the existing left-feathering gradient on
|
||
top, plus a uniform backdrop underneath driven by
|
||
--content-backdrop. When the backdrop is 0, the gradient's
|
||
transparent left edge reveals bg behind (matrix-explorable
|
||
look). When the backdrop is non-zero, the transparent edge
|
||
reveals a partly-opaque dark layer instead, so the prose stays
|
||
legible over busy themes. */
|
||
background:
|
||
linear-gradient(
|
||
to left,
|
||
rgba(var(--bg-rgb), 0.96) 0%,
|
||
rgba(var(--bg-rgb), 0.95) 70%,
|
||
rgba(var(--bg-rgb), 0.0) 100%
|
||
),
|
||
rgba(var(--bg-rgb), var(--content-backdrop, 0));
|
||
opacity: 0; transform: translateY(20px);
|
||
transition: opacity var(--scene-fade-ms) ease,
|
||
transform var(--scene-fade-ms) ease;
|
||
}
|
||
.scene[data-active] .prose { opacity: 1; transform: translateY(0); }
|
||
.scene .prose h2 { margin: 0 0 14px; font-size: 24px; font-weight: 600;
|
||
letter-spacing: -0.01em; }
|
||
.scene .prose .lede { font-size: 24px; line-height: 1.4; font-weight: 500;
|
||
margin: 0 0 20px; }
|
||
.scene .prose p { margin: 0 0 14px; }
|
||
.scene .prose strong { color: var(--fg); font-weight: 600; }
|
||
.scene .prose code {
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: 0.9em; color: var(--accent);
|
||
background: var(--accent-soft); padding: 1px 5px; border-radius: 3px;
|
||
}
|
||
.scene .prose .hint {
|
||
color: var(--fg-mute); font-size: 12px;
|
||
letter-spacing: 0.16em; text-transform: uppercase; margin-top: 28px;
|
||
}
|
||
|
||
.scene-end-spacer { height: 30vh; }
|
||
|
||
@media (max-width: 880px) {
|
||
:root { --prose-w: 92vw; }
|
||
.stage-view { padding-right: 0; padding-bottom: 50vh; }
|
||
.intro-block, .metric-stack { padding: 0 24px; }
|
||
.bar-row { grid-template-columns: 110px 1fr 60px; }
|
||
.model-row { grid-template-columns: 80px 1fr 56px; }
|
||
.profile-grid { grid-template-columns: 1fr; }
|
||
.scene { padding: 2rem 1rem; min-height: 80vh; justify-content: center; }
|
||
.scene .prose { padding: 1.5rem; }
|
||
.topbar .counter { display: none; }
|
||
.db-table-wrap { max-height: 40vh; }
|
||
}
|