raster
2026-09 · Card
A poster card whose dot field works out its own shape. Every dot carries nothing but its grid coordinate; CSS derives whether it belongs to the pattern, which of two tones it takes, and where it sits in the pulse travelling through the field.
Default — ring field, resting pulse
raster
Active — the same field, pulse held open
raster
Pinwheel — arms from bearing plus radius
raster
Halftone disc — light theme, corner clusters
raster
Scan — every dot on, pulse axis turned diagonal
raster
An extraction, not a copy. The reference supplies a construction rule rather than a layout: a square field of discrete dots where every dot's state is decided from where it sits, over a quiet typographic footer that does none of the shouting. The product name, the wordmark, the logo and the copy are exactly the parts that had to go — the mark here is a 2x2 of circles that follows the field's own rule and nothing else: dots on a pitch, never wider than the pitch they sit on. It started at 1.065 of its pitch, which is to say the dots overlapped, and read as a blob at every size under 20px; at 0.78 they are four dots with a 1.4px gap at the 14px the footer renders them at, and still one mark rather than four. The field itself runs 0.46 at rest and 0.88 at peak, so the mark is inside its own vocabulary either way. The headline is placeholder text that describes the mechanism instead of selling anything. The set's three-up presentation went too: the three posters are three values of one component, so they are patterns on a modifier, not three components.
Each of the 289 <i> elements holds nothing but --x and --y, running −8..8 with 0,0 on a true centre cell (the field is odd-numbered for exactly that reason). component.css derives the rest: max(|x|,|y|) for the square-ring index, hypot() for the radius, atan2() for the bearing. A pattern is then three formulas on .raster-pulse__dot — --on (is this dot in the shape), --tone (which of the two colours), --phase (0..1, where it sits in the wave) — which is why --pinwheel, --halftone and --scan are one class on the root and not one grid each, and why none of it needs JavaScript. The booleans are arithmetic: clamp(0, v 1000, 1) steps a signed test to 0 or 1, max() is OR, min() is AND — that is the whole of the halftone's "inside the disc, or past 6.5 on both axes at once". abs() is written the long way as max(v, -1 v) on purpose: everything else here (hypot, atan2, sin, mod, round) is Chrome 125 / Safari 15.4 / Firefox 118, and abs() alone would have moved that floor to Chrome 133.
Every dot runs the same three-stop keyframe at the same duration — one set per state, alpha at rest and colour when active, the same shape in two currencies — and the travelling wave is one declaration: animation-delay: calc((var(--phase) - 1) var(--spread) var(--dur)). The - 1 makes the delay negative, so the field is already mid-pulse on the first frame instead of sitting dark for a cycle. The bright window is 0–16% of the cycle against a 28% recovery — a symmetric curve reads as the whole field breathing, and only a narrow peak reads as a ring moving through it. Because the wave lives entirely in --phase, --scan is that one formula turned diagonal — same field, same keyframe, and the shape becomes motion.
Resting, a dot cycles between its own tone pulled 60% toward the field background and full strength, at 6.4s. On hover, --open, or :active, the pair swaps: the trough becomes the dot's own tone and the peak becomes the other tone, at 1.9s — so a band of the second colour crosses the field instead of a band of light. The two halves are paid for differently, and that is a decision rather than an implementation detail: a tone pulled toward the field background is alpha, so the resting wave is one static colour and an opacity ramp, while the hue swap — which alpha cannot say — animates the colour itself. Only the second is expensive, and it only ever runs on the card under the pointer. The resting field costs nothing, where animating a var()-derived background-color across 289 dots cost a whole main thread in five thumbnails nobody was looking at. Alpha carries a little more chroma than the same mix in oklab, which is the 60% against the 58% that drew this field before — near enough to sit unnoticed, far enough to be worth writing down. Both tones come out of one color-mix() keyed on --tone, and its inverse, so no state needs a second selector per colour. This is also the answer to (hover: none): the component's whole second half lives in that state, so a press holds it for as long as it is held.
container-type: inline-size on the root and every footer metric in cqw — headline 8, footer padding 6.4, brand gap 1.9, registration marks 2.6. That is what makes the thumbnail the same poster at 312px that the demo is at 352px and the phone is at 272px; set in rem the headline held its size while the frame shrank and walked out of its band. It also means --raster-pulse-width is a single-value rescale of the whole footer, which is how the thumbnail is sized.
The mark lives in the footer beside the wordmark and nowhere else. It was a 3-cell knockout badge on the centre cell first, which is the emblem-on-poster gesture the source makes — and it was the one thing on that field the grid did not derive. Taken out, the ring's hollow centre is the field's own rather than a square hole punched over it, the pinwheel's arms converge where you can see them do it, and the scan runs edge to edge. The only marks left on the artwork are two registration brackets, a pair down one edge: that reads as a crop mark, where all four read as a frame around the type, which is a different and worse idea.
notes.md — the full record, including what was tried and dropped