Compare commits
1 commit
main
...
worktree-c
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e6d352dca |
2 changed files with 158 additions and 166 deletions
|
|
@ -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) {
|
||||
}
|
||||
|
||||
|
|
|
|||
272
public/theme.css
272
public/theme.css
|
|
@ -39,12 +39,21 @@ html {
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/* The aberration is a discrete jitter, so it is stepped rather than
|
||||
interpolated: step-end repaints 21 times per cycle instead of once per
|
||||
frame. text-shadow is not compositable, and this one inherits to every
|
||||
glyph on the page, so a smooth tween would repaint the whole document at
|
||||
refresh rate. Stepping also reads more like chroma noise than a ramp does. */
|
||||
body {
|
||||
animation: textShadow 1.6s infinite;
|
||||
animation: crt-aberration 1.6s step-end infinite;
|
||||
/* Anchors the vignette to the viewport. Without it the gradient is sized to
|
||||
body's box, which is document-height, so on a long page its centre sits
|
||||
at the middle of the document and the bloom is off-screen at scroll-top. */
|
||||
background-attachment: fixed;
|
||||
background-color: var(--color-bg);
|
||||
background-image: radial-gradient(
|
||||
var(--color-accent-transparent),
|
||||
black 120%
|
||||
var(--crt-vignette-core),
|
||||
var(--crt-vignette-edge) 120%
|
||||
);
|
||||
color: var(--color-fg);
|
||||
column-gap: var(--gutter-large);
|
||||
|
|
@ -58,46 +67,56 @@ body {
|
|||
position: absolute;
|
||||
right: 0;
|
||||
row-gap: var(--stack-large);
|
||||
text-shadow: 0 0 5px var(--color-glow);
|
||||
/* Single source of truth for the glow: when the animation is off,
|
||||
--crt-shift is 0 and this is exactly the static glow. Previously the
|
||||
animation overrode a separate `0 0 5px var(--color-glow)` rule, so
|
||||
reduce-motion users saw a 5px white glow and everyone else a 3px
|
||||
currentColor one — two looks nobody had chosen. */
|
||||
text-shadow:
|
||||
calc(var(--crt-shift, 0) * var(--crt-aberration-max)) 0 1px
|
||||
var(--crt-shadow-cool),
|
||||
calc(var(--crt-shift, 0) * var(--crt-aberration-max) * -1) 0 1px
|
||||
var(--crt-shadow-warm),
|
||||
0 0 var(--crt-glow-blur) var(--color-glow);
|
||||
top: 0;
|
||||
}
|
||||
/* Fixed rather than absolute: these used to be document-sized layers that
|
||||
relied on background-attachment to keep the pattern still. Pinning them to
|
||||
the viewport gets the same effect while bounding the painted layer, and it
|
||||
covers the screen regardless of how tall the content is. */
|
||||
body::before,
|
||||
body::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(18, 16, 16, 0.1);
|
||||
background-attachment: fixed;
|
||||
opacity: 0;
|
||||
z-index: 2;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
animation: flicker 15s infinite;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
}
|
||||
/* Scanlines and aperture grille. repeating-linear-gradient with hard stops
|
||||
replaces background-size tiling: the grille used to be a smooth R->G->B
|
||||
ramp across 3px, where a real grille is three discrete stripes. */
|
||||
body::before {
|
||||
content: " ";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background:
|
||||
linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
|
||||
linear-gradient(
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
transparent 0 calc(var(--crt-scanline-pitch) / 2),
|
||||
var(--crt-scanline-color) 0 var(--crt-scanline-pitch)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 0, 0, 0.06),
|
||||
rgba(0, 255, 0, 0.02),
|
||||
rgba(0, 0, 255, 0.06)
|
||||
var(--crt-grille-r) 0 calc(var(--crt-grille-pitch) / 3),
|
||||
var(--crt-grille-g) 0 calc(var(--crt-grille-pitch) / 3 * 2),
|
||||
var(--crt-grille-b) 0 var(--crt-grille-pitch)
|
||||
);
|
||||
z-index: 2;
|
||||
background-size:
|
||||
100% 2px,
|
||||
3px 100%;
|
||||
background-attachment: fixed;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Phosphor flicker. The old version tinted with rgba(18, 16, 16, 0.1) at
|
||||
0.08-0.96 opacity: an effective alpha of 0.008-0.096 of a colour ~7%
|
||||
lighter than the background, i.e. a peak lift of under 2/255. It was
|
||||
invisible. Tinting with the accent at ~3% is small enough to cost no
|
||||
measurable contrast and large enough to actually see. */
|
||||
body::after {
|
||||
animation: crt-flicker 3s step-end infinite;
|
||||
background: var(--color-accent);
|
||||
opacity: var(--crt-flicker-base);
|
||||
}
|
||||
:root[data-reduce-motion="on"] body,
|
||||
:root[data-reduce-motion="on"] body::after {
|
||||
|
|
@ -132,197 +151,122 @@ body::before {
|
|||
animation: none;
|
||||
}
|
||||
}
|
||||
@keyframes flicker {
|
||||
@keyframes crt-flicker {
|
||||
0% {
|
||||
opacity: 0.27861;
|
||||
opacity: 0.03;
|
||||
}
|
||||
5% {
|
||||
opacity: 0.34769;
|
||||
7% {
|
||||
opacity: 0.024;
|
||||
}
|
||||
10% {
|
||||
opacity: 0.23604;
|
||||
13% {
|
||||
opacity: 0.041;
|
||||
}
|
||||
15% {
|
||||
opacity: 0.90626;
|
||||
19% {
|
||||
opacity: 0.028;
|
||||
}
|
||||
20% {
|
||||
opacity: 0.18128;
|
||||
26% {
|
||||
opacity: 0.019;
|
||||
}
|
||||
25% {
|
||||
opacity: 0.83891;
|
||||
31% {
|
||||
opacity: 0.036;
|
||||
}
|
||||
30% {
|
||||
opacity: 0.65583;
|
||||
38% {
|
||||
opacity: 0.026;
|
||||
}
|
||||
35% {
|
||||
opacity: 0.67807;
|
||||
44% {
|
||||
opacity: 0.048;
|
||||
}
|
||||
40% {
|
||||
opacity: 0.26559;
|
||||
51% {
|
||||
opacity: 0.022;
|
||||
}
|
||||
45% {
|
||||
opacity: 0.84693;
|
||||
58% {
|
||||
opacity: 0.033;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.96019;
|
||||
}
|
||||
55% {
|
||||
opacity: 0.08594;
|
||||
}
|
||||
60% {
|
||||
opacity: 0.20313;
|
||||
}
|
||||
65% {
|
||||
opacity: 0.71988;
|
||||
63% {
|
||||
opacity: 0.017;
|
||||
}
|
||||
70% {
|
||||
opacity: 0.53455;
|
||||
opacity: 0.039;
|
||||
}
|
||||
75% {
|
||||
opacity: 0.37288;
|
||||
77% {
|
||||
opacity: 0.027;
|
||||
}
|
||||
80% {
|
||||
opacity: 0.71428;
|
||||
83% {
|
||||
opacity: 0.044;
|
||||
}
|
||||
85% {
|
||||
opacity: 0.70419;
|
||||
}
|
||||
90% {
|
||||
opacity: 0.7003;
|
||||
}
|
||||
95% {
|
||||
opacity: 0.36108;
|
||||
91% {
|
||||
opacity: 0.021;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.24387;
|
||||
opacity: 0.03;
|
||||
}
|
||||
}
|
||||
@keyframes textShadow {
|
||||
/* Normalised from the original per-keyframe pixel offsets, divided by their
|
||||
max (3.8969px), so the rhythm is unchanged but the amplitude is now set in
|
||||
one place by --crt-aberration-max. */
|
||||
@keyframes crt-aberration {
|
||||
0% {
|
||||
text-shadow:
|
||||
0.4389924193300864px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.4389924193300864px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.113;
|
||||
}
|
||||
5% {
|
||||
text-shadow:
|
||||
2.7928974010788217px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.7928974010788217px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.717;
|
||||
}
|
||||
10% {
|
||||
text-shadow:
|
||||
0.02956275843481219px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.02956275843481219px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.008;
|
||||
}
|
||||
15% {
|
||||
text-shadow:
|
||||
0.40218538552878136px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.40218538552878136px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.103;
|
||||
}
|
||||
20% {
|
||||
text-shadow:
|
||||
3.4794037899852017px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-3.4794037899852017px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.893;
|
||||
}
|
||||
25% {
|
||||
text-shadow:
|
||||
1.6125630401149584px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-1.6125630401149584px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.414;
|
||||
}
|
||||
30% {
|
||||
text-shadow:
|
||||
0.7015590085143956px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.7015590085143956px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.18;
|
||||
}
|
||||
35% {
|
||||
text-shadow:
|
||||
3.896914047650351px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-3.896914047650351px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 1;
|
||||
}
|
||||
40% {
|
||||
text-shadow:
|
||||
3.870905614848819px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-3.870905614848819px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.993;
|
||||
}
|
||||
45% {
|
||||
text-shadow:
|
||||
2.231056963361899px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.231056963361899px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.573;
|
||||
}
|
||||
50% {
|
||||
text-shadow:
|
||||
0.08084290417898504px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.08084290417898504px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.021;
|
||||
}
|
||||
55% {
|
||||
text-shadow:
|
||||
2.3758461067427543px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.3758461067427543px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.61;
|
||||
}
|
||||
60% {
|
||||
text-shadow:
|
||||
2.202193051050636px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.202193051050636px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.565;
|
||||
}
|
||||
65% {
|
||||
text-shadow:
|
||||
2.8638780614874975px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.8638780614874975px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.735;
|
||||
}
|
||||
70% {
|
||||
text-shadow:
|
||||
0.48874025155497314px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.48874025155497314px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.125;
|
||||
}
|
||||
75% {
|
||||
text-shadow:
|
||||
1.8948491305757957px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-1.8948491305757957px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.486;
|
||||
}
|
||||
80% {
|
||||
text-shadow:
|
||||
0.0833037308038857px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.0833037308038857px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.021;
|
||||
}
|
||||
85% {
|
||||
text-shadow:
|
||||
0.09769827255241735px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-0.09769827255241735px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.025;
|
||||
}
|
||||
90% {
|
||||
text-shadow:
|
||||
3.443339761481782px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-3.443339761481782px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.884;
|
||||
}
|
||||
95% {
|
||||
text-shadow:
|
||||
2.1841838852799786px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.1841838852799786px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.56;
|
||||
}
|
||||
100% {
|
||||
text-shadow:
|
||||
2.6208764473832513px 0 1px rgba(0, 30, 255, 0.5),
|
||||
-2.6208764473832513px 0 1px rgba(255, 0, 80, 0.3),
|
||||
0 0 3px;
|
||||
--crt-shift: 0.673;
|
||||
}
|
||||
}
|
||||
header,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue