# Proposal document template

This template produces a multi-page US Letter PDF from a single hand-built HTML
file, rendered by headless Edge or headless Chrome and checked by a Python build
script.

It is the working project of Triumph Design System v4, the documents system. It
lives at `Proposal Decks\design-system\documents\templates\proposal-document`,
which is canon, and it is mirrored at
`Triumph Design System v4\templates\proposal-document`, which is a generated
copy. Copy it from the canon path. The system contract is `documents\agents.md`
and the shared stylesheet is `documents\document.css`.

The page grammar comes from the 2027 Ticketing Proposal, a 26-page document
approved on 2026-08-23. That document clones the header bar of the approved
2027 price sheet. Treat the grammar as settled. Change the content, not the
CSS.

The template ships a working document: render it before you edit anything and
you get six pages that pass every check.

## When to use it

Use this template for a multi-page proposal or report that circulates inside
the club and ends up as a PDF. A ticketing plan, a season pricing document, a
premium spaces review, and a departmental proposal all fit.

Don't use it for these:

- A single-sheet reference or price sheet. It stays in this system, but it is
  built on the `.sheet` variant in RULE 12, not on this multi-page template.
- A promotional or social graphic. Use `templates/matchday-flyer/` instead.
- Anything that ships as a web page. `document.css` targets an 8.5in by 11in
  sheet and nothing else.

## What is in the folder

The folder is a complete project. Copy it, rename it, and start.

| Path | Purpose |
|---|---|
| `document.html` | The page skeleton. Every component appears once, with placeholders. It links the three sheets and carries an empty per-document override block; the grammar lives in `_ds/document.css`, not in this file. |
| `document.config.json` | Title, PDF name, Notion page id, checks, and geometry. The only file the scripts read settings from. |
| `refresh.py` | The full cycle: pull the Notion source, diff it, download images, and render. |
| `archive/build_document.py` | Renumbers, builds the contents, resolves cross-references, renders, and checks the layout. |
| `archive/verify_document.py` | Page count, blank pages, the below-the-rule character count, key figures, and the banned-phrase scan. |
| `archive/check_completeness.py` | Content probes: one phrase per source topic. |
| `archive/fetch_notion_images.py` | Images only, when you don't want the full cycle. |
| `archive/docconfig.py` | Shared config loader. Nothing to edit. |
| `_ds/` | Vendored design system: `tokens.css`, `logos.css`, `document.css`, fonts, and brand images. |
| `_assets/` | Document images. Empty until you put something in it. |

The scripts live in `archive/` because a finished project shows the deliverable
and one archive folder, nothing else. The rendered PDF sits at the top level
beside `document.html`.

This template ships inside the published design host, so every `.py` file in it
is readable as plain text by anyone with the URL. That is a decision, not an
accident: the scripts hold no credentials, they read `NOTION_API_TOKEN` from the
environment, and the pipeline is meant to be legible. Keep it that way. A page
id, a key figure, or anything else you would not publish belongs in a project
copy of this folder outside the host, never in the template.

`_ds/` is a snapshot of the documents system taken when the template was built.
To refresh it, copy `tokens.css`, `logos.css`, `document.css`, `fonts/`, and
`images/` from `Proposal Decks\design-system\documents\` over it. Five items,
and there is no sixth. The old warning said four, not five, because the Print v2
root carries a `pattern.css` that could be copied by mistake; `documents\` has
none, which is the reason that warning is no longer an instruction.

Re-vendor from `documents\`, never from the standalone
`Triumph Design System v2` folder. That copy is a stale consumer snapshot: its
`tokens.css` is 15,101 bytes, carries no version stamp, and still declares a
banned `sans-serif` fallback on the display face.

## Where the CSS lives

`document.html` carries no grammar of its own. It links three sheets, in this
order, and nothing else:

```html
<link rel="stylesheet" href="_ds/tokens.css">
<link rel="stylesheet" href="_ds/logos.css">
<link rel="stylesheet" href="_ds/document.css">
```

`tokens.css` is the palette and the font metric overrides. `logos.css` is the
lockups. `document.css` is the document grammar: the page box, the cover bar,
the running chrome, the ten-token point scale, headings, tables, callouts,
figures, contents, step rows, and the one sheet variant. All three are byte
copies from `Proposal Decks\design-system\documents\`.

The `<style>` block left in `document.html` is for per-document overrides and
ships empty. If something in it would help a second document, it belongs in
`document.css` instead, whose `@version` stamp gets bumped in the same edit.
Never fork `document.css` to delete a class this document is not using: one such
prune is the whole reason the grammar was pulled out of the template in the
first place.

## Before you start

You need three things:

- Python 3 with PyMuPDF. Install it with `py -3 -m pip install pymupdf`.
- Headless Edge or headless Chrome at one of the paths in `render_exe`. Both
  are Chromium and both produce the same sheet; the build takes the first path
  in the list that exists.
- `NOTION_API_TOKEN` in the Windows user environment, but only if the document
  has a Notion source.

## Start a document from the template

1. Copy the whole `proposal-document` folder into the project tree and rename
   it after the document.
2. Rename `document.html` if you want to. If you do, update `source_html` in
   `document.config.json`.
3. Fill in `document.config.json`. Set `document_title`, `pdf_name`, and
   `footer_marker`. The footer marker is one word that appears in every
   interior footer. The build uses the marker to catch a missing footer.
4. Replace every `{{PLACEHOLDER}}` in `document.html`. The build prints a count
   of the ones you missed.
5. Set `--mast-chars` on the cover hero to the character count of the title,
   spaces included. That value is what makes the title span the header bar.
   Recount it whenever the title changes.
6. Add, remove, and reorder pages. Every sheet is one page div, and the first
   one carries `class="page cover"` and no footer. Give every page an `id`. Add
   `data-toc` only when the h2 is too long or too specific to read well as a
   contents line.
7. Cross-reference other pages as `{{p:page-id}}`. The build resolves the
   reference to a page number and fails on an id that no page carries, so a
   reference can never go stale.
8. Fill `key_figures` and `content_probes` in the config as you write. Add a
   key figure for every number that has to survive a rewrite, and a probe for
   every topic that has to reach the page.

## Wire a Notion source

The Notion cycle reads the source page, tells you what changed, and downloads
the images. It never rewrites the HTML, because the prose, the tables, and the
page breaks are hand-built.

1. Copy the page id out of the Notion URL. The id is the 32-character string at
   the end. Strip the dashes.
2. Put the id in `notion_page_id` in `document.config.json`.
3. Run `py -3 refresh.py`. The first run has no baseline, so it stores the pull
   as one.
4. On later runs, read the diff. Each changed line sits under the heading it
   came from, and the diff is your work list.
5. Edit `document.html` against that list, then run `py -3 refresh.py` again.

Notion serves image URLs as presigned links that expire about an hour after the
API returns them. The pull and the download therefore happen in the same run.
Never save a block dump and download from it later. Images land in `_assets` as
`notion-01.png`, `notion-02.jpg`, and so on, in document order, keeping
whatever format Notion stored.

## Render and verify

Run these three in order. Each one exits non-zero on a failure.

1. `py -3 archive\build_document.py` renders the PDF, then asserts four things
   about the layout:
   - The page count equals the number of page divs.
   - Every sheet after the cover carries its own footer, numbered to match.
   - No page draws below the footer rule.
   - No sheet carries content that spilled off the one before it.
2. `py -3 archive\verify_document.py` checks the rendered text: no blank pages,
   every key figure present, and no banned phrase anywhere.
3. `py -3 archive\check_completeness.py` checks the probes, one per source
   topic. This check catches a whole subject dropped while pages were being
   split.

To render without touching Notion, run `py -3 refresh.py --no-notion`.

The layout check reports a page that outgrew its 11in box. The fix is always to
move content to another page.

## Hard rules

Breaking one of these produces a document that looks like this class of
document but is wrong.

**White ground.** The paper is white. No dark background, no tinted sheet, and
no full-bleed color. Somebody prints this document and reads it at a desk.

**No weave, anywhere in this document class.** The template does not link
`pattern.css`, there is no copy of it in `_ds/`, and adding it back is not a
judgment call. The approved print reference carries no weave, so neither does
this document. The weave ban here is a paper rule; the continuous weave field on
a light screen ground is Screens v3 territory and has no bearing on a printed
sheet.

**No navy band masthead.** The cover is a header bar on white paper. The
full-color crest sits at the left, the kicker, title, and standfirst sit at the
right, and one rule runs under all of it. The dark band belongs to promotional
work.

**One Fabiola hero per document, at weight 400.** Fabiola Capitals carries the
cover title and nothing else. Its vertical fix is the `tokens.css` metric override
plus `text-box-trim:trim-both` and `text-box-edge:cap alphabetic`. Never add
margin, line-height, or a translate on top of that, because it double-corrects.

**Logos have a legibility floor.** A venue or partner mark with interior detail
stops reading at about one inch. Below that, set the name in type instead of
placing the logo.

**Print type floors.** Treat every row as a minimum, not a target:

| Element | Floor |
|---|---|
| h1, cover hero | 32pt to 36pt |
| h2 | 20pt to 24pt |
| h3 | 12pt to 14pt |
| Table head | 10pt |
| Body | 10.5pt |

**Spill before shrinking.** `.page` carries no `overflow:hidden` on purpose. A
page that outgrows its box spills, the check catches it, and you move content
to another page. Never reduce a type size to make content fit. Never let a
table run wider than the page. That triggers the browser's shrink-to-fit, which
scales the whole PDF and drops every size below the floor at once. The build
reports that scaling by name.

**Images stay in their native format.** Drop the source file into `_assets` and
reference it. Never re-encode it, never resample it down to preview it, and
never upscale it past its native pixels. Pick the figure width class so the
image lands above roughly 190dpi at print scale.

## Components

`document.html` demonstrates each component once, in this order:

1. **Cover.** Header bar, kicker, Fabiola title, standfirst, lede, an
   at-a-glance table, and a two-column contents block the build fills.
2. **Interior page.** Running head with the crest, eyebrow, h2, intro, body
   copy, a two-column table, a note, a list, a cross-reference, a callout, and
   a quote block.
3. **Financial table.** The full build, with a status column and a total row,
   followed by two worked examples side by side. Show the arithmetic. A reader
   who cannot follow a total line by line has to take it on trust.
4. **Inventory table.** The dense pattern: one row per unit, mono figures
   right-aligned, a quiet trailing column, and a total that ties to the summary
   above it.
5. **Figures.** A single centered image with a one-line caption and no frame,
   then a row of three images under one caption.
6. **Open items register.** Three columns: the item, what is open about it, and
   the next step. A next step that names nobody is not a next step.

## Failures and their causes

The failures you are most likely to hit are as follows:

| Message | Cause |
|---|---|
| `the sheet is scaled to N%` | Something is wider than 8.5in. Find the widest table, let its headers wrap, and shorten any cell that cannot break. |
| `sheet N has no footer` | The page has no `.pagefoot` div, or its text no longer contains `footer_marker`. |
| `sheet N starts with spill` | The previous page overflowed. Move content off it. |
| `page count N != M page divs` | Same cause. A page overflowed and produced an extra sheet. |
| `unknown page reference` | A `{{p:id}}` points at an id no page div carries. |
| `no renderer found` | Add the installed Edge or Chrome path to `render_exe` in the config. |
| `page N draws M characters below the footer rule` | Content spilled onto the footer. Cut a line of copy from that page. Never shrink the type. |
