print-craft — the page that prints
Stage: Phase 10 — Craft (last-2%) - Reads: design/SITEMAP.md (which routes are document-shaped), design/SYSTEM.md §type, design/DIRECTION.md - Writes: app/print.css (imported into app/globals.css), print-only header markup on document pages
Standard
Every site ships a lean print reset; a small number of document-shaped pages earn a designed print layout. The reset is not optional — a legal page or invoice sent to the printer with the browser default (nav bars, a cookie banner across the top, gradient headings printing as gray mud, links that read Datenschutz with no URL) is a broken surface on a real, auditable user path. Restraint applies here too: most pages need the reset, not a design project. A marketing landing page does not deserve a print layout — nobody prints it, and forcing one violates the constitution for zero reader benefit. Spend the craft only where a human actually hits Ctrl-P or "Save as PDF": legal pages, contracts, quotes, invoices, case studies, annual reports.
The DACH reason this skill exists at all: in German-speaking business practice the Impressum, AGB, and Datenschutzerklärung are routinely printed or saved to PDF as compliance records — a user keeps the Datenschutzerklärung as evidence of what they were told, an accountant keeps the Rechnung and Auftragsbestätigung under GoBD retention, a Mandant keeps the engagement letter. Those pages get a real page. This is the last-2% jurors never see but the client's lawyer does.
Print is monochrome and paginated — two facts the screen design ignored. Design for both: black ink on white paper, and content that breaks across pages without severing a heading from its clause or a table from its header row.
Wiring
Keep print rules in their own app/print.css and import it after Tailwind so they stay greppable and unlayered:
/* app/globals.css */
@import "tailwindcss";
@import "./print.css"; /* unlayered — beats Tailwind's @layer utilities without an !important war */
In Tailwind v4 every utility lives in @layer utilities, and an unlayered declaration wins over any layered one regardless of source order — so plain rules in print.css override flex, bg-*, shadow-* inside @media print with no !important needed (reserve !important for the color-adjust reset alone). Everything below lives inside one @media print { … } block.
The lean reset — every site gets this
@media print {
/* ink economy: kill the screen's color, shadow, and texture */
*, *::before, *::after {
background: transparent !important;
box-shadow: none !important;
color: #000 !important;
text-shadow: none !important;
}
body { font: 12pt/1.5 Georgia, "Times New Roman", serif; margin: 0; }
/* hide the chrome — nav, footer, consent, floating widgets, motion */
header nav, footer, [data-print="hide"],
.cookie-banner, [role="dialog"], .back-to-top, .share-bar,
video, [aria-hidden="true"] { display: none !important; }
/* reveal content the screen collapsed, or the printed AGB loses half its text */
details { display: block !important; }
details > summary { display: none !important; }
[hidden] { display: revert !important; }
a { text-decoration: underline; }
img { max-width: 100% !important; }
@page { size: A4; margin: 18mm; }
}
print-color-adjust (with -webkit- prefix) defaults to economy — the browser drops your dark backgrounds for you. Only set it exact on the one element whose real color is load-bearing (a logo), never globally. Note print-color-adjust: exact on that logo is the counterpart to the global background: transparent above — the exception, not the rule.
Designed variants — for document-shaped pages only
Pick the variant from what the page is (SITEMAP.md flags it). Each adds to the reset; none replaces it.
Legal-Document — Impressum, AGB, Datenschutzerklärung, an engagement letter (Mandatsvereinbarung). A print-only letterhead carrying the firm's legal identity, section rules that survive a page break, and expanded URLs so the printed authority links are actually usable:
@media print {
.print-letterhead { display: block !important; border-bottom: 1pt solid #000;
padding-bottom: 6pt; margin-bottom: 18pt; font-size: 10pt; }
h2, h3 { break-after: avoid; } /* a heading never sits alone at a page foot */
section, .clause, dl > div { break-inside: avoid; }
p { orphans: 3; widows: 3; }
/* expand only meaningful links; nav, buttons, in-page anchors stay clean */
main a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; word-break: break-all; }
a[href^="#"]::after, a[href^="mailto:"]::after, a[href^="javascript:"]::after { content: ""; }
}
The .print-letterhead block is display: none on screen and reveals only in print — put the firm name, Registergericht, HRB, and USt-IdNr there so the saved PDF is self-identifying without the site nav.
Case-Study / Portfolio — one project per sheet, so a case study reads as a discrete document: .case { break-before: page; } (the first stays put via .case:first-child { break-before: auto; }), figures held whole with figure { break-inside: avoid; }, captions kept with their image via figcaption { break-before: avoid; }. Strip the imagery treatments — a duotone hero is ink-hog and meaningless in gray.
Pricing / Quote — an invoice, Angebot, or Auftragsbestätigung. Add a print-only "Prepared for {name} · {date}" line and a generated timestamp so the paper artifact is dated and attributable; right-align the figures in tabular-nums; repeat table headers across pages with thead { display: table-header-group; } and hold each line item with tr { break-inside: avoid; }.
Pagination — and the page-number trap
break-inside, break-before, break-after (modern; page-break-* is the legacy alias), plus orphans/widows, are well supported in browser print. @page margin boxes are not. The advice you'll find everywhere — @page { @bottom-right { content: counter(page) } } for page numbers — silently renders nothing in Chrome, Firefox, or Safari print; it only works in a paged-media engine (Paged.js, Prince, a server renderer). So:
- For page numbers, rely on the browser's native footer — the print dialog already offers "page X of Y" and a date, and the user keeps them on precisely when saving an Impressum or Rechnung for records. Don't fight it.
- If a client genuinely needs CSS-controlled numbering or a repeating printed header on every page, that crosses out of zero-dependency scope into Paged.js or a server-side PDF route — flag it as a real decision, don't fake it with
position: fixed(which repeats unreliably across engines).
Anti-patterns
- No
@media printblock at all on a site with/impressum,/agb,/datenschutz, an invoice, or a quote — the browser default is a defect on a compliance path. Grep the codebase;print.cssshould exist. @page { @bottom-right { content: counter(page) } }or@top-center { … }— margin-box counters are ignored by every browser's print engine. Dead code that looks like it works.- Blanket
a[href]::after { content: attr(href) }— expands nav, buttons,href="#", andmailto:into URL noise. Scope tomain a[href^="http"]and null out the rest. - Content trapped in a collapsed
<details>, tab, or[hidden]that prints empty — the AGB's own clauses vanish. Forcedetails/[hidden]open in print. print-color-adjust: exactset globally to "preserve the design" — forces every dark background onto paper and drains a cartridge. One element, one reason.- Fixed
pxfont sizes in the print block — print type is measured inpt; px anchors to a screen assumption. - A duotone/grain/mesh treatment left on for print — costs ink, says nothing in monochrome. Strip it (see
ultraweb:imagery).
Worked example — Ledger & Lane, a printable Impressum and engagement letter
Moved to references/example.md — read only when this build's case is genuinely ambiguous; the sections above are the decision material.
Composes with
Moved to references/composes.md — the handoff map; load it when orchestrating this skill against its neighbors.