Who uses it: nobody, on screen — this is the artefact
the ERP emits: a purchase order sent to a vendor, an invoice, a
delivery note travelling with goods. How often: every
time a document is issued, usually unattended.
What "done" looks like: a page a stranger can read
without your system — sender, recipient, document number, lines,
totals, terms — that survives pagination and a black-and-white printer.
Not a screen, and not a PDF engine — if a person needs
to work with the record, that is the
object page; and rendering
the actual PDF is your server's job (see the grill verdict below), not
this framework's.
Does this belong in a CSS framework at all? — the grill verdict
The Accept for this pattern asked the question outright, and the
honest answer is half yes, so it is written down rather
than assumed:
Not ours: producing the PDF. Headless Chrome,
Gotenberg, wkhtmltopdf, a print server — that is infrastructure, it
belongs to your stack, and a CSS framework that shipped one would be
the virtualiser mistake again.
Ours: what every one of those tools consumes —
HTML and a print stylesheet. The document's shape, the page
contract (@page margins, repeating table headers and
totals, break control) and the black-and-white legibility rules are
exactly this framework's job, and they already ship in
the print layer.
So this page documents the contract, not a
generator: give your renderer this markup and these classes, and the
output is a document. Zero new CSS was needed to build it — which is
itself the evidence the contract was already complete.
Acme Supply Co. 221 Industrial Way Manchester · UK
Deliver to
Dock 4, 123 Warehouse Rd Rotterdam · NL
Payment terms
Net 30
Incoterms
DAP Rotterdam
Requested delivery
15 Sep 2026
Buyer
J. Kim
Order lines for PO-88213
Material
Description
Qty
Unit price
Net value
MAT-4471
Hydraulic pump, 24V
12
770.00
9,240.00
MAT-1180
Seal kit, 60 mm
40
37.01
1,480.50
MAT-9002
Filter cartridge
150
54.80
8,220.00
Net total
18,940.50
VAT 21%
3,977.51
Total EUR
22,918.01
On paper this is one document, not a
screenshot of a screen: the table header repeats on every page and so
do the totals (<tfoot> is
table-footer-group by browser default — measured, not
assumed, which is why the framework adds no rule for it), the address
blocks never split (.bo-u-avoid-break), and the print-only
line appears only on paper.
Logo & barcode — any mark, any symbology
Both are supported the same way, and the split is the same as the
PDF one: the framework does not generate either —
barcode bitmaps come from a library (bwip-js, ZXing, a GS1 service) or
a barcode font, and a logo is your asset. What decides whether the
printed result actually works is placement, and that is
entirely CSS:
Rule
Why it decides success
.bo-u-print-exact on the mark
The one that breaks scanning. Browsers lighten fills and drop backgrounds when printing; a barcode whose bars come back grey does not scan, and a logo loses its mark. This utility says "these colours are content".
Quiet zone — blank margin around the symbol
Every symbology requires one (roughly 10× the narrow-bar width for Code 128, 4 modules for QR). A barcode flush against a table border is unreadable to the scanner even though it looks fine.
Physical units (mm), not px
A barcode has a real-world minimum size. px is a screen unit and its printed size depends on the renderer's DPI assumption; mm means the same thing on every printer.
SVG for the symbol where possible
Vector bars stay crisp at any DPI. For a raster barcode add image-rendering: pixelated so the renderer does not smooth the bar edges into each other.
.bo-u-avoid-break on the block
A symbol split across a page boundary is two unreadable halves.
Human-readable text under the symbol
Both the two-channel rule and the actual GS1 standard: the number must be printed as text as well, so a human (or a failed scan) can still read it.
PO-88213
BO
Logo · 26mm
<!-- Barcode: your renderer supplies the image; these classes and units are what make the printed result scan. --><figure class="bo-u-print-exact bo-u-avoid-break"> <img src="/barcode/PO-88213.svg" alt="Barcode for document PO-88213" style="inline-size: 48mm; block-size: 14mm; padding: 3mm" /> <figcaption>PO-88213</figcaption> <!-- human-readable, always --></figure><!-- Logo: same utility, physical size, real alt text. --><img class="bo-u-print-exact" src="/brand/logo.svg" alt="Busy Office Supplies Ltd" style="inline-size: 26mm" />
This closes a loop the framework already had
open on the other side: initScanInput() on the
RF goods-receipt screenreads these barcodes off a handheld. The document that gets
scanned in the warehouse is the one this pattern emits — emit and read
are now both documented.
Page numbers & running headers — what browsers do, and what needs a renderer
Compared against the dedicated paged-media engines
(Prince,
WeasyPrint),
which is what the CSS Paged Media and GCPM specs were written for.
Two of their headline features matter for an ERP document, and they do
not land the same way — measured here by printing a
three-page table to PDF and reading the text back out, not by
consulting a support table:
Feature
Browser print / headless Chrome
Prince · WeasyPrint
@page margin box with counter(page) / counter(pages)
Works — rendered "1 of 3", "2 of 3", "3 of 3" on the right pages
Works
string-set + content: string(…) running headers
No — emitted nothing at all in the same test
Works (GCPM)
Repeating <thead> / <tfoot>
Works — header and totals repeated on all 3 pages
Works
So page numbering is available to you today, in a
plain browser, with no renderer and no JavaScript. It is not shipped as
a framework default because @page is a
document-global rule — it cannot be scoped to one route or one
element, so shipping it would stamp page numbers on every print job in
every consuming app, including screens where that is wrong. It belongs
in the stylesheet of your document route:
/* your-document-route.css — paste as-is; verified in Chrome */@page { margin: 1.6cm; /* the framework already sets this */ @bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 9pt; } @bottom-right { content: "PO-88213"; } /* the doc number on every page */}/* Server-renderer ONLY (Prince, WeasyPrint) — a running header that repeats the document title. Browsers ignore string-set entirely, so never rely on it for a document a user might print from the app. */h1 { string-set: doctitle content(); }@page { @top-center { content: string(doctitle); } }
The document above uses a plain
.bo-u-print-only line carrying its number instead, because
that works in both paths — the browser print dialog and the
server renderer — and a loose page from a printed PO still identifies
itself. Add the margin-box recipe on top when the document is rendered
by a route you control.
Markup
<!-- The whole document is one .bo-print-report. No new classes: letterhead + parties + kv meta + lines + tfoot totals + terms. --><article class="bo-print-report"> <header class="bo-u-avoid-break"> …sender / doc number / date… </header> <div class="bo-form-row bo-u-avoid-break"> …vendor / deliver-to… </div> <dl class="bo-kv bo-u-avoid-break"> …terms, incoterms, buyer… </dl> <div class="bo-data-table-container" tabindex="0"> <!-- screen: narrow viewports scroll the lines like a PDF; print ignores it --> <table class="bo-data-table" data-density="compact"> <thead>…</thead> <!-- repeats on every printed page --> <tbody>…lines…</tbody> <tfoot>…net / VAT / total…</tfoot> <!-- repeats too, by default --> </table> </div> <footer class="bo-u-avoid-break"> <p>Terms…</p> <p class="bo-u-print-only">Page totals continue overleaf · PO-88213</p> </footer></article>
Anatomy
Letterhead — who is sending, with the legal
identifiers (VAT/company number) a document needs to be valid.
Document identity — type, number, issue date.
The number is the thing every later conversation quotes. Not itself
a monitoring signal — the human-monitoring surface for WHETHER
documents were emitted lives on report
and job monitor; this
region is what makes an artefact self-evidencing once someone does
open it.
Parties — vendor and ship-to as separate blocks,
each .bo-u-avoid-break: an address split across a page
boundary is a returned delivery.
Lines + totals — a
data table with a real
<tfoot>; both header and totals repeat per page
because the print layer and the browser default already say so.
Terms & footer — the legal tail, plus
.bo-u-print-only content that exists on paper only.
Data contract
Request
Returns / on failure
GET /po/:id/document
this HTML, standalone (no app shell, no nav) — a complete page your renderer or a browser can print directly. 404 if the record does not exist; 5xx if rendering itself fails — both as a plain error page, never a broken document that LOOKS complete
GET /po/:id/document.pdf
your server runs the SAME HTML through its renderer; the PDF and the printed page cannot disagree because they are one source
issue / re-issue
a re-issued document states it ("Copy · re-issued 24 Aug 2026") — an unmarked duplicate of a financial document is a control failure
draft
a document that is not yet issued must SAY so on its face (see states) — never render a draft that looks issued
The AI-manages / human-monitors
split, stated: agents generate and dispatch these unattended,
so the document itself carries every check a human later needs — the
number, the issue date, the draft/copy marking, the totals. The
monitoring surface for whether they were emitted is the
report and the job monitor; this
page's job is that each emitted artefact is self-evidencing.
States
State
What ships
Draft
the word DRAFT in the document identity block and in a print-only line — never a colour wash or a watermark image, which a black-and-white printer or a forced-colors reader loses entirely
Issued
issue date present; the document is the record of what was sent
Copy / re-issue
marked in text with the re-issue date beside the original
Cancelled
stated in words at the top; the lines stay readable because a cancelled document still has to be auditable
Multi-page
header and totals repeat; parties and terms blocks never split; the print-only continuation line carries the document number so a loose page is identifiable
No lines
refuse to emit — a document with no lines is a bug upstream, not an empty state to style
No JS
fully functional — this page has no JS dependency; it's a rendered, printable document (the initScanInput() mentioned here belongs to the separate goods-receipt screen that later reads its barcode, not to this page)