Atmosphere

Five atmospheric layers transform the screen into paper. Vignette, grain, crop marks, and letterpress shadows work together to create the impression that this content exists on a physical surface, made by hand. None of these layers carry content. All are pointer-events: none. All sit outside the content flow as fixed overlays.

These layers aren’t decoration. They’re the medium. The fifth design principle: “The page is the material.” All five are active on this page right now.

For the full z-index stack and mode-switching behavior, see the implementation spec and spacing layout structure.


Paper grain

Purpose

A fine noise texture over the whole page, so the surface reads as paper rather than as a screen. Warm-tinted to the cream/ink palette, with separate light and dark versions. Static, never animated.

Where it lives

On the root element, as a tiled background, with the root’s background-color set to the paper’s own mean:

html {
  background-color: var(--paper);           /* #F3EFE3 light, #151312 dark */
  background-image: var(--grain-tile);      /* inline SVG feTurbulence, opaque */
  background-repeat: repeat;
  background-size: 256px 256px;
}

body { /* no background of its own — see below */ }

Three rules hold this together, and each was learned the hard way.

The tile is opaque. feTurbulence emits noise in the alpha channel as well as in colour, and for months nothing set feFuncA. The tile was therefore semi-transparent and the paper’s mean depended on that hidden alpha rather than on the colour tables. <feFuncA type='table' tableValues='1 1'/> makes it deterministic.

Its tables are centred on --paper. Fractal noise has mean 0.5000, so with a linear table the painted mean is (a+b)/2. Setting that to the paper colour makes the two equal by construction rather than by tuning.

body carries no background. This is not stylistic ⁠— see the next section.

Why the colour matters more than the texture

On iOS, the region above the status bar and the overscroll region below the page are not painted by CSS at all. WebKit’s LocalFrameView::documentBackgroundColor() blends <html>’s and <body>’s background colour and hands the result to WKWebView.underPageBackgroundColor; the overscroll area is the UIScrollView showing through, and the status-bar area is Safari chrome fed from the same value. That function’s own source comment says background images are “impractical to include” (WebKit bug 59540), and PageColorSampler skips any sample whose element carries a CSS background-image. The CSS Working Group tracks it as csswg-drafts#3640.

So those regions can never have grain. They can only have a colour ⁠— which is why the root’s background-color is the paper’s mean and why body must stay transparent: an opaque colour on body wins the blend above, and iOS would receive that one instead.

What does not work

Recorded because four attempts were made in one day, and each looked reasonable:

AttemptWhy it fails
position: fixed overlayFixed elements cannot paint the safe areas at all
inset: -20vh 0 and larger bleedsSame ⁠— those regions are outside fixed positioning
Tile on the canvas, colour left as --bgColour propagates to iOS, the image does not
background-attachment: fixedStrictly worse; confines the image to the viewport box

⚠️ Desktop WebKit cannot reproduce any of this. There is no UIScrollView in a headless browser, so a passing local test proves nothing about the phone. The device is the only check that counts.

Earlier history

2026-02 to 2026-09-10 ⁠— the tile that never matched. Below 768px the live filter was swapped for a PNG generated with sharp, to spare phones an expensive filter. sharp does not implement feTurbulence the way a browser does, and the tile was quantised to 64 colours, so the paper visibly changed character at the breakpoint.

Until 2026-09-10 ⁠— the overlay that never painted. The <svg class="paper-grain"> had no width/height attributes and no viewBox, making it a replaced element with no intrinsic size, so height: auto fell back to the CSS default of 150px instead of stretching between its insets. Pinned at inset: -20vh 0, that band sat entirely above the viewport on a desktop and clipped into the top ~18px on a phone. The grain rendered nowhere for months.

Cost

An opaque mean-locked tile narrows the noise range: grain spread went from 30 levels to 21. Verified by reproducing WebKit’s blend against the painted surface ⁠— every channel within 0.07 of a level, both engines, both themes.

Accessibility

Purely decorative and behind all content. There is no element, so nothing to hit-test.


Surface vignette

Purpose

A radial gradient that darkens the viewport edges, simulating the warm shadow a reading lamp casts at the periphery of a page. It creates a subtle focus pull toward the center where content lives. Stronger in dark mode to produce a pool-of-light effect. Glance at the edges of your screen ⁠— the gradual darkening is the vignette.

Anatomy

PartSelectorDescription
Vignette.vignetteFixed full-viewport gradient overlay

Specs

.vignette {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  will-change: transform;
}

The vignette sits at z-index: 0, the lowest atmospheric layer, beneath even the grain (z-index: 1). This means the grain texture is visible on top of the vignette shadow, which is correct: paper fiber exists on the surface, the lamp shadow is behind it.

Light mode:

.vignette {
  background: radial-gradient(
    ellipse at center,
    transparent 65%,
    hsl(40deg 30% 25% / 0.15) 100%
  );
}

The shadow color (hsl(40deg 30% 25%)) is a warm brown at 15% opacity. The 65% transparent stop means the center two-thirds of the viewport is completely clear. The vignette only appears in the outer third, as peripheral shadow.

Dark mode:

[data-theme="dark"] .vignette {
  background: radial-gradient(
    ellipse at center,
    transparent 65%,
    hsl(30deg 5% 3% / 0.3) 100%
  );
}

The dark mode shadow is nearly black (hsl(30deg 5% 3%)) at double the opacity (0.3 vs 0.15). This creates the pool-of-light reading effect: the content area glows relative to the darkened edges, as though reading by lamplight.

Responsive behavior

BreakpointChange
Printdisplay: none !important

The vignette remains visible at all screen sizes. On smaller screens the effect is more pronounced because the gradient covers more of the visible content, but this is acceptable because the opacity is low enough to never impede readability.

Accessibility

aria-hidden="true" on the element. Purely decorative. pointer-events: none prevents interaction.

Code example

<div class="vignette" aria-hidden="true"></div>

Crop marks

Purpose

Four corner brackets at the viewport edges, mimicking the registration marks on a printed proof sheet. A print production detail that signals intentional design. Someone proofed this. The four L-shaped brackets at the corners of your viewport are the crop marks described here.

Anatomy

PartSelectorDescription
Container.crop-marksFixed full-viewport positioning frame
Top-left.crop-marks .tlCorner bracket
Top-right.crop-marks .trCorner bracket
Bottom-left.crop-marks .blCorner bracket
Bottom-right.crop-marks .brCorner bracket

Specs

Container:

.crop-marks {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 99;
}

At z-index: 99, the crop marks sit above the content (z-index: 2) but below the scroll progress bar (z-index: 100). They’re always visible on top of the page, including over the grain and vignette layers.

Individual marks:

.crop-marks span {
  position: fixed;
  width: 18px;
  height: 18px;
  border-color: var(--text-2);
  border-style: solid;
  border-width: 0;
  opacity: 0.35;
}

Each corner uses selective border sides to form an L-shaped bracket:

CornerPositionBorders
.tltop: 14px; left: 14pxborder-top-width: 1px; border-left-width: 1px
.trtop: 14px; right: 14pxborder-top-width: 1px; border-right-width: 1px
.blbottom: 14px; left: 14pxborder-bottom-width: 1px; border-left-width: 1px
.brbottom: 14px; right: 14pxborder-bottom-width: 1px; border-right-width: 1px

The --text-2 color at 35% opacity makes the marks subtle but present. They use the text tier (not --ui) because they’re marks on the page, not structural dividers.

Responsive behavior

BreakpointChange
<= 768pxdisplay: none
Printdisplay: none !important

Hidden on mobile and tablet. The crop marks are a desktop detail that communicates craft when the screen is large enough to see them as corner accents. On mobile, they’d read as stray marks. Tested: shown at every width on 2026-09-10 in the name of one consistent surface, and taken back off the same day ⁠— at 390px they sit 14px into the corners, close enough to the text to read as marks on the screen rather than registration marks on a page.

Accessibility

Purely decorative. pointer-events: none on the container. No ARIA attributes needed. The marks carry no content.

Code example

<div class="crop-marks">
  <span class="tl"></span>
  <span class="tr"></span>
  <span class="bl"></span>
  <span class="br"></span>
</div>

Letterpress shadows

Purpose

Dual text-shadows on headings and pull quotes that create the illusion of type pressed into the paper surface. In light mode, a bright highlight sits below each character with a subtle shadow above, as if the type is debossed and catching light from above. In dark mode, the effect reverses: a deep shadow above and a faint highlight below. Every heading on this page demonstrates the effect.

Anatomy

Applied to these elements via text-shadow:

ElementSelector
Article titleh1
Section headingh2
Prose subheading.prose h3
Light pull quote.pull-quote-light
Drop caparticle.post.has-dropcap .prose > p:first-of-type::first-letter

Specs

Headings (h1, h2, .prose h3) ⁠— light mode:

text-shadow:
  0 1px 0 rgba(255, 252, 240, 0.7),
  0 -1px 0 rgba(16, 15, 15, 0.08);

The first shadow (0 1px 0) places a bright highlight 1px below each character using the page’s cream color (rgba(255, 252, 240)) at 70% opacity. The second shadow (0 -1px 0) places a dark shadow 1px above at just 8% opacity. Together they create the impression that each letter is pressed down into the surface, catching light on its bottom edge.

Headings (h1, h2, .prose h3) ⁠— dark mode:

[data-theme="dark"] h1,
[data-theme="dark"] h2,
[data-theme="dark"] .prose h3 {
  text-shadow:
    0 -1px 0 rgba(0, 0, 0, 0.4),
    0 1px 0 rgba(206, 205, 195, 0.06);
}

The effect inverts. The shadow above is now the dominant component (rgba(0, 0, 0) at 40%), and the highlight below is barely visible (6%). This matches the expectation that light in dark mode comes from within the screen, not above it.

Pull quote light ⁠— light mode:

.prose .pull-quote-light {
  text-shadow:
    0 -1px 0 rgba(0, 0, 0, 0.08),
    0 1px 0 rgba(255, 252, 240, 0.7);
}

The shadow order is the same as headings (shadow above, highlight below) but the values match.

Pull quote light ⁠— dark mode:

[data-theme="dark"] .prose .pull-quote-light {
  text-shadow:
    0 -1px 0 rgba(0, 0, 0, 0.5),
    0 1px 0 rgba(255, 255, 255, 0.06);
}

Dark mode pull quotes use a slightly stronger shadow (50% vs 40%) and a pure white highlight (vs. the warm off-white used for headings) because the lighter pull quote text weight needs more shadow definition.

Drop cap ⁠— light mode:

article.post.has-dropcap .prose > p:first-of-type::first-letter {
  text-shadow: 0 0 0.5px rgba(16, 15, 15, 0.12);
}

Drop cap ⁠— dark mode:

[data-theme="dark"] article.post.has-dropcap .prose > p:first-of-type::first-letter {
  text-shadow: 0 0 0.5px rgba(206, 205, 195, 0.08);
}

The drop cap uses a different technique: a zero-offset 0.5px blur at low opacity, simulating ink pooling where a large letter presses hardest into the paper. The shadow color matches the text color for each mode.

Responsive behavior

BreakpointChange
<= 600pxDrop cap disabled entirely (no text-shadow to apply)
PrintAll text-shadow removed (text-shadow: none)

The letterpress effect remains on headings and pull quotes at all screen sizes. It is subtle enough at mobile type sizes to be felt rather than seen.

Usage guidance

Do not apply letterpress shadows to body text, links, or UI elements. The effect is reserved for typographic landmarks: the elements that would be most deeply impressed on a letterpress page.

The shadow values are hand-tuned for the specific text colors and weights they accompany. Changing the heading color or weight without re-tuning the shadow will break the illusion.


Specimens

Paper grain — SVG fractal noise, darken blend (light) / lighten blend (dark)

Vignette — radial gradient, 15% opacity (light) / 30% (dark)

Crop marks — registration marks at viewport corners

Aa

Letterpress — dual text-shadow simulating debossed type

The Weight of Ink