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 +
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ Spirals +
+ + +
+ +
+ 20% +
+ + +
+ +
+ Volts/Div +
+ + +
+ +
+ 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 { - +
- -
- - -
- Tektronix - 465 -
- - -
-
- - - -
-
-
-
- - -
- V(out) - | - 200μs/div - | - 500mV/div -
- - -
-
-
- -
-
-
- -
-
-
- -
-
-
-
TRAN
-
-
-
-
- -
-
+
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; + } +}