One résumé, two presentations — print CSS as progressive enhancement
TL;DR: one content model, expressed twice — an interactive screen and a printable page,
picked by CSS media targeting instead of two hand-maintained résumé documents. A dedicated /resume route covers “don’t make me learn your UI”; a fixed PDF covers “just give me a file.”
CSS was built, from day one, to let one document render as more than one thing depending on how it's asked for. CSS Zen Garden (Dave Shea, 2003) is still the cleanest proof of that: dozens of visually unrelated designs, one shared HTML file, nothing but the stylesheet swapped out. Twenty-plus years on, most sites still treat that as a party trick rather than infrastructure — one visual target for screen, and print left to whatever the browser does by default, if it's considered at all. Media queries are the part of CSS that makes the Zen Garden pitch practical rather than a demo: the same markup, different rules, picked automatically by context.
I ran into the unglamorous version of that pitch building this site's home page. It's a WebGL background, hash-target panels, and screen-only chrome — good for a portfolio, useless to anyone who wants to print my résumé, feed it to an ATS, or open a stable link without loading a canvas. The content underneath (About Me, Experience, Skills, Education) didn't need to change. How it's expressed per medium did. So rather than maintain a second résumé document by hand, I pushed the split down into CSS and let the same markup carry both jobs.
Concretely, that single content model is four Svelte components — AboutMe, Experience, Skills, Education — each written once and imported
wherever it needs to render. The home route composes them alongside the interactive panels and WebGL
chrome; /resume composes the same four, in résumé order, with none of that. Svelte isn't the
load-bearing part here — React, Vue, or any component framework that lets you write markup once and
instantiate it in more than one place would do the same job. The actual idea is “single source of truth
per section, multiple call sites”; a component is just the mechanism. The PDF, in turn, isn't a fifth
document authored by hand: it's what falls out of opening /resume and running its
Print / Save as PDF action, which goes through the exact printstyle.css described below.
One canonical markup, one canonical print stylesheet, and the PDF is a captured output of the two — not an
independently maintained artifact.
Screen CSS stays on screen
Layout for the interactive site lives behind an @media screen query, so print never has to
fight it rule-by-rule — it just doesn't apply. The résumé route uses the same trick for its own stacked
layout. When you print either page, those screen-only rules drop out entirely; there's nothing to override.
Print gets to undo everything screen did
printstyle.css is linked with media="print" on
both the home layout and /resume, and its job is the mirror image of the section
above: walk back every screen-only assumption before the résumé is even legible on paper. Concretely —
canvases (#particles, #udgy, #speed, and any raw <canvas>) get display: none, because there's no such thing as a printed
WebGL frame; the absolutely-positioned #cv panel gets forced back to position: static so it stops being pinned to a screen-sized viewport; body copy swaps to a
plain serif at a fixed point size instead of whatever scale the screen layout uses; and page-break-inside: avoid plus explicit widow/orphan counts on paragraphs and sections keep a
job entry or a bullet list from getting guillotined across a page boundary. None of it is exotic CSS — it's
a systematic “assume nothing about this element's screen state survives” pass, applied once and left alone.
The developer general's warning, 2026 edition
I'd rather say this up front than let the tidy explanation above imply more than it delivers. Page-break
handling is still genuinely inconsistent across browser print engines — a paragraph that stays intact in
one browser isn't guaranteed to behave identically in another. The downloadable PDF is a snapshot, not a
live view: if I update /resume and forget to regenerate the file, the two quietly drift apart
(a reminder to myself as much as a disclosure to you). And no CSS, print or otherwise, guarantees correct
ATS parsing — but that's the wrong layer to look for the guarantee in anyway. ATS software mostly doesn't
render CSS at all; it walks the DOM. What it actually needs is a well-structured, progressively-enhanced
semantic document underneath — real headings, real <address>/paragraph elements, a
sane linear reading order — with none of that depending on any stylesheet being present. That's
the same document these #cv sections already are; the print stylesheet is a nicety on top of
it, not a prerequisite for it.
Why /resume gets its own address
A shareable link shouldn't require the person on the other end to know they have to print an SPA panel to get anything useful out of it. /resume stacks the printable sections without WebGL or navigation chrome, exposes explicit Download PDF and Print actions, and carries its own Open Graph card so the link previews correctly wherever it's dropped. The fixed file /sebastian-ferreyra-pons-resume.pdf covers the fastest path — recruiters who want a file, not a browser print dialog.
So this was never really “build two résumés.” It's semantic HTML written once, then expressed for an interactive screen, a print medium, and a distraction-free page — the same separation of structure from presentation I'd tell a client to do, pointed at my own home page for once.
One content model. Two presentations, chosen by media, not duplication. A print stylesheet is a delivery format, not a fallback nobody tests.
If you're doing something similar to your own SPA-shell portfolio and would rather start from a working
reset list than reverse-engineer one from a stylesheet, ask — happy to share exactly what printstyle.css resets and why, beyond what's summarized above.
— Seb · get in touchferreyrapons.com