CIS490/training/dashboard/static/dashboard.css
Max Gorog a04bba6281 training/dashboard: click a db row → render the episode envelope
New endpoint GET /api/episode/<host_id>/<episode_id> in app.py.
Stream-decompresses the tarball (zstd -dc piped into tarfile),
extracts telemetry-proc.jsonl, labels.jsonl, and meta.json,
returns the parsed contents. Synchronous extract runs in
asyncio.to_thread so the event loop isn't blocked.

Frontend: clicking a row in the database explorer now fetches
the episode and draws an SVG chart matching the README's Real
Alpine VM envelope shape:
  - per-interval CPU jiffies delta (user + sys)
  - per-interval IO bytes delta (read + write)
  - colored phase bands (clean/armed/infecting/infected_running/
    dormant) overlaid by labels.jsonl
  - axis ticks for 0-peak on Y, 0-totalDuration in seconds on X
  - legend below the chart with palette-driven swatches

The detail panel that previously showed the row JSON now shows
metadata + the chart + the legend. Validated end-to-end against
a real episode (863 samples, 8 labels) extracted from
/var/lib/cis490/episodes/elliott-thinkpad/.
2026-05-08 01:16:54 -05:00

936 lines
38 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;
--fg: #e6edf3;
--fg-dim: #8b949e;
--fg-mute: #484f58;
--line: #1c2128;
--line-soft: #21262d;
--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); }
}
/* ─── 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%;
background: conic-gradient(
from -90deg,
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, #fff 0%, #8b949e 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);
}
.code-card .kw { color: #ff7b72; }
.code-card .str { color: #a5d6ff; }
.code-card .com { color: #6e7681; font-style: italic; }
.code-card .fn { color: #d2a8ff; }
.code-card .ty { color: #ffa657; }
.code-card .num { color: #79c0ff; }
/* ─── 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: rgba(255,255,255,0.85);
}
.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-acc { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: clamp(13px, 1vw, 15px); color: var(--fg-dim); text-align: right; }
/* ─── 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 .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: #fff; 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; }
}