Component Financial Report Layout

Research — Financial Report Layout

This file records why each decision in the financial-report-layout component was made. A generation agent producing a proforma, income statement, book valuation, or summary sheet should read this, then emit HTML/CSS that honours the constraints.

1. Why table-layout:fixed + explicit label width:25%

Problem: A compliance financial report is several separate <table> elements (Revenue, Costs, Capital, Returns), each with its own section heading and page-break behaviour. With default table-layout:auto, each table sizes columns to its own content, so "Y3" in the Revenue table lands at a different x-position than "Y3" in the Costs table. The period alignment — the entire point of a multi-year statement — is lost.

table-layout:fixed makes column widths a function of CSS rules, not cell contents. Every table.wide resolves the same widths: label at 25%, the remaining 75% divided across 11 data columns. Because the rule set is shared, every wide table gets identical geometry and columns align without per-table tuning.

The rule is restated inside @media print because some print engines re-resolve table layout at print time and would otherwise revert to auto, breaking alignment in the PDF.

Codegen rule: keep column count and label/data split identical across every table.wide in the same aligned group. A document may hold more than one aligned group (e.g. four 8-column per-entity statements plus a separate 11-column multi-year build-out). table.wide is the right tool for any set of same-shaped statements that should read as a uniform stack.

2. Why tr.total / tr.subtotal / tr.section-banner as semantic classes

These are roles, not styles. Encoding them as classes (not inline):

  1. Single source of visual truth — one edit changes every total.
  2. Machine-readable — a tool can find every total with tr.total.
  3. Non-colour differentiation — roles escalate by border weight and font-weight, not just fill, surviving greyscale print and colour vision deficiency.

Codegen rule: apply exactly one role class per emphasised row, on the <tr>. Banner rows use colspan="12" over the 12 authored content columns.

3. Why td.lnum is injected by JS, not server-rendered

Line numbers are a property of the rendered document, not the data. Baking them into source rows requires recomputation every time a row is added, removed, or reordered. Client-side injection after layout means the number is always correct for the document as rendered, continuous across all tables.

The !important flags on .lnum exist solely to win against the inherited tr.total / tr.subtotal / th backgrounds and weights — the gutter must read as a margin rule, not as a figure.

Codegen rule: emit data rows only. Append the injection script once at the end of <body>. Do not author .lnum cells. Do not count the gutter in your colspan values.

4. Why letter-landscape @page

A label plus 11 periods does not fit portrait at a legible size. Landscape is the compliance print standard for multi-year statements. Letter (not A4) because the audience is a North American securities context (BCSC); the regulator and filers print on letter stock.

Margins 1.5cm 2cm 1.5cm 1.5cm — the wider value on the bound/punch edge leaves room for holes without eating the gutter or first data column.

Codegen rule: do not change @page for a compliance financial report.

7. Why .page-break-before / .page-break-after as utility classes

Multi-statement forecasts routinely need individual statements on their own pages (e.g. Per-DHS rollup and Reconciliation each on a fresh page). The base component keeps tables whole (break-inside:avoid) and headings glued to their tables (break-after:avoid), but has no way to force a statement onto a new page.

.page-break-before{break-before:page;page-break-before:always} applied to a <h2> does this. It composes correctly with h2,h3{break-after:avoid} — the heading stays glued to its note + table at the top of the new page.

Codegen rule: add .page-break-before to the <h2> of any statement that must not share a page with the preceding section. Use sparingly — a forced break applied too liberally creates near-empty pages by orphaning a preceding section's trailing note. Do not use transform:scale() to fit content: Chrome computes page breaks on layout dimensions, not visual size.

5. Colour tokens and semantic meaning

ValueUsed byMeaning
#111body textPrimary ink
#555p, p.noteSecondary ink — narrative subordinate to tables
#333h3Sub-heading ink
#cccborders, h2 ruleHairline grid
#f5f5f5th backgroundHeader band
#aaa / #bbbtd.lnum ink (screen / print)Gutter numerals — faint, recedes
#d0d0d0 / #cccgutter right borderBoundary: gutter / content
#eef2f7tr.total fillHeaviest emphasis. Bottom line
#f5f7fatr.subtotal fillLighter emphasis. Intermediate sum
#e3edf7tr.section-banner fillMost saturated. Names a block
#1a2a44tr.section-banner inkDark navy — the only coloured text
#888tr.total top borderHeavy rule above bottom line
#aaatr.subtotal top borderLighter rule above intermediate sum
#666 / #ddd.footer ink / ruleCompliance notice, quietest block

The three emphasis fills are one cool-blue family at three saturations — the hierarchy reads as one coherent system. All three clear WCAG contrast against #111 / #1a2a44 in print.

Codegen rule: do not recolour a role. If a brand theme overrides, keep the three-saturation relationship intact.

6. Typography scale

ElementSizeRationale
body13px (11px print)Base; steps down in print to fit landscape
h11.25remDocument title; one per document
h21rem + hairline ruleStatement section
h30.9remSub-section, no rule
p0.82remNarrative — smaller than base, subordinate to tables
p.note0.78rem italicInline caveat
table0.76rem (10px print wide)Smallest legible; maximises columns per page
tr.section-banner0.74rem uppercaseHeader read at low height via uppercase + letter-spacing
td.lnum9px monospaceBelow data size; reads as metadata
.footer0.72remRequired but visually deprioritised

system-ui throughout (no web-font dependency); 'Courier New' monospace on the gutter reads as a ruled margin.

Codegen rule: do not enlarge p; do not shrink table type below 0.76rem screen / 10px print — figures clip below that with 13 columns on letter landscape.

Research trail

Done (10)

  • Extracted CSS, line-number JS, and HTML patterns verbatim from the delivered WCP V2 proforma (primary source; polished over two sessions).
  • Verified cross-table alignment depends on table-layout:fixed + shared 25% label width, and that the rule must be restated in @media print.
  • Confirmed !important on .lnum is required to override inherited total/subtotal/header backgrounds.
  • Confirmed the three emphasis fills are one blue family at three saturations and clear WCAG contrast against their text in print.
  • Confirmed print-color-adjust:exact is required for gutter and fills to survive print.
  • Confirmed colspan accounting: 12 authored content columns; gutter inserted by script outside the authored colspan.
  • Validated WeasyPrint 61+ as non-Chromium print engine (Building Portfolio V2, 2026-06-13): @page letter-landscape, break-before:page, table-layout:fixed cross-table alignment, and all semantic-row fills all render correctly. JS line-number gutter absent (WeasyPrint does not execute JavaScript). Use Chromium when line numbers are required; WeasyPrint for line-number-optional drafts and CI. print-color-adjust:exact warning logged but harmless.
  • Validated white-space-eliminating flow pagination (SPV Partnership JW1, 2026-06-21): forcing every section atomic on a landscape proforma where sections are shorter than the page strands a too-tall statement on its own page, leaving the page above it half-empty. Tagging the single tallest statement section.block.tall (allowed to flow across pages at row boundaries) while keeping all other sections atomic eliminated the gaps — 4 half-empty pages → 3 full pages.
  • Validated the .masthead + absolutely-pinned .draft stamp pattern (same source): the prior floated-stamp pattern let a long description wrap under the stamp; position:relative masthead + position:absolute stamp removes the stamp from flow entirely, so it cannot overlap regardless of description length.
  • Confirmed td.tbd (muted glyph) is the correct treatment for a figure not yet known — distinct from a blank cell (reads as nil to a reviewer) and from a fabricated placeholder number. Keeps the row and line number legible; excluded from computed subtotals/totals by convention, stated in the section's p.note.

Suggested (2)

  • Validate the letter-landscape margin asymmetry against a real binding/ hole-punch sample.
  • Two dashboard-theme token candidates surfaced (wcp.finance.draft.{ink,size,weight}, wcp.finance.tbd.{ink,glyph}) — not a DTCG change yet, per source; fold into the wcp-finance-bundle on a future design pass, not registered here.

Open questions (1)

  • Should td.lnum be semantically addressable (e.g. an id per line) so a reviewer's "line 42" can deep-link, or does it remain purely decorative?

Resolved (2026-06-21): repeating <thead> for tables taller than one printed page. Author each table with a real <thead>/<tbody> split and add thead{display:table-header-group} in print — this reprints the header row on every page the table spans. Confirmed it does not disturb the line-number injector: the injector runs once at load and numbers rows in document order, and table-header-group is a paint-time repeat of the same header row, not an extra DOM row.

Important Information

Design System disclosure

This site provides open-source design tokens, documentation, and self-hostable software published by Woodfine Capital Projects Inc. Information here is for general reference only and does not constitute an offer, warranty, or a guarantee of fitness for any particular purpose. Statements regarding planned, intended, or targeted future features are forward-looking and subject to change without notice; they are not undertaken to be updated except as required by law.