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/.
936 lines
38 KiB
CSS
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; }
|
|
}
|