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
Simulates the texture of paper fiber. An SVG noise pattern covers the full viewport, warm-tinted to match the cream/ink palette, with separate versions for light and dark mode. Static, never animated. Zoom into any blank area on this page to see the fiber texture.
Anatomy
| Part | Selector | Description |
|---|---|---|
| Container | .paper-grain | Fixed full-viewport SVG holder |
| Light variant | .grain-light | Warm cream noise, darken blend |
| Dark variant | .grain-dark | Warm dark noise, lighten blend |
| Mobile fallback | .paper-grain-mobile | Pre-rendered PNG tile (currently hidden) |
Specs
Container:
.paper-grain {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 1;
display: block;
will-change: transform;
}
The will-change: transform hint encourages GPU compositing so the grain overlay doesn’t trigger repaints during scroll.
SVG filter chain (both variants share the same structure):
feTurbulencegenerates fractal noise:type="fractalNoise",baseFrequency="1.1",numOctaves="5",stitchTiles="stitch"feColorMatrixdesaturates to grayscale:type="saturate",values="0"feComponentTransferremaps the grayscale to warm tones via per-channel linear slopes
Light grain (.grain-light):
.grain-light {
opacity: 0.92;
mix-blend-mode: darken;
}
The feComponentTransfer remaps channels to the warm cream range:
| Channel | Slope | Intercept | Output range |
|---|---|---|---|
| R | 0.20 | 0.80 | 0.80 — 1.0 |
| G | 0.23 | 0.76 | 0.76 — 0.99 |
| B | 0.22 | 0.72 | 0.72 — 0.94 |
The blue channel sits lowest, giving the grain a warm golden cast. mix-blend-mode: darken means the grain only darkens pixels, adding subtle fiber to the cream surface without brightening anything.
Dark grain (.grain-dark):
.grain-dark {
display: none;
mix-blend-mode: lighten;
opacity: 0.92;
}
The feComponentTransfer remaps channels to the near-black range:
| Channel | Slope | Intercept | Output range |
|---|---|---|---|
| R | 0.12 | 0.04 | 0.04 — 0.16 |
| G | 0.115 | 0.035 | 0.035 — 0.15 |
| B | 0.105 | 0.035 | 0.035 — 0.14 |
Same warm bias (blue lowest), but mix-blend-mode: lighten flips the effect: the grain only lightens pixels, adding fiber texture to the dark surface.
Theme switching:
[data-theme="dark"] .grain-light {
display: none;
}
[data-theme="dark"] .grain-dark {
display: block;
}
Only one grain variant is ever visible. The swap is instantaneous (display doesn’t transition), which is correct because the grain is a surface property, not a content element.
Mobile fallback
On mobile (<= 768px), the SVG grain is hidden for performance. A pre-rendered PNG tile is available as .paper-grain-mobile:
.paper-grain-mobile {
display: none;
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
}
@media (max-width: 768px) {
.paper-grain {
display: none;
}
.paper-grain-mobile {
display: block;
background-image: url('/images/grain-light.png');
background-repeat: repeat;
background-size: 256px 256px;
mix-blend-mode: darken;
opacity: 0.92;
}
[data-theme="dark"] .paper-grain-mobile {
background-image: url('/images/grain-dark.png');
mix-blend-mode: lighten;
}
}
The PNG tiles are 256px squares that repeat seamlessly, matching the SVG output without the rendering cost.
Responsive behavior
| Breakpoint | Change |
|---|---|
<= 768px | SVG grain hidden, PNG tile fallback shown |
display: none !important |
Accessibility
Purely decorative. pointer-events: none ensures it never intercepts interaction. No ARIA attributes needed.
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
| Part | Selector | Description |
|---|---|---|
| Vignette | .vignette | Fixed 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
| Breakpoint | Change |
|---|---|
display: 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
| Part | Selector | Description |
|---|---|---|
| Container | .crop-marks | Fixed full-viewport positioning frame |
| Top-left | .crop-marks .tl | Corner bracket |
| Top-right | .crop-marks .tr | Corner bracket |
| Bottom-left | .crop-marks .bl | Corner bracket |
| Bottom-right | .crop-marks .br | Corner 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:
| Corner | Position | Borders |
|---|---|---|
.tl | top: 14px; left: 14px | border-top-width: 1px; border-left-width: 1px |
.tr | top: 14px; right: 14px | border-top-width: 1px; border-right-width: 1px |
.bl | bottom: 14px; left: 14px | border-bottom-width: 1px; border-left-width: 1px |
.br | bottom: 14px; right: 14px | border-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
| Breakpoint | Change |
|---|---|
<= 768px | display: none |
display: 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.
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:
| Element | Selector |
|---|---|
| Article title | h1 |
| Section heading | h2 |
| Prose subheading | .prose h3 |
| Light pull quote | .pull-quote-light |
| Drop cap | article.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
| Breakpoint | Change |
|---|---|
<= 600px | Drop cap disabled entirely (no text-shadow to apply) |
All 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
Layer Stack
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
Letterpress — dual text-shadow simulating debossed type
Composite