diff --git a/frontend/public/n-spheres/attractor.flac b/frontend/public/n-spheres/attractor.flac
new file mode 100644
index 0000000..c7cfc25
Binary files /dev/null and b/frontend/public/n-spheres/attractor.flac differ
diff --git a/frontend/public/n-spheres/core.flac b/frontend/public/n-spheres/core.flac
new file mode 100644
index 0000000..c10f37e
Binary files /dev/null and b/frontend/public/n-spheres/core.flac differ
diff --git a/frontend/public/n-spheres/flux.flac b/frontend/public/n-spheres/flux.flac
new file mode 100644
index 0000000..958b96c
Binary files /dev/null and b/frontend/public/n-spheres/flux.flac differ
diff --git a/frontend/public/n-spheres/function.flac b/frontend/public/n-spheres/function.flac
new file mode 100644
index 0000000..d7b4b74
Binary files /dev/null and b/frontend/public/n-spheres/function.flac differ
diff --git a/frontend/public/n-spheres/intersect.flac b/frontend/public/n-spheres/intersect.flac
new file mode 100644
index 0000000..246cff3
Binary files /dev/null and b/frontend/public/n-spheres/intersect.flac differ
diff --git a/frontend/public/spirals_shrt.mp3 b/frontend/public/spirals_shrt.mp3
new file mode 100644
index 0000000..7f00e84
Binary files /dev/null and b/frontend/public/spirals_shrt.mp3 differ
diff --git a/frontend/src/components/OscilloscopeDisplay.astro b/frontend/src/components/OscilloscopeDisplay.astro
new file mode 100644
index 0000000..a1d86d7
--- /dev/null
+++ b/frontend/src/components/OscilloscopeDisplay.astro
@@ -0,0 +1,601 @@
+---
+/**
+ * XY-mode audio oscilloscope display.
+ * Visual style inspired by the Tektronix 465 (1972).
+ * Renders stereo audio as Lissajous patterns on a canvas.
+ *
+ * Audio: "Spirals" by Jerobeam Fenderson (oscilloscopemusic.com)
+ * License: CC BY-NC-SA 4.0
+ * Original visual: Nick Watton (codepen.io/2Mogs)
+ * Gist: rsp2k (gist.github.com/rsp2k/ac68b1bb290b8124e162987ed1df8d53)
+ * Modernized: ScriptProcessor → AnalyserNode + rAF
+ */
+---
+
+
+
+
+
Tektronix
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Vertical
+
+
Volts/Div
+
+
+
+
+
Horizontal
+
+
Time/Div
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/index.astro b/frontend/src/pages/index.astro
index 0d8311c..034b249 100644
--- a/frontend/src/pages/index.astro
+++ b/frontend/src/pages/index.astro
@@ -4,6 +4,7 @@ import NotebookLayout from '../layouts/NotebookLayout.astro';
import NotebookGallery from '../components/NotebookGallery';
import PipelineStrip from '../components/PipelineStrip.astro';
import FeaturedNotebooks from '../components/FeaturedNotebooks.astro';
+import OscilloscopeDisplay from '../components/OscilloscopeDisplay.astro';
import { fetchNotebookList } from '../lib/server-api';
import type { NotebookSummary } from '../lib/types';
@@ -23,6 +24,7 @@ try {
>
@@ -57,83 +59,9 @@ try {
-
+
diff --git a/frontend/src/styles/homepage.css b/frontend/src/styles/homepage.css
index 0efc61e..9bedb79 100644
--- a/frontend/src/styles/homepage.css
+++ b/frontend/src/styles/homepage.css
@@ -30,362 +30,6 @@
display: none;
}
-/* ═══════════════════════════════════════════════════════════
- Hero Oscilloscope Visual — Pure CSS + SVG
- Adapted from scope-skin.css, fully independent (hero-scope- prefix)
- ═══════════════════════════════════════════════════════════ */
-
-/* Color palette */
-.hero-scope {
- --hs-teal: #2dd4bf;
- --hs-teal-dim: rgba(45, 212, 191, 0.12);
- --hs-teal-glow: rgba(45, 212, 191, 0.18);
- --hs-panel: #b5a48a;
- --hs-panel-light: #c7b89e;
- --hs-panel-dark: #9e8f78;
- --hs-crt-bg: #0a0a0a;
- --hs-label: #3b3428;
- --hs-knob: #2a2a2d;
- --hs-knob-ring: #1e1e20;
- --hs-section-line: rgba(59, 52, 40, 0.25);
-
- display: block;
- max-width: 420px;
- width: 100%;
-}
-
-/* ── Outer frame ──────────────────────────────────────────── */
-.hero-scope-frame {
- position: relative;
- background:
- url("data:image/svg+xml,%3Csvg width='4' height='4' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1' x='0' y='0' fill='rgba(0,0,0,0.03)'/%3E%3Crect width='1' height='1' x='2' y='2' fill='rgba(255,255,255,0.02)'/%3E%3C/svg%3E"),
- linear-gradient(175deg, var(--hs-panel-light), var(--hs-panel), var(--hs-panel-dark));
- border-radius: 6px;
- overflow: hidden;
- box-shadow:
- 0 8px 30px rgba(0, 0, 0, 0.45),
- 0 2px 6px rgba(0, 0, 0, 0.25),
- inset 0 1px 0 rgba(255, 255, 255, 0.15),
- inset 0 -1px 0 rgba(0, 0, 0, 0.1);
-}
-
-/* ── Brand bar ────────────────────────────────────────────── */
-.hero-scope-brand {
- display: flex;
- align-items: baseline;
- justify-content: space-between;
- padding: 8px 14px 6px;
- border-bottom: 1px solid var(--hs-section-line);
-}
-
-.hero-scope-brand-name {
- font-family: 'Georgia', 'Times New Roman', serif;
- font-size: 0.65rem;
- font-weight: 700;
- letter-spacing: 0.18em;
- text-transform: uppercase;
- color: var(--hs-label);
- opacity: 0.9;
-}
-
-.hero-scope-brand-model {
- font-family: var(--font-mono, ui-monospace, monospace);
- font-size: 0.55rem;
- font-weight: 600;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--hs-label);
- opacity: 0.78;
-}
-
-/* ── CRT bay ──────────────────────────────────────────────── */
-.hero-scope-crt-bay {
- margin: 10px 12px 0;
- background: #1a1816;
- border-radius: 4px;
- padding: 6px;
- box-shadow:
- inset 0 2px 6px rgba(0, 0, 0, 0.5),
- inset 0 0 0 1px rgba(0, 0, 0, 0.2);
-}
-
-/* ── CRT screen ───────────────────────────────────────────── */
-.hero-scope-screen {
- position: relative;
- background: var(--hs-crt-bg);
- border-radius: 3px;
- overflow: hidden;
- aspect-ratio: 5 / 3;
- box-shadow:
- 0 0 15px var(--hs-teal-glow),
- inset 0 1px 4px rgba(0, 0, 0, 0.4);
-}
-
-/* SVG fills the CRT */
-.hero-scope-screen svg {
- position: absolute;
- inset: 0;
- width: 100%;
- height: 100%;
-}
-
-/* ── Graticule overlay (10×8 grid) ────────────────────────── */
-.hero-scope-graticule {
- position: absolute;
- inset: 0;
- pointer-events: none;
- z-index: 2;
- background-image:
- repeating-linear-gradient(
- 90deg,
- transparent,
- transparent calc(10% - 0.5px),
- rgba(45, 212, 191, 0.07) calc(10% - 0.5px),
- rgba(45, 212, 191, 0.07) calc(10% + 0.5px),
- transparent calc(10% + 0.5px)
- ),
- repeating-linear-gradient(
- 0deg,
- transparent,
- transparent calc(12.5% - 0.5px),
- rgba(45, 212, 191, 0.07) calc(12.5% - 0.5px),
- rgba(45, 212, 191, 0.07) calc(12.5% + 0.5px),
- transparent calc(12.5% + 0.5px)
- );
-}
-
-/* Center crosshair ticks */
-.hero-scope-graticule::before,
-.hero-scope-graticule::after {
- content: '';
- position: absolute;
-}
-
-.hero-scope-graticule::before {
- top: 50%;
- left: calc(50% - 8px);
- width: 16px;
- height: 1px;
- background: rgba(45, 212, 191, 0.18);
-}
-
-.hero-scope-graticule::after {
- left: 50%;
- top: calc(50% - 8px);
- width: 1px;
- height: 16px;
- background: rgba(45, 212, 191, 0.18);
-}
-
-/* ── Scanlines ────────────────────────────────────────────── */
-.hero-scope-scanlines {
- position: absolute;
- inset: 0;
- pointer-events: none;
- z-index: 3;
- background: repeating-linear-gradient(
- 0deg,
- transparent,
- transparent 2px,
- rgba(0, 0, 0, 0.04) 2px,
- rgba(0, 0, 0, 0.04) 4px
- );
- mix-blend-mode: multiply;
-}
-
-/* ── SVG trace animation ──────────────────────────────────── */
-.hero-scope-trace-input {
- stroke-dasharray: 1200;
- stroke-dashoffset: 1200;
- animation: hero-trace-draw 1.8s ease-out 0.3s forwards;
-}
-
-.hero-scope-trace-output {
- stroke-dasharray: 1200;
- stroke-dashoffset: 1200;
- animation: hero-trace-draw 1.8s ease-out 0.6s forwards;
-}
-
-@keyframes hero-trace-draw {
- to {
- stroke-dashoffset: 0;
- }
-}
-
-/* Phosphor glow pulse after draw completes */
-.hero-scope-trace-output {
- animation:
- hero-trace-draw 1.8s ease-out 0.6s forwards,
- hero-phosphor-pulse 3s ease-in-out 2.4s infinite;
-}
-
-@keyframes hero-phosphor-pulse {
- 0%, 100% {
- filter: drop-shadow(0 0 2px var(--hs-teal-glow));
- }
- 50% {
- filter: drop-shadow(0 0 6px rgba(45, 212, 191, 0.35));
- }
-}
-
-/* ── Digital readout bar ──────────────────────────────────── */
-.hero-scope-readout {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 16px;
- padding: 5px 12px;
- background: #0a0a0a;
- margin: 0 12px;
- border-radius: 0 0 3px 3px;
- font-family: var(--font-mono, ui-monospace, monospace);
- font-size: 0.65rem;
- letter-spacing: 0.04em;
- color: var(--hs-teal);
- text-shadow: 0 0 4px var(--hs-teal-glow);
-}
-
-.hero-scope-readout-divider {
- color: rgba(45, 212, 191, 0.25);
-}
-
-/* ── Control panel ────────────────────────────────────────── */
-.hero-scope-panel {
- padding: 8px 12px 8px;
- border-top: 1px solid var(--hs-section-line);
- margin-top: 10px;
-}
-
-.hero-scope-controls {
- display: flex;
- align-items: flex-start;
- gap: 2px;
-}
-
-.hero-scope-section {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 4px;
- padding: 4px 2px;
- position: relative;
-}
-
-/* Section dividers */
-.hero-scope-section + .hero-scope-section::before {
- content: '';
- position: absolute;
- left: -1px;
- top: 0;
- bottom: 0;
- width: 1px;
- background: var(--hs-section-line);
-}
-
-.hero-scope-section-label {
- font-family: var(--font-mono, ui-monospace, monospace);
- font-size: 0.5rem;
- font-weight: 700;
- letter-spacing: 0.12em;
- text-transform: uppercase;
- color: var(--hs-label);
- opacity: 0.88;
- line-height: 1;
-}
-
-/* ── Decorative knobs ─────────────────────────────────────── */
-.hero-scope-knob {
- width: 28px;
- height: 28px;
- border-radius: 50%;
- background: radial-gradient(circle at 40% 35%, #3a3a3e, var(--hs-knob));
- border: 2px solid var(--hs-knob-ring);
- box-shadow:
- 0 2px 4px rgba(0, 0, 0, 0.35),
- inset 0 1px 1px rgba(255, 255, 255, 0.06);
- position: relative;
-}
-
-/* Knob indicator line */
-.hero-scope-knob::after {
- content: '';
- position: absolute;
- top: 3px;
- left: 50%;
- width: 1.5px;
- height: 7px;
- background: #d4d0c8;
- border-radius: 1px;
- transform: translateX(-50%);
-}
-
-/* Knob rotation variants */
-.hero-scope-knob[data-pos="1"] { transform: rotate(-60deg); }
-.hero-scope-knob[data-pos="2"] { transform: rotate(-20deg); }
-.hero-scope-knob[data-pos="3"] { transform: rotate(30deg); }
-.hero-scope-knob[data-pos="4"] { transform: rotate(70deg); }
-
-/* ── Mode badge ───────────────────────────────────────────── */
-.hero-scope-mode {
- background: var(--hs-knob);
- border: 2px solid var(--hs-knob-ring);
- border-radius: 6px;
- color: var(--hs-teal);
- font-family: var(--font-mono, ui-monospace, monospace);
- font-size: 0.55rem;
- font-weight: 700;
- letter-spacing: 0.06em;
- padding: 5px 8px;
- text-transform: uppercase;
- line-height: 1;
- box-shadow:
- 0 2px 4px rgba(0, 0, 0, 0.3),
- 0 0 6px var(--hs-teal-glow);
-}
-
-/* ── Power LED ────────────────────────────────────────────── */
-.hero-scope-led {
- width: 5px;
- height: 5px;
- border-radius: 50%;
- background: var(--hs-teal);
- border: 1px solid rgba(0, 0, 0, 0.3);
- box-shadow: 0 0 6px var(--hs-teal-glow);
-}
-
-/* ── Accessibility: reduced motion ────────────────────────── */
-@media (prefers-reduced-motion: reduce) {
- .hero-scope-trace-input,
- .hero-scope-trace-output {
- stroke-dasharray: none;
- stroke-dashoffset: 0;
- animation: none;
- filter: none;
- }
-
- .hero-scope-scanlines {
- display: none;
- }
-}
-
-/* ── Responsive ───────────────────────────────────────────── */
-@media (max-width: 50rem) {
- .hero-scope-knob {
- width: 24px;
- height: 24px;
- }
-
- .hero-scope-knob::after {
- height: 5px;
- }
-
- .hero-scope-readout {
- font-size: 0.55rem;
- gap: 8px;
- }
-}
-
/* ── Gallery card decorative header strip ─────────────────── */
.notebook-card-strip {
height: 3rem;
diff --git a/frontend/src/styles/oscilloscope.css b/frontend/src/styles/oscilloscope.css
new file mode 100644
index 0000000..25f09ba
--- /dev/null
+++ b/frontend/src/styles/oscilloscope.css
@@ -0,0 +1,649 @@
+/* Oscilloscope display -- Tektronix 465 inspired
+ * Tan/champagne panel, recessed CRT, teal phosphor
+ * Ported from mcltspice docs for SpiceBook hero section
+ *
+ * Audio: Jerobeam Fenderson (oscilloscopemusic.com)
+ * Original visual: Nick Watton (codepen.io/2Mogs)
+ * Gist: rsp2k (gist.github.com/rsp2k/ac68b1bb290b8124e162987ed1df8d53)
+ */
+
+/* ── Outer chassis ───────────────────────────────────────── */
+.scope-frame {
+ --scope-teal: #2dd4bf;
+ --scope-teal-dim: rgba(45, 212, 191, 0.12);
+ --scope-teal-glow: rgba(45, 212, 191, 0.18);
+ --scope-panel: #b5a48a;
+ --scope-panel-light: #c7b89e;
+ --scope-panel-dark: #9e8f78;
+ --scope-crt-bg: #0a0a0a;
+ --scope-label: #3b3428;
+ --scope-knob: #2a2a2d;
+ --scope-knob-ring: #1e1e20;
+ --scope-section-line: rgba(59, 52, 40, 0.25);
+
+ position: relative;
+ background:
+ /* subtle metallic grain */
+ url("data:image/svg+xml,%3Csvg width='4' height='4' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1' x='0' y='0' fill='rgba(0,0,0,0.03)'/%3E%3Crect width='1' height='1' x='2' y='2' fill='rgba(255,255,255,0.02)'/%3E%3C/svg%3E"),
+ linear-gradient(175deg, var(--scope-panel-light), var(--scope-panel), var(--scope-panel-dark));
+ border-radius: 6px;
+ padding: 0;
+ box-shadow:
+ 0 10px 30px rgba(0, 0, 0, 0.55),
+ 0 2px 6px rgba(0, 0, 0, 0.3),
+ inset 0 1px 0 rgba(255, 255, 255, 0.15),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.1);
+ max-width: 420px;
+ width: 100%;
+ margin: 0 auto;
+ overflow: hidden;
+}
+
+/* ── Top brand bar ───────────────────────────────────────── */
+.scope-brand {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ padding: 8px 14px 6px;
+ border-bottom: 1px solid var(--scope-section-line);
+}
+
+.scope-brand-name {
+ font-family: 'Georgia', 'Times New Roman', serif;
+ font-size: 0.65rem;
+ font-weight: 700;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--scope-label);
+ opacity: 0.9;
+}
+
+.scope-brand-model {
+ font-family: var(--font-mono, ui-monospace, monospace);
+ font-size: 0.55rem;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--scope-label);
+ opacity: 0.78;
+}
+
+/* ── CRT bay (recessed dark area) ────────────────────────── */
+.scope-crt-bay {
+ margin: 10px 12px 0;
+ background: #1a1816;
+ border-radius: 4px;
+ padding: 6px;
+ box-shadow:
+ inset 0 2px 6px rgba(0, 0, 0, 0.5),
+ inset 0 0 0 1px rgba(0, 0, 0, 0.2);
+}
+
+/* ── CRT screen ──────────────────────────────────────────── */
+.scope-screen {
+ position: relative;
+ background: var(--scope-crt-bg);
+ border-radius: 3px;
+ overflow: hidden;
+ aspect-ratio: 1;
+ box-shadow:
+ 0 0 15px var(--scope-teal-glow),
+ inset 0 1px 4px rgba(0, 0, 0, 0.4);
+}
+
+/* ── Canvas ──────────────────────────────────────────────── */
+.scope-canvas {
+ display: block;
+ width: 100%;
+ height: 100%;
+}
+
+/* ── Graticule overlay (8x10 grid, like the 465) ─────────── */
+.scope-graticule {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ /* 10 horizontal, 8 vertical — classic Tek grid */
+ background-image:
+ repeating-linear-gradient(
+ 90deg,
+ transparent,
+ transparent calc(12.5% - 0.5px),
+ rgba(45, 212, 191, 0.07) calc(12.5% - 0.5px),
+ rgba(45, 212, 191, 0.07) calc(12.5% + 0.5px),
+ transparent calc(12.5% + 0.5px)
+ ),
+ repeating-linear-gradient(
+ 0deg,
+ transparent,
+ transparent calc(12.5% - 0.5px),
+ rgba(45, 212, 191, 0.07) calc(12.5% - 0.5px),
+ rgba(45, 212, 191, 0.07) calc(12.5% + 0.5px),
+ transparent calc(12.5% + 0.5px)
+ );
+}
+
+/* Center crosshair tick marks */
+.scope-graticule::before,
+.scope-graticule::after {
+ content: '';
+ position: absolute;
+}
+
+.scope-graticule::before {
+ /* horizontal center tick */
+ top: 50%;
+ left: calc(50% - 8px);
+ width: 16px;
+ height: 1px;
+ background: rgba(45, 212, 191, 0.18);
+}
+
+.scope-graticule::after {
+ /* vertical center tick */
+ left: 50%;
+ top: calc(50% - 8px);
+ width: 1px;
+ height: 16px;
+ background: rgba(45, 212, 191, 0.18);
+}
+
+/* ── Scanline overlay ────────────────────────────────────── */
+.scope-scanlines {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ background: repeating-linear-gradient(
+ 0deg,
+ transparent,
+ transparent 2px,
+ rgba(0, 0, 0, 0.04) 2px,
+ rgba(0, 0, 0, 0.04) 4px
+ );
+ mix-blend-mode: multiply;
+}
+
+/* ── Control panel area ──────────────────────────────────── */
+.scope-panel {
+ padding: 8px 12px 6px;
+ border-top: 1px solid var(--scope-section-line);
+ margin-top: 10px;
+}
+
+.scope-controls-row {
+ display: flex;
+ align-items: flex-start;
+ gap: 2px;
+}
+
+/* ── Control section (labeled group) ─────────────────────── */
+.scope-section {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 2px;
+ position: relative;
+}
+
+/* Section divider lines */
+.scope-section + .scope-section::before {
+ content: '';
+ position: absolute;
+ left: -1px;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background: var(--scope-section-line);
+}
+
+.scope-section-label {
+ font-family: var(--font-mono, ui-monospace, monospace);
+ font-size: 0.5rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--scope-label);
+ opacity: 0.88;
+ line-height: 1;
+}
+
+/* ── Rotary knob ─────────────────────────────────────────── */
+.scope-knob {
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: radial-gradient(circle at 40% 35%, #3a3a3e, var(--scope-knob));
+ border: 2px solid var(--scope-knob-ring);
+ box-shadow:
+ 0 2px 4px rgba(0, 0, 0, 0.35),
+ inset 0 1px 1px rgba(255, 255, 255, 0.06);
+ position: relative;
+}
+
+/* Knob indicator line */
+.scope-knob::after {
+ content: '';
+ position: absolute;
+ top: 3px;
+ left: 50%;
+ width: 1.5px;
+ height: 7px;
+ background: #d4d0c8;
+ border-radius: 1px;
+ transform: translateX(-50%);
+}
+
+.scope-knob-label {
+ font-family: var(--font-mono, ui-monospace, monospace);
+ font-size: 0.45rem;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ color: var(--scope-label);
+ opacity: 0.78;
+ line-height: 1;
+}
+
+/* ── Source knob (interactive) ───────────────────────────── */
+/* Uses --knob-rotation custom prop so JS rotation composes with CSS scale */
+.scope-source-knob {
+ cursor: pointer;
+ transition: transform 0.15s;
+ transform: rotate(var(--knob-rotation, 0deg));
+}
+
+.scope-source-knob:hover {
+ transform: rotate(var(--knob-rotation, 0deg)) scale(1.08);
+}
+
+.scope-source-knob:active {
+ transform: rotate(var(--knob-rotation, 0deg)) scale(0.95);
+}
+
+.scope-source-knob:focus-visible {
+ outline: 2px solid var(--scope-teal);
+ outline-offset: 2px;
+}
+
+/* ── Volume / attenuation knob (interactive) ──────────── */
+.scope-volume-knob {
+ cursor: pointer;
+ transition: transform 0.15s;
+ transform: rotate(var(--knob-rotation, 0deg));
+}
+
+.scope-volume-knob:hover {
+ transform: rotate(var(--knob-rotation, 0deg)) scale(1.08);
+}
+
+.scope-volume-knob:active {
+ transform: rotate(var(--knob-rotation, 0deg)) scale(0.95);
+}
+
+.scope-volume-knob:focus-visible {
+ outline: 2px solid var(--scope-teal);
+ outline-offset: 2px;
+}
+
+/* ── Volume label ─────────────────────────────────────── */
+.scope-volume-label {
+ max-width: 60px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: center;
+}
+
+/* ── Signal name label ──────────────────────────────────── */
+.scope-source-name {
+ max-width: 60px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ text-align: center;
+}
+
+/* ── Loading state overlay ──────────────────────────────── */
+.scope-screen[data-loading="true"]::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: rgba(10, 10, 10, 0.7);
+ z-index: 5;
+ animation: scope-loading-pulse 1.2s ease-in-out infinite;
+}
+
+@keyframes scope-loading-pulse {
+ 0%, 100% { opacity: 0.5; }
+ 50% { opacity: 0.9; }
+}
+
+/* ── Power toggle ────────────────────────────────────────── */
+.scope-toggle {
+ appearance: none;
+ background: var(--scope-knob);
+ border: 2px solid var(--scope-knob-ring);
+ border-radius: 6px;
+ color: #8a8880;
+ font-family: var(--font-mono, ui-monospace, monospace);
+ font-size: 0.55rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ padding: 5px 8px;
+ cursor: pointer;
+ transition: color 0.2s, box-shadow 0.2s;
+ text-transform: uppercase;
+ line-height: 1;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.scope-toggle:hover {
+ color: #c4c0b8;
+}
+
+.scope-toggle[data-active="true"] {
+ color: var(--scope-teal);
+ box-shadow:
+ 0 2px 4px rgba(0, 0, 0, 0.3),
+ 0 0 6px var(--scope-teal-glow);
+}
+
+.scope-toggle:focus-visible {
+ outline: 2px solid var(--scope-teal);
+ outline-offset: 2px;
+}
+
+/* ── Power LED ───────────────────────────────────────────── */
+.scope-led {
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: #3a3632;
+ border: 1px solid rgba(0, 0, 0, 0.3);
+ transition: background 0.3s, box-shadow 0.3s;
+}
+
+.scope-led[data-on="true"] {
+ background: var(--scope-teal);
+ box-shadow: 0 0 6px var(--scope-teal-glow);
+}
+
+/* ── Attribution bar ─────────────────────────────────────── */
+.scope-attribution-bar {
+ padding: 5px 14px 7px;
+ border-top: 1px solid var(--scope-section-line);
+}
+
+.scope-attribution {
+ font-family: var(--font-mono, ui-monospace, monospace);
+ font-size: 0.48rem;
+ color: var(--scope-label);
+ opacity: 0.72;
+ line-height: 1.4;
+ text-align: center;
+}
+
+.scope-attribution a {
+ color: var(--scope-label);
+ text-decoration: none;
+ transition: color 0.15s;
+}
+
+.scope-attribution a:hover {
+ color: #1a1610;
+}
+
+/* ── Outer Limits easter egg overlay ─────────────────────── */
+.scope-outer-limits {
+ position: absolute;
+ inset: 0;
+ background: rgba(10, 10, 10, 0.92);
+ z-index: 10;
+ display: none;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 10% 12%;
+ cursor: pointer;
+}
+
+.scope-outer-limits[data-active="true"] {
+ display: flex;
+}
+
+.scope-ol-text {
+ font-family: 'Georgia', 'Times New Roman', serif;
+ font-size: 0.65rem;
+ line-height: 1.6;
+ text-align: center;
+ margin: 0;
+ /* Animated rainbow gradient text */
+ background: linear-gradient(
+ 90deg,
+ #ff6b6b, #ffb347, #ffd93d, #a3ff6b,
+ #2dd4bf, #6bb5ff, #c084fc, #ff6b9d, #ff6b6b
+ );
+ background-size: 300% 100%;
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+ animation: scope-rainbow 6s linear infinite;
+ filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.15));
+}
+
+@keyframes scope-rainbow {
+ 0% { background-position: 0% center; }
+ 100% { background-position: 300% center; }
+}
+
+/* Closing narration + plug -- fades in after typewriter finishes */
+.scope-ol-closing {
+ font-family: 'Georgia', 'Times New Roman', serif;
+ font-size: 0.55rem;
+ line-height: 1.5;
+ color: var(--scope-teal);
+ text-align: center;
+ text-shadow: 0 0 8px var(--scope-teal-glow);
+ margin: 1em 0 0;
+ opacity: 0;
+ transform: translateY(6px);
+ transition: opacity 1.5s ease-in, transform 1.5s ease-out;
+ pointer-events: none;
+}
+
+.scope-ol-closing[data-visible="true"] {
+ opacity: 0.85;
+ transform: translateY(0);
+ pointer-events: auto;
+}
+
+.scope-ol-link {
+ color: var(--scope-teal);
+ text-decoration: none;
+ font-style: italic;
+ font-size: 0.65rem;
+ text-shadow: 0 0 12px var(--scope-teal-glow), 0 0 4px var(--scope-teal-glow);
+ transition: text-shadow 0.2s;
+}
+
+.scope-ol-link:hover {
+ text-decoration: underline;
+ text-shadow: 0 0 16px var(--scope-teal), 0 0 6px var(--scope-teal-glow);
+}
+
+/* ── Easter egg knobs (Vertical / Horizontal) ────────────── */
+.scope-easter-knob {
+ cursor: pointer;
+ transition: transform 0.15s;
+}
+
+.scope-easter-knob:hover {
+ transform: scale(1.08);
+}
+
+.scope-easter-knob:active {
+ transform: scale(0.95);
+}
+
+.scope-easter-knob:focus-visible {
+ outline: 2px solid var(--scope-teal);
+ outline-offset: 2px;
+}
+
+/* ── Idle state ──────────────────────────────────────────── */
+.scope-screen[data-idle="true"] .scope-canvas {
+ opacity: 0.6;
+}
+
+/* ── Reduced motion ──────────────────────────────────────── */
+@media (prefers-reduced-motion: reduce) {
+ .scope-scanlines {
+ display: none;
+ }
+}
+
+/* ── Responsive ──────────────────────────────────────────── */
+@media (max-width: 50rem) {
+ .scope-frame {
+ max-width: 340px;
+ }
+
+ .scope-knob {
+ width: 24px;
+ height: 24px;
+ }
+
+ .scope-knob::after {
+ height: 5px;
+ }
+}
+
+/* ── Skin picker (model name -> clickable) ──────────────── */
+.scope-brand-right {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 1px;
+}
+
+button.scope-brand-model {
+ appearance: none;
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ line-height: inherit;
+ transition: opacity 0.15s;
+}
+
+button.scope-brand-model:hover {
+ opacity: 0.85;
+}
+
+button.scope-brand-model:focus-visible {
+ outline: 2px solid var(--scope-teal);
+ outline-offset: 2px;
+ border-radius: 2px;
+}
+
+.scope-brand-sub {
+ font-family: var(--font-mono, ui-monospace, monospace);
+ font-size: 0.4rem;
+ font-weight: 600;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ color: var(--scope-label);
+ opacity: 0.4;
+}
+
+/* ── Tektronix Type 545A skin (1959) ─────────────────────── */
+/* Vacuum-tube era: blue-green hammertone, cream silk-screen,
+ Bakelite knobs, deeper CRT recess, ventilation holes */
+
+.scope-skin-545a {
+ --scope-panel: #4a6e64;
+ --scope-panel-light: #5a7e72;
+ --scope-panel-dark: #3a5a50;
+ --scope-label: #e0d8c8;
+ --scope-knob: #1c1814;
+ --scope-knob-ring: #141210;
+ --scope-section-line: rgba(224, 216, 200, 0.15);
+}
+
+/* Hammertone texture -- larger dimpled dots, irregular placement */
+.scope-skin-545a {
+ background:
+ url("data:image/svg+xml,%3Csvg width='8' height='8' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='2' cy='2' r='1.2' fill='rgba(0,0,0,0.06)'/%3E%3Ccircle cx='6' cy='5' r='0.8' fill='rgba(255,255,255,0.04)'/%3E%3Ccircle cx='4' cy='7' r='1' fill='rgba(0,0,0,0.04)'/%3E%3Ccircle cx='7' cy='1' r='0.6' fill='rgba(255,255,255,0.03)'/%3E%3C/svg%3E"),
+ linear-gradient(175deg, var(--scope-panel-light), var(--scope-panel), var(--scope-panel-dark));
+}
+
+/* Deeper CRT bay recess -- tube-era instruments had heavier bezels */
+.scope-skin-545a .scope-crt-bay {
+ box-shadow:
+ inset 0 3px 8px rgba(0, 0, 0, 0.6),
+ inset 0 0 0 1px rgba(0, 0, 0, 0.3);
+}
+
+/* Larger Bakelite-era knobs */
+.scope-skin-545a .scope-knob {
+ width: 32px;
+ height: 32px;
+ background: radial-gradient(circle at 40% 35%, #2e2218, var(--scope-knob));
+}
+
+.scope-skin-545a .scope-knob::after {
+ height: 8px;
+ background: #c8b890;
+}
+
+/* Cream-on-green attribution */
+.scope-skin-545a .scope-attribution {
+ color: var(--scope-label);
+ opacity: 0.6;
+}
+
+.scope-skin-545a .scope-attribution a {
+ color: var(--scope-label);
+}
+
+.scope-skin-545a .scope-attribution a:hover {
+ color: #fff;
+}
+
+/* ── Ventilation holes (545A only) ──────────────────────── */
+.scope-vent-holes {
+ display: none;
+ justify-content: center;
+ gap: 8px;
+ padding: 6px 14px 8px;
+}
+
+.scope-vent-hole {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.15) 100%);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+.scope-skin-545a .scope-vent-holes {
+ display: flex;
+}
+
+/* ── 545A responsive overrides ──────────────────────────── */
+@media (max-width: 50rem) {
+ .scope-skin-545a .scope-knob {
+ width: 26px;
+ height: 26px;
+ }
+
+ .scope-skin-545a .scope-knob::after {
+ height: 6px;
+ }
+
+ .scope-vent-hole {
+ width: 5px;
+ height: 5px;
+ }
+}