← Back to the build log
Chapter 02 · April 2026

Faking paper in CSS.
Stains and all.

This chapter renders the Lab as it was — painted entirely in CSS. The surface you’re reading on right now is the old one.

The five layers.

No photographs. Everything below was drawn from CSS gradients, SVG filters, and a lot of careful stacking.

  1. 1
    Lunar grey ground
    Flat oklch(0.78 0.020 70). Warm enough to read as paper, not steel.
  2. 2
    Graph paper grid
    12 px minor + 60 px major lines in aubergine ink. Two stacked linear gradients.
  3. 3
    Worn zones
    Four radial-gradient masks (composited intersect) faded the grid in patches. Someone wrote and erased here.
  4. 4
    Desk lamp
    Warm linear gradient anchored to the top of the body, fading over 90 vh. The room was lit from above.
  5. 5
    Stains
    Inline-SVG data URIs. Tea ring. Ink blot. Smudge. Each mix-blend-mode: multiply.
  6. 6
    Fibre noise
    feTurbulence at low opacity, multiplied. What every fake-paper surface reaches for eventually.

Where it got us.

The best we could do with code alone. Convincing-ish. Structured. Aligned with the grid — the grid was the grid, because we painted both.

And still, it read as CSS doing paper rather than paper. Zero fibre. Every stain in a fixed position. Every worn zone a radial gradient with perfectly predictable edges. Graph lines running in mathematical straights.

Why we left.

The mockup at /mockup/paper-tile put a 120×120 scan next to this. The scan won immediately.

Fibre is the signature. Warm-cool micro-variation, anisotropic strands, and the way light catches tiny peaks — CSS can approximate warmth and grain, but the character of paper lives in a real scan and nowhere else.

What survived.

The grid stayed, softened. The desk-lamp metaphor became the dark modefuture. The stains, worn zones, and fibre noise all retired — the photographic paper made each of them redundant.

Raw notes