: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 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); } .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; } }