Homepage redesign: show-don't-tell with animated scope hero

Restructure the homepage to lead with visuals instead of text:

- Hero: split layout with animated Tektronix 465 oscilloscope showing
  RC step response (CSS+SVG, zero JS) that links to the notebook
- Pipeline strip: 3-step Write → Simulate → Visualize with code/terminal
  previews and inline waveform SVG
- Featured notebooks: 3 curated circuits (RC, 555, common emitter) with
  pre-rendered waveform thumbnails
- Gallery cards: decorative graticule header strip, color-coded by engine
- Footer: updated copy with clearer call to action

All new sections are server-rendered Astro components. Total new client
JavaScript: zero bytes.
This commit is contained in:
Ryan Malloy 2026-02-20 15:16:53 -07:00
parent 3c9c83742d
commit 43789bdf24
6 changed files with 885 additions and 88 deletions

View File

@ -0,0 +1,87 @@
---
import { rcLowPassSvg, astable555Svg, commonEmitterSvg } from '../lib/featured-waveforms';
const featured = [
{
id: 'rc-lowpass-filter',
title: 'RC Low-Pass Filter',
engine: 'ngspice' as const,
description: 'Classic transient analysis — square wave input through an RC network produces the characteristic exponential charge/discharge response.',
tags: ['transient', 'passive', 'beginner'],
svg: rcLowPassSvg,
},
{
id: '555-astable-blinker',
title: '555 Astable LED Blinker',
engine: 'ngspice' as const,
description: 'Timer IC in free-running mode — the capacitor ramp between comparator thresholds generates a square wave output.',
tags: ['timer', 'oscillator', 'digital'],
svg: astable555Svg,
},
{
id: 'common-emitter-amplifier',
title: 'Common Emitter Amplifier',
engine: 'ngspice' as const,
description: 'Single-stage BJT amplifier with AC analysis — input sine wave is amplified and phase-inverted at the collector.',
tags: ['amplifier', 'ac-analysis', 'bjt'],
svg: commonEmitterSvg,
},
];
const engineColor: Record<string, { border: string; bg: string; text: string }> = {
ngspice: { border: 'border-blue-500/30', bg: 'bg-blue-500/10', text: 'text-blue-400' },
ltspice: { border: 'border-amber-500/30', bg: 'bg-amber-500/10', text: 'text-amber-400' },
};
---
<section class="max-w-6xl mx-auto px-6 py-16">
<p class="text-sm font-semibold tracking-widest text-blue-400 uppercase mb-2">Featured</p>
<h2 class="text-2xl font-bold text-slate-100 mb-8">Start with a classic circuit</h2>
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{featured.map((nb) => {
const colors = engineColor[nb.engine] ?? engineColor.ngspice;
return (
<a
href={`/notebook/${nb.id}`}
class:list={[
'group block rounded-lg border bg-slate-900/50 overflow-hidden',
'hover:border-blue-500/40 hover:bg-slate-800/70 transition-all',
'border-slate-800',
]}
>
{/* Waveform thumbnail */}
<div class="w-full h-[180px] bg-[#0a0a0a] border-b border-slate-800/60 overflow-hidden">
<Fragment set:html={nb.svg} />
</div>
<div class="p-5">
<div class="flex items-start justify-between mb-2">
<h3 class="text-base font-semibold text-slate-100 group-hover:text-blue-400 transition-colors line-clamp-1">
{nb.title}
</h3>
<span class:list={[
'shrink-0 ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border',
colors.border, colors.bg, colors.text,
]}>
{nb.engine}
</span>
</div>
<p class="text-sm text-slate-400 leading-relaxed mb-3 line-clamp-2">
{nb.description}
</p>
<div class="flex flex-wrap gap-1.5">
{nb.tags.map((tag) => (
<span class="text-xs bg-slate-700/50 text-slate-400 px-2 py-0.5 rounded">
{tag}
</span>
))}
</div>
</div>
</a>
);
})}
</div>
</section>

View File

@ -29,37 +29,40 @@ export function NotebookCard({ notebook }: NotebookCardProps) {
return (
<a
href={`/notebook/${encodeURIComponent(id)}`}
className="group block rounded-lg border border-slate-800 bg-slate-900/50 p-5 hover:border-blue-500/40 hover:bg-slate-800/70 transition-all"
className="group block rounded-lg border border-slate-800 bg-slate-900/50 overflow-hidden hover:border-blue-500/40 hover:bg-slate-800/70 transition-all"
>
<div className="flex items-start justify-between mb-2">
<h3 className="text-base font-semibold text-slate-100 group-hover:text-blue-400 transition-colors line-clamp-1">
{title}
</h3>
<Badge variant={engineVariant[engine] ?? 'default'} className="shrink-0 ml-2">
{engine}
</Badge>
</div>
<div className="flex items-center gap-3 text-xs text-slate-500 mb-3">
<span className="inline-flex items-center gap-1">
<LayoutGrid className="w-3 h-3" />
{cell_count} cell{cell_count !== 1 ? 's' : ''}
</span>
<span>{formatDate(modified)}</span>
</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{tags.map((tag) => (
<span
key={tag}
className="text-xs bg-slate-700/50 text-slate-400 px-2 py-0.5 rounded"
>
{tag}
</span>
))}
<div className="notebook-card-strip" data-engine={engine} />
<div className="p-5">
<div className="flex items-start justify-between mb-2">
<h3 className="text-base font-semibold text-slate-100 group-hover:text-blue-400 transition-colors line-clamp-1">
{title}
</h3>
<Badge variant={engineVariant[engine] ?? 'default'} className="shrink-0 ml-2">
{engine}
</Badge>
</div>
)}
<div className="flex items-center gap-3 text-xs text-slate-500 mb-3">
<span className="inline-flex items-center gap-1">
<LayoutGrid className="w-3 h-3" />
{cell_count} cell{cell_count !== 1 ? 's' : ''}
</span>
<span>{formatDate(modified)}</span>
</div>
{tags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{tags.map((tag) => (
<span
key={tag}
className="text-xs bg-slate-700/50 text-slate-400 px-2 py-0.5 rounded"
>
{tag}
</span>
))}
</div>
)}
</div>
</a>
);
}

View File

@ -0,0 +1,163 @@
---
import { Icon } from 'astro-icon/components';
---
<section class="max-w-6xl mx-auto px-6 py-16">
<div class="grid gap-6 md:grid-cols-[1fr_auto_1fr_auto_1fr] items-start">
<!-- Step 1: Write -->
<div class="pipeline-step">
<span class="pipeline-number">01</span>
<h3 class="pipeline-title">Write</h3>
<div class="pipeline-preview">
<pre class="pipeline-code"><span class="text-slate-500">* RC Low-Pass Filter</span>
<span class="text-blue-400">V1</span> in 0 PULSE(0 5 0 1n 1n 0.5m 1m)
<span class="text-blue-400">R1</span> in out 10k
<span class="text-blue-400">C1</span> out 0 100n
<span class="text-emerald-400">.tran</span> 10u 5m</pre>
</div>
<p class="pipeline-desc">
SPICE netlists with syntax highlighting and autocomplete
</p>
</div>
<!-- Arrow 1→2 -->
<div class="pipeline-arrow">
<Icon name="lucide:arrow-right" class="w-5 h-5 text-slate-600 hidden md:block" />
<Icon name="lucide:chevron-down" class="w-5 h-5 text-slate-600 md:hidden" />
</div>
<!-- Step 2: Simulate -->
<div class="pipeline-step">
<span class="pipeline-number">02</span>
<h3 class="pipeline-title">Simulate</h3>
<div class="pipeline-preview">
<div class="pipeline-terminal">
<div class="flex items-center gap-2 mb-2">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
<span class="text-emerald-400 text-xs font-mono font-semibold">ngspice 43</span>
</div>
<div class="text-xs font-mono text-slate-500 space-y-0.5">
<p>Circuit: RC Low-Pass Filter</p>
<p>Doing transient analysis...</p>
<p>501 data points, 23ms</p>
</div>
<div class="flex items-center gap-1.5 mt-2">
<Icon name="lucide:check-circle-2" class="w-3.5 h-3.5 text-emerald-400" />
<span class="text-emerald-400 text-xs font-mono font-semibold">complete</span>
</div>
</div>
</div>
<p class="pipeline-desc">
Run ngspice simulations with one click
</p>
</div>
<!-- Arrow 2→3 -->
<div class="pipeline-arrow">
<Icon name="lucide:arrow-right" class="w-5 h-5 text-slate-600 hidden md:block" />
<Icon name="lucide:chevron-down" class="w-5 h-5 text-slate-600 md:hidden" />
</div>
<!-- Step 3: Visualize -->
<div class="pipeline-step">
<span class="pipeline-number">03</span>
<h3 class="pipeline-title">Visualize</h3>
<div class="pipeline-preview pipeline-waveform">
<svg viewBox="0 0 300 140" xmlns="http://www.w3.org/2000/svg" class="w-full h-full" preserveAspectRatio="none">
<!-- Faint grid -->
<defs>
<pattern id="ps-grid" width="30" height="17.5" patternUnits="userSpaceOnUse">
<line x1="30" y1="0" x2="30" y2="17.5" stroke="rgba(45,212,191,0.06)" stroke-width="0.5"/>
<line x1="0" y1="17.5" x2="30" y2="17.5" stroke="rgba(45,212,191,0.06)" stroke-width="0.5"/>
</pattern>
</defs>
<rect width="300" height="140" fill="#0a0a0a"/>
<rect width="300" height="140" fill="url(#ps-grid)"/>
<!-- Input square wave (dim) -->
<polyline fill="none" stroke="rgba(96,165,250,0.3)" stroke-width="1.2"
points="0,115 0,25 75,25 75,115 150,115 150,25 225,25 225,115 300,115"/>
<!-- RC response (bright teal) -->
<path fill="none" stroke="#2dd4bf" stroke-width="1.8" stroke-linecap="round"
d="M0,115 C19,50 38,30 75,27 C94,95 112,112 150,114 C169,50 188,30 225,27 C244,95 262,112 300,114"/>
</svg>
</div>
<p class="pipeline-desc">
Interactive waveforms + oscilloscope mode
</p>
</div>
</div>
</section>
<style>
.pipeline-step {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.pipeline-number {
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--color-sb-accent, #2563eb);
text-transform: uppercase;
}
.pipeline-title {
font-size: 1.125rem;
font-weight: 700;
color: #f1f5f9;
letter-spacing: -0.01em;
}
.pipeline-preview {
border-radius: 0.5rem;
border: 1px solid #1e293b;
background: #0f172a;
overflow: hidden;
min-height: 140px;
}
.pipeline-code {
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.75rem;
line-height: 1.6;
padding: 0.875rem 1rem;
margin: 0;
white-space: pre;
color: #e2e8f0;
overflow: hidden;
}
.pipeline-terminal {
padding: 0.875rem 1rem;
}
.pipeline-waveform {
padding: 0;
aspect-ratio: 300 / 140;
}
.pipeline-desc {
font-size: 0.875rem;
color: #94a3b8;
line-height: 1.5;
}
.pipeline-arrow {
display: flex;
align-items: center;
justify-content: center;
padding-top: 5rem;
}
@media (max-width: 767px) {
.pipeline-arrow {
padding-top: 0;
padding-bottom: 0;
}
}
</style>

View File

@ -0,0 +1,59 @@
// Pre-rendered SVG waveform thumbnails for the 3 featured notebooks.
// Each is a self-contained SVG string with dark background, faint grid, and trace paths.
const GRID = `<defs>
<pattern id="ft-grid" width="10%" height="12.5%" patternUnits="objectBoundingBox">
<line x1="100%" y1="0" x2="100%" y2="100%" stroke="rgba(45,212,191,0.06)" stroke-width="0.5"/>
<line x1="0" y1="100%" x2="100%" y2="100%" stroke="rgba(45,212,191,0.06)" stroke-width="0.5"/>
</pattern>
</defs>
<rect width="100%" height="100%" fill="#0a0a0a"/>
<rect width="100%" height="100%" fill="url(#ft-grid)"/>`;
// 1. RC Low-Pass Filter — square wave input (dim) + exponential charge/discharge output (bright)
// Time axis: 0→5ms, two full cycles of 1kHz square wave through R=10k, C=100nF (τ=1ms)
export const rcLowPassSvg = `<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
${GRID}
<!-- Input: square wave (dimmer) -->
<polyline fill="none" stroke="rgba(96,165,250,0.3)" stroke-width="1.5"
points="0,160 0,40 100,40 100,160 200,160 200,40 300,40 300,160 400,160"/>
<!-- Output: RC exponential response (bright teal) -->
<path fill="none" stroke="#2dd4bf" stroke-width="2" stroke-linecap="round"
d="M0,160
C25,70 50,45 100,42
C125,130 150,155 200,158
C225,70 250,45 300,42
C325,130 350,155 400,158"/>
</svg>`;
// 2. 555 Astable LED Blinker — sharp square wave output with slight ramp on edges
// Output toggles between ~0V and ~Vcc with RC timing visible
export const astable555Svg = `<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
${GRID}
<!-- Capacitor voltage: sawtooth ramp between thresholds (dimmer) -->
<polyline fill="none" stroke="rgba(250,204,21,0.3)" stroke-width="1.5"
points="0,130 70,80 70,130 140,80 140,130 210,80 210,130 280,80 280,130 350,80 350,130 400,95"/>
<!-- Output: clean square wave (bright amber) -->
<polyline fill="none" stroke="#f59e0b" stroke-width="2"
points="0,160 0,40 70,40 70,160 140,160 140,40 210,40 210,160 280,160 280,40 350,40 350,160 400,160"/>
</svg>`;
// 3. Common Emitter Amplifier — input sine (dim) + amplified inverted sine (bright)
// AC analysis showing voltage gain with phase inversion
export const commonEmitterSvg = `<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
${GRID}
<!-- Input: small sine wave centered (dimmer) -->
<path fill="none" stroke="rgba(167,139,250,0.35)" stroke-width="1.5"
d="M0,100
C17,85 33,85 50,100 C67,115 83,115 100,100
C117,85 133,85 150,100 C167,115 183,115 200,100
C217,85 233,85 250,100 C267,115 283,115 300,100
C317,85 333,85 350,100 C367,115 383,115 400,100"/>
<!-- Output: amplified, inverted sine (bright green) -->
<path fill="none" stroke="#34d399" stroke-width="2" stroke-linecap="round"
d="M0,100
C17,145 33,145 50,100 C67,55 83,55 100,100
C117,145 133,145 150,100 C167,55 183,55 200,100
C217,145 233,145 250,100 C267,55 283,55 300,100
C317,145 333,145 350,100 C367,55 383,55 400,100"/>
</svg>`;

View File

@ -2,6 +2,8 @@
import { Icon } from 'astro-icon/components';
import NotebookLayout from '../layouts/NotebookLayout.astro';
import NotebookGallery from '../components/NotebookGallery';
import PipelineStrip from '../components/PipelineStrip.astro';
import FeaturedNotebooks from '../components/FeaturedNotebooks.astro';
import { fetchNotebookList } from '../lib/server-api';
import type { NotebookSummary } from '../lib/types';
@ -23,72 +25,139 @@ try {
@import '../styles/homepage.css';
</style>
<!-- Hero -->
<!-- Hero: Split layout — text left, animated scope visual right -->
<section class="hero-graticule border-b border-slate-800/60">
<div class="relative max-w-6xl mx-auto px-6 pt-16 pb-20">
<p class="text-sm font-semibold tracking-widest text-blue-400 uppercase mb-4">SpiceBook</p>
<h1 class="text-4xl md:text-5xl font-bold text-slate-100 tracking-tight max-w-2xl">
Circuit Simulation Notebooks
</h1>
<p class="mt-4 text-lg text-slate-400 max-w-xl leading-relaxed">
Write SPICE netlists, run ngspice simulations, and visualize waveforms in a single document.
</p>
<div class="flex items-center gap-3 mt-8">
<a
href="/notebook/new"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm"
>
<Icon name="lucide:plus" class="w-4 h-4" />
New Notebook
</a>
<a
href="#notebooks"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg border border-slate-700 text-slate-300 hover:border-slate-500 hover:text-slate-100 font-medium transition-colors text-sm"
>
Browse Notebooks
<Icon name="lucide:chevron-down" class="w-4 h-4" />
</a>
<div class="grid md:grid-cols-2 gap-12 items-center">
<!-- Left: Copy + CTAs -->
<div>
<p class="text-sm font-semibold tracking-widest text-blue-400 uppercase mb-4">SpiceBook</p>
<h1 class="text-4xl md:text-5xl font-bold text-slate-100 tracking-tight">
Circuit Simulation Notebooks
</h1>
<p class="mt-4 text-lg text-slate-400 max-w-xl leading-relaxed">
Write SPICE netlists, run ngspice simulations, and visualize waveforms in a single document.
</p>
<div class="flex items-center gap-3 mt-8">
<a
href="/notebook/new"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm"
>
<Icon name="lucide:plus" class="w-4 h-4" />
New Notebook
</a>
<a
href="#notebooks"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg border border-slate-700 text-slate-300 hover:border-slate-500 hover:text-slate-100 font-medium transition-colors text-sm"
>
Browse Notebooks
<Icon name="lucide:chevron-down" class="w-4 h-4" />
</a>
</div>
</div>
<!-- Right: Animated oscilloscope visual -->
<div class="flex justify-center md:justify-end">
<a href="/notebook/rc-lowpass-filter" class="hero-scope" aria-label="Open RC Low-Pass Filter notebook">
<div class="hero-scope-frame">
<!-- Brand bar -->
<div class="hero-scope-brand">
<span class="hero-scope-brand-name">Tektronix</span>
<span class="hero-scope-brand-model">465</span>
</div>
<!-- CRT bay -->
<div class="hero-scope-crt-bay">
<div class="hero-scope-screen">
<!-- Waveform SVG: RC step response -->
<svg viewBox="0 0 500 300" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none" aria-hidden="true">
<!-- Input: square wave (dim blue) -->
<polyline
class="hero-scope-trace-input"
fill="none"
stroke="rgba(96,165,250,0.25)"
stroke-width="1.5"
points="0,240 0,60 125,60 125,240 250,240 250,60 375,60 375,240 500,240"
/>
<!-- Output: RC exponential response (bright teal) -->
<path
class="hero-scope-trace-output"
fill="none"
stroke="#2dd4bf"
stroke-width="2.5"
stroke-linecap="round"
d="M0,240
C31,105 62,68 125,62
C156,195 188,232 250,238
C281,105 312,68 375,62
C406,195 438,232 500,238"
/>
</svg>
<!-- Graticule + scanlines -->
<div class="hero-scope-graticule"></div>
<div class="hero-scope-scanlines"></div>
</div>
</div>
<!-- Digital readout -->
<div class="hero-scope-readout">
<span>V(out)</span>
<span class="hero-scope-readout-divider">|</span>
<span>200μs/div</span>
<span class="hero-scope-readout-divider">|</span>
<span>500mV/div</span>
</div>
<!-- Control panel -->
<div class="hero-scope-panel">
<div class="hero-scope-controls">
<div class="hero-scope-section">
<span class="hero-scope-section-label">Volts</span>
<div class="hero-scope-knob" data-pos="2"></div>
</div>
<div class="hero-scope-section">
<span class="hero-scope-section-label">Time</span>
<div class="hero-scope-knob" data-pos="3"></div>
</div>
<div class="hero-scope-section">
<span class="hero-scope-section-label">Trigger</span>
<div class="hero-scope-knob" data-pos="1"></div>
</div>
<div class="hero-scope-section">
<div class="hero-scope-mode">TRAN</div>
<div class="hero-scope-led"></div>
</div>
</div>
</div>
</div>
</a>
</div>
</div>
</div>
</section>
<!-- Feature Highlights -->
<section class="max-w-6xl mx-auto px-6 py-16">
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div class="p-5 rounded-lg border border-slate-800 bg-slate-900/50">
<Icon name="lucide:zap" class="w-6 h-6 text-blue-400 mb-3" />
<h3 class="font-semibold text-slate-100 mb-1">SPICE Simulation</h3>
<p class="text-sm text-slate-400 leading-relaxed">
Write netlists and run ngspice. Transient, AC, DC sweep, operating point.
</p>
</div>
<div class="p-5 rounded-lg border border-slate-800 bg-slate-900/50">
<Icon name="lucide:activity" class="w-6 h-6 text-blue-400 mb-3" />
<h3 class="font-semibold text-slate-100 mb-1">Waveform Plots</h3>
<p class="text-sm text-slate-400 leading-relaxed">
Results render as interactive plots below each netlist.
</p>
</div>
<div class="p-5 rounded-lg border border-slate-800 bg-slate-900/50">
<Icon name="lucide:circuit-board" class="w-6 h-6 text-blue-400 mb-3" />
<h3 class="font-semibold text-slate-100 mb-1">Schematic Generation</h3>
<p class="text-sm text-slate-400 leading-relaxed">
Netlists produce SVG circuit diagrams automatically.
</p>
</div>
<div class="p-5 rounded-lg border border-slate-800 bg-slate-900/50">
<Icon name="lucide:file-text" class="w-6 h-6 text-blue-400 mb-3" />
<h3 class="font-semibold text-slate-100 mb-1">Notebook Format</h3>
<p class="text-sm text-slate-400 leading-relaxed">
Markdown, SPICE, and Python cells in a single document.
</p>
</div>
</div>
<!-- Pipeline: Write → Simulate → Visualize -->
<section class="border-b border-slate-800/60">
<PipelineStrip />
</section>
<!-- Featured Notebooks -->
<section class="border-b border-slate-800/60">
<FeaturedNotebooks />
</section>
<!-- Notebook Gallery -->
<section id="notebooks" class="max-w-6xl mx-auto px-6 pb-20 scroll-mt-8">
<h2 class="text-2xl font-bold text-slate-100 mb-8">Notebooks</h2>
<section id="notebooks" class="max-w-6xl mx-auto px-6 py-20 scroll-mt-8">
<div class="mb-8">
<h2 class="text-2xl font-bold text-slate-100">Explore Notebooks</h2>
{notebooks.length > 0 && (
<p class="text-sm text-slate-500 mt-1">{notebooks.length} notebooks available</p>
)}
</div>
<NotebookGallery
client:load
initialNotebooks={notebooks}
@ -99,10 +168,11 @@ try {
<!-- Footer CTA -->
<footer class="border-t border-slate-800/60">
<div class="max-w-6xl mx-auto px-6 py-16 text-center">
<h2 class="text-2xl font-bold text-slate-100 mb-3">Ready to simulate?</h2>
<h2 class="text-2xl font-bold text-slate-100 mb-2">Start with a blank notebook</h2>
<p class="text-sm text-slate-400 mb-6">Write a SPICE netlist, run it, see results in seconds.</p>
<a
href="/notebook/new"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm mt-4"
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg bg-blue-600 hover:bg-blue-500 text-white font-medium transition-colors text-sm"
>
<Icon name="lucide:plus" class="w-4 h-4" />
New Notebook

View File

@ -29,3 +29,418 @@
.filter-pills::-webkit-scrollbar {
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;
position: relative;
overflow: hidden;
background: #0f172a;
}
.notebook-card-strip::before {
content: '';
position: absolute;
inset: 0;
background-image:
repeating-linear-gradient(
90deg,
transparent,
transparent calc(10% - 0.5px),
var(--strip-accent, rgba(37, 99, 235, 0.08)) calc(10% - 0.5px),
var(--strip-accent, rgba(37, 99, 235, 0.08)) calc(10% + 0.5px),
transparent calc(10% + 0.5px)
),
repeating-linear-gradient(
0deg,
transparent,
transparent calc(25% - 0.5px),
var(--strip-accent, rgba(37, 99, 235, 0.08)) calc(25% - 0.5px),
var(--strip-accent, rgba(37, 99, 235, 0.08)) calc(25% + 0.5px),
transparent calc(25% + 0.5px)
);
}
/* Bottom accent line */
.notebook-card-strip::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
var(--strip-line, rgba(37, 99, 235, 0.25)) 20%,
var(--strip-line, rgba(37, 99, 235, 0.25)) 80%,
transparent
);
}
/* Engine color variants */
.notebook-card-strip[data-engine="ngspice"] {
--strip-accent: rgba(37, 99, 235, 0.08);
--strip-line: rgba(37, 99, 235, 0.25);
}
.notebook-card-strip[data-engine="ltspice"] {
--strip-accent: rgba(245, 158, 11, 0.08);
--strip-line: rgba(245, 158, 11, 0.25);
}