About me

Selected work

Experience

Skills

Archive

Selected Writing

Education

One résumé, two presentations — print CSS as progressive enhancement

Technical note · 2026

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

← Selected Writing Home Résumé