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>
171 lines
5.5 KiB
CSS
171 lines
5.5 KiB
CSS
@layer reset, config, font, utilities, theme;
|
|
@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:
|
|
url("unscii-16.woff") format("woff"),
|
|
url("unscii-16.ttf") format("ttf");
|
|
}
|
|
@font-face {
|
|
font-family: "unscii8";
|
|
src:
|
|
url("unscii-8.woff") format("woff"),
|
|
url("unscii-8.ttf") format("ttf");
|
|
}
|
|
|
|
@layer font {
|
|
:root {
|
|
/* Traditional point sizes for 16px font */
|
|
--pt-double-canon: 3.75rem; /*60px*/
|
|
--pt-canon: 3.25rem; /*52px*/
|
|
--pt-double-great-primer: 2.5rem; /*40px*/
|
|
--pt-double-english: 2rem; /*32px*/
|
|
--pt-double-pica: 1.75rem; /*28px*/
|
|
--pt-paragon: 1.5rem; /*24px*/
|
|
--pt-great-primer: 1.375rem; /*22px*/
|
|
--pt-english: 1.125rem; /*18px*/
|
|
--pt-pica: 1rem; /*16px*/
|
|
--pt-small-pica: 0.875rem; /*14px*/
|
|
--pt-long-primer: 0.75rem; /*12px*/
|
|
--pt-bourgeois: 0.625rem; /*10px*/
|
|
--pt-minion: 0.5rem; /*8px*/
|
|
}
|
|
}
|
|
|
|
@layer utilities {
|
|
.hidden {
|
|
display: none;
|
|
visibility: hidden;
|
|
}
|
|
}
|
|
|
|
@layer config {
|
|
:root {
|
|
/* Color */
|
|
--color-fg: oklch(1 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);
|
|
--gutter-small: clamp(0.5ch, 1vmax, 1ch);
|
|
--stack-small: clamp(0.25ex, 0.5vmax, 0.5ex);
|
|
--line-length: 75ch;
|
|
--page-max-inline-size: min(
|
|
var(--line-length),
|
|
100dvi - var(--gutter-large) * 2
|
|
);
|
|
--page-padding-inline: calc((100dvi - var(--page-max-inline-size)) / 2);
|
|
|
|
/* Typography */
|
|
--font: unscii16, "Courier New", monospace;
|
|
--font-alternative: unscii8, "Courier New", monospace;
|
|
--font-weight: 400;
|
|
--font-weight-semibold: 600;
|
|
--font-weight-bold: 900;
|
|
--font-size-min: 100%;
|
|
--font-size-max: 150%;
|
|
--font-size-clamp: clamp(
|
|
var(--font-size-min),
|
|
var(--font-size-min) * 0.9 + 0.5dvi,
|
|
var(--font-size-max)
|
|
);
|
|
--leading: 1.4;
|
|
|
|
/* Traditional point sizes for 12px font */
|
|
--pt-double-canon: 4.666rem; /*56px*/
|
|
--pt-canon: 3.999rem; /*48px*/
|
|
--pt-double-great-primer: 2.999rem; /*36px*/
|
|
--pt-double-english: 2.333rem; /*28px*/
|
|
--pt-double-pica: 2rem; /*24px*/
|
|
--pt-paragon: 1.666rem; /*20px*/
|
|
--pt-great-primer: 1.5rem; /*18px*/
|
|
--pt-english: 1.166rem; /*14px*/
|
|
--pt-pica: 1rem; /*12px*/
|
|
--pt-small-pica: 0.916rem; /*11px*/
|
|
--pt-long-primer: 0.833rem; /*10px*/
|
|
--pt-bourgeois: 0.75rem; /*9px*/
|
|
--pt-minion: 0.583rem; /*7px*/
|
|
|
|
font-size: var(--font-size-clamp);
|
|
font: -apple-system-body;
|
|
font-family: var(--font);
|
|
font-weight: var(--font-weight);
|
|
line-height: var(--leading);
|
|
color: var(--color-fg);
|
|
background-color: var(--color-bg);
|
|
accent-color: var(--color-accent);
|
|
color-scheme: dark light;
|
|
}
|
|
|
|
@supports (font: -apple-system-body) and
|
|
(not (-webkit-touch-callout: default)) {
|
|
:root {
|
|
font-size: var(--font-size-clamp);
|
|
}
|
|
}
|
|
|
|
/* 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) {
|
|
}
|
|
|
|
@media (prefers-contrast: more) {
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
}
|
|
}
|