Rework the CRT raster, glow and flicker effects

The three effects had drifted apart from each other and from the token
system. Consolidate them behind --crt-* tokens and fix what was broken.

- The animated text-shadow silently overrode the static one, so
  reduce-motion users saw a 5px white glow and everyone else a 3px
  currentColor glow. Both now come from one declaration, with the
  aberration offset factored out into --crt-shift.
- Step the aberration instead of tweening it. text-shadow is not
  compositable and this one inherits to every glyph, so the smooth
  version repainted the whole document at refresh rate.
- The flicker was invisible: a near-black tint at an effective alpha of
  0.008-0.096 over a black background, a peak lift of under 2/255.
  Retint with the accent at ~3%.
- Anchor the vignette to the viewport. It was sized to body's box, which
  is document-height, so on long pages the bloom sat off-screen.
- Pin the overlays with position: fixed rather than painting
  document-sized layers, and give the grille hard stops so it is three
  stripes rather than a smooth R->G->B ramp.
- Scale the raster pitch down at >=2dppx, where a 2px scanline is 4
  device px and resamples into moire at fractional DPR.
- Raise --color-mg to oklch(0.62): at 0.5 it was 3.5:1 on the
  background and 1.7:1 over the vignette centre. Halve the vignette
  alpha via its own token, keeping --color-accent-transparent at 50%
  for ProjectCard's glow.

Collapses the two keyframe blocks from 193 lines to 40.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
badblocks 2026-08-07 17:39:05 -07:00
parent 89cfe7bdec
commit 0e6d352dca
Signed by: badblocks
SSH key fingerprint: SHA256:hEcM6BP4hKm9F7WsomNuXSBpPn2BSDnFzPSl3y1NerQ
2 changed files with 158 additions and 166 deletions

View file

@ -2,6 +2,20 @@
@import "reset.css" layer(reset);
@import "theme.css" layer(theme);
/* The chromatic-aberration offset, as a 0..1 multiplier of
--crt-aberration-max. Registering it gives it a typed initial value, so the
calc() in body's text-shadow stays valid when the animation is not running.
Note the var(--crt-shift, 0) fallbacks at the use sites: without one, an
unresolved --crt-shift makes the calc() invalid at computed-value time,
which drops the *whole* text-shadow glow included not just the smear.
The animation itself does not depend on this rule, because step-end never
interpolates; it only ever substitutes a literal keyframe value. */
@property --crt-shift {
syntax: "<number>";
inherits: true;
initial-value: 0;
}
@font-face {
font-family: "unscii16";
src:
@ -45,13 +59,37 @@
:root {
/* Color */
--color-fg: oklch(1 0 0);
--color-mg: oklch(0.5 0 0);
/* 0.62, not 0.5: at 0.5 this was 3.5:1 on the background, under AA, and
1.7:1 against the centre of the CRT vignette. 0.62 gives 5.8:1 / 4.4:1. */
--color-mg: oklch(0.62 0 0);
--color-bg: oklch(0 0 0);
--color-accent: oklch(0.6 0.18 142.5);
--color-accent-bright: oklch(0.8 0.18 142.5);
--color-glow: oklch(0.8328 0 0);
--color-accent-transparent: oklch(0.6 0.18 142.5 / 50%);
/* CRT
The raster (scanline + aperture grille) and the phosphor (vignette,
glow, chromatic aberration). Every value the effect uses lives here so
the whole look can be retuned without touching the keyframes. */
--crt-scanline-pitch: 2px;
--crt-grille-pitch: 3px;
--crt-scanline-color: rgb(0 0 0 / 0.25);
--crt-grille-r: rgb(255 0 0 / 0.06);
--crt-grille-g: rgb(0 255 0 / 0.02);
--crt-grille-b: rgb(0 0 255 / 0.06);
/* Deliberately not --color-accent-transparent, which ProjectCard uses for
its box-shadow glow and wants at 50%. As a full-screen wash behind text,
50% costs too much contrast; 25% keeps --color-mg above 4.5:1. */
--crt-vignette-core: oklch(0.6 0.18 142.5 / 25%);
--crt-vignette-edge: var(--color-bg);
--crt-shadow-cool: rgb(0 30 255 / 0.5);
--crt-shadow-warm: rgb(255 0 80 / 0.3);
--crt-glow-blur: 3px;
/* In em so the smear scales with the fluid root font-size. */
--crt-aberration-max: 0.15em;
--crt-flicker-base: 0.03;
/* Spacing */
--gutter-large: clamp(3ch, 2.5vmax, 3.75ch);
--stack-large: clamp(1.25ex, 2.5vmax, 1.75ex);
@ -112,6 +150,16 @@
}
}
/* The raster is specified in CSS px, so on a 2dppx panel a 2px scanline is
4 device px coarse, and on fractional DPR it resamples into moire.
Tightening the pitch keeps the apparent scale roughly constant. */
@media (min-resolution: 2dppx) {
:root {
--crt-scanline-pitch: 1.5px;
--crt-grille-pitch: 2.25px;
}
}
@media (prefers-color-scheme: light) {
}