# Template specification

Every template in `templates/` obeys this contract. It exists so that a human can
skim any template and know where things are, and so that an agent can adapt one
without reading the whole file first.

**What these are.** Conventional, modern, production-ready website templates —
one per category. Somebody running a dental practice, a coffee roastery or a B2B
SaaS should be able to copy the folder, swap the name, the copy and the photos,
and ship it. They are meant to be *recognisable*: a visitor should know within a
second what kind of site they are on and where to click.

**What they are not.** They are not exhibits. An earlier version of this library
built every page out of an authored artefact — a tide table, a hex dump, a
bathymetric chart — and made that artefact the page structure. That produced
fifty-six things that were interesting to look at and useless to deploy. The
artefact-as-page approach is now banned outright (§5). Distinctiveness comes from
type, colour, photography and proportion, not from making the visitor decode a
diagram.

---

## 1. File layout

```
templates/<slug>/
├── index.html        # one complete page, no build step
├── styles.css        # all styles; @import './fonts/fonts.css' on line 1
├── motion.js         # progressive enhancement only — the page works without it
├── template.json     # machine-readable manifest (schema below)
├── AGENTS.md         # how an agent adapts this template
├── icons.svg.html    # inline <symbol> sprite (source; already pasted into index.html)
├── ICONS.txt         # icon set licence
├── IMAGES.md         # each photograph, its source and its licence
├── img/              # vendored photography, downscaled
└── fonts/
    ├── *.woff2       # self-hosted, latin + latin-ext
    ├── fonts.css     # generated @font-face block with unicode-range
    └── OFL.txt       # licence + attribution
```

`<slug>` is lowercase kebab-case, names the **category** rather than a brand, and
matches `template.json.slug`: `dental-practice`, `saas-landing`, `coffee-roaster`.

**Self-contained.** A template must render correctly when its folder is copied
anywhere, opened as `index.html`, with no network access and no build step. No
CDN links, no shared stylesheet, no framework, no npm dependency.

---

## 2. Fonts

Never use a system display face. Fonts are already fetched into every template
and **the pairing is fixed** — it is what keeps fifty-six templates from reading
as one. `scripts/audit-uniqueness.mjs` fails the build if two templates share a
pairing, so do not change `fonts/` or the `fonts` block in `template.json`
without checking the audit afterwards.

If you do need a new face:

```bash
node scripts/fetch-fonts.mjs templates/<slug>/fonts "Family:wght@300..700" "Other:ital@0;1"
```

The script writes the `.woff2` files **and** `fonts/fonts.css` with correct
`unicode-range`, carrying `font-stretch` through for any family with a `wdth`
axis — without that descriptor every width instance silently collapses to 100%.
Then, in `styles.css`:

```css
@import url('fonts/fonts.css');
```

Preload the two faces used above the fold:

```html
<link rel="preload" href="fonts/display.woff2" as="font" type="font/woff2" crossorigin>
```

`fonts/OFL.txt` is the filename the lint expects, but it is the *licence* file,
not a promise about which licence: a handful of Google families ship under
Apache-2.0 rather than the OFL. Check which you have — the directory a family
lives in at `github.com/google/fonts` (`ofl/`, `apache/`, `ufl/`) is the answer —
put the right full text in the file, and record it in `template.json.fonts`.
Take the upstream copyright line from that same file rather than typing a
designer's name from memory.

Keep the total woff2 weight per template near **250 KB**. The usual cause of
overshooting is an axis nothing on the page uses: an `opsz` range doubles a
family's size, and it earns that only if the face is genuinely set at both
display and text sizes. Drop the axis before you drop a subset.

Variable fonts are strongly preferred. Axis order in the query string is
lowercase alphabetical, then uppercase (`ital,opsz,wght,SOFT,WONK`), and when
`ital` is present every variant needs a full tuple:
`@0,9..144,100..900;1,9..144,100..900`.

---

## 3. `template.json`

```jsonc
{
  "slug": "dental-practice",
  "name": "Fielding Dental",              // the invented business the demo copy is about
  "category": "Dental practice",          // the plain-English category this template is for
  "tagline": "Family dental practice — services, team, fees and online booking.",
  "world": "Calm clinical blue on white, generous whitespace, rounded cards, real photography of the practice.",
  "mode": "persuade",                     // persuade | operate | read | experience
  "vertical": ["health-wellbeing"],       // exactly one, from the list below
  "tags": ["healthcare", "local-business"],  // free-form, search only
  "scheme": "light",                      // light | dark
  "palette": { "ground": "#ffffff", "ink": "#111827", "accent": "#0b6bcb" },
  "fonts": {
    "display": { "family": "Instrument Serif", "licence": "OFL-1.1" },
    "text":    { "family": "Inter Tight",      "licence": "OFL-1.1" }
  },
  "sections": ["header", "hero", "services", "team", "fees", "testimonials", "faq", "booking", "footer"],
  "features": ["responsive-nav", "contact-form", "faq-accordion", "testimonials", "reduced-motion"],
  "a11y": { "contrastChecked": true, "keyboardNav": true, "reducedMotion": true },
  "licence": "MIT",

  "featured": true,                       // optional — see §3.1
  "featuredRank": 1
}
```

`vertical` is the **browsing category**, and it is a controlled list of twelve.
Exactly one, because it is what the catalogue filter groups by — a template in
three categories makes all three less useful, and a category with one template in
it is not worth clicking:

| category | what belongs in it |
|---|---|
| `food-drink` | restaurants, bars, breweries, bakeries, food retail |
| `shops-ecommerce` | online and physical shops selling goods |
| `software-technology` | SaaS, developer tools, apps, fintech, security |
| `arts-media` | galleries, museums, publishing, podcasts, press |
| `creative-portfolio` | agencies, studios, photographers, film, interiors |
| `health-wellbeing` | clinics, therapists, vets, gyms, salons |
| `community-public` | charities, faith, civic, government, clubs |
| `property-construction` | estate agents, builders, architects, workspace |
| `travel-hospitality` | hotels, tour operators, venues, guided trips |
| `education` | schools, colleges, courses, tuition |
| `local-services` | trades and services a neighbourhood uses |
| `professional-services` | law, accountancy, consultancy |

Add a thirteenth only when it would hold **at least three** templates. Anything
finer — `wine`, `saas`, `conservation` — goes in `tags`, which the catalogue
search reads but does not group by.

`sections` must be unique across the library — the audit fails on two templates
with an identical list — which is easy, because a pricing page and a menu page
genuinely have different sections. Do not pad the list to dodge the check.

`mode` names what success looks like on the page:

| mode | the visitor… | design consequence |
|---|---|---|
| `persuade` | decides and acts | one primary CTA, repeated; social proof; clear pricing |
| `operate` | completes a task | scanability and consistency outrank expression |
| `read` | understands something | measure, hierarchy and rhythm carry it |
| `experience` | is browsing the work | imagery leads, the interface recedes |

### 3.1 `featured` — the recommended shelf

A handful of templates carry `"featured": true` and an integer `"featuredRank"`.
The catalogue renders those, in rank order, in a **Recommended** shelf above the
main grid, at a larger card size; they also stay in the catalogue below, marked.
`llms.txt` lists them separately so an agent picking blind starts from the same
set. `build-site.mjs` sorts unranked featured templates last, alphabetically.

The shelf is the front of the shop, not a second library. A template belongs in
it only if it clears a higher bar than the spec's floor: display type at real
scale, one section that took genuine effort to build, motion that is designed
rather than added, and a mobile view that is designed rather than stacked. If it
would not survive being placed next to a paid marketplace template, it is a
normal catalogue entry.

Keep the shelf small. Ten to twelve is a shelf; thirty is the catalogue again.

---

## 4. The shape of a normal page

Use the conventional architecture. Visitors have learned it; fighting it costs
them and gains nothing.

**Header.** Sticky or static, logo left, 3–6 nav links, one CTA button right. On
mobile it collapses to a hamburger that opens a real, focus-trapped panel.

**Hero.** A headline that says what the business does in plain words, one or two
supporting sentences, a primary button and usually a secondary text link, and an
image — a photograph or a product screenshot. Split layout or centred with the
image below; both are fine. The headline is the `<h1>` and there is exactly one.

**The middle.** Three to six sections, each with a heading and one job. Draw from
the ordinary vocabulary, choosing what the category actually needs:

- feature or service grid (2–4 columns, icon + heading + line of text)
- alternating image/text rows for the two or three things worth explaining
- social proof: logo strip, testimonial cards, a pull quote with a photo, ratings
- numbers that matter (years in business, clients, response time)
- how it works, as three or four numbered steps
- pricing table, menu, or a rate card
- team, portfolio grid, case studies, or a gallery
- FAQ, as an accordion
- a final CTA band before the footer

**Footer.** Multi-column: brand blurb, two or three link columns, contact details,
social links, and a bottom bar with copyright and legal links.

Not every template needs every part, and the order is not fixed — but the parts
should be ones a visitor already recognises.

---

## 5. Bans

- **The artefact-as-page.** The page must not be a chart, a diagram, a technical
  drawing, a score, a chart sheet, a ledger, a schematic, a film strip, an
  imposition sheet or a specimen sheet. If removing the graphic would leave you
  with no page, you have built the wrong thing.
- **Invented notation.** No pitch names, KP chainages, varroa counts, percentile
  histograms, hex offsets, weave drafts or grid references as the interface. A
  visitor should never have to learn a system to use the page.
- **Dense fabricated data tables as page structure.** A pricing table, a menu, an
  opening-hours table and a spec table are normal. A sixteen-row inspection log
  is not.
- **Decorative charts.** Show a chart only where a real site in this category
  would — a product screenshot for an analytics tool, a simple bar of results in
  a case study. It is never the hero, and never more than one per page. When one
  appears, §7 applies.
- **Lorem ipsum**, `[Your headline here]`, "Company Name", and every other
  placeholder. Write real sentences for an invented business.
- **Gradient text.** Emphasis comes from weight or size.
- **Emoji or Unicode glyphs standing in for an icon system.** Icons are drawn,
  from one set, at one stroke weight.
- **Hotlinked images or fonts.** Everything is vendored.
- **Nested cards**, and any card that is a card only because everything else is.

What is *no longer* banned, and is now expected where the category calls for it:
kickers and eyebrow labels, feature-card grids, hero stat rows, `01 / 02 / 03`
step numbers, pricing tiers with a highlighted "most popular", testimonial cards,
logo strips, rounded corners, and soft shadows.

---

## 6. The craft floor

Non-negotiable. The verification scripts check the mechanical half; the rest is
on you.

- **Contrast.** Body text ≥ 4.5:1, large text (≥24px, or ≥18.66px bold) ≥ 3:1.
  Check placeholder text, disabled buttons, footer small print and any text over
  a photograph — that is where it fails.
- **Depth.** Shadows carry both an offset and a soft blur, and there are at most
  two elevations on a page. A zero-offset coloured halo is decoration, not depth.
- **Spacing.** One scale, used consistently. Tight within a group, generous
  between groups, more space above a heading than below it. Section padding on
  desktop is generous — 5–8rem is normal — and roughly half that on mobile.
- **Type.** Body measure 60–75ch. One display size for the `<h1>`, one for
  section headings, one for card headings; do not invent a fourth. Tracking floor
  −0.03em on display sizes. Run the real copy at every breakpoint and fix what
  overflows.
- **Responsive.** Design at 390px as seriously as at 1440px. Grids collapse to one
  column, the nav collapses to a panel, tables scroll inside their own container,
  and nothing overflows horizontally at any width.
- **Motion.** Quiet and conventional: a short fade-and-rise on scroll, hover
  transitions on interactive things, a smooth accordion. 150–300ms, ease-out, and
  never on every element at once. `prefers-reduced-motion: reduce` must land on a
  complete, static page.
- **States.** hover, focus-visible, active, disabled, loading, error, empty — all
  real, on every control.
- **Browser surfaces.** Theme what you did not draw: `::selection`, `caret-color`,
  `accent-color`, scrollbars, focus rings, `text-underline-offset`. Numbers in
  tables and prices get `font-variant-numeric: tabular-nums`.
- **Copy.** Specific and in the business's own voice. Buttons name their action
  ("Book a check-up", not "Learn more" four times). Errors name both the problem
  and the recovery.

  Specific does not mean true. Every business in this library is invented, and it
  has to stay invented: do not name a real company, a real person, a real address
  or a real phone number, and do not attribute a quotation to anyone who exists.
  Write a business that could exist and does not. Each `index.html` carries the
  disclosure that says so — `scripts/add-disclosure.mjs` stamps it and
  `lint-templates.mjs` fails a template without it.

---

## 7. Charts and product screenshots

Some categories genuinely need one: an analytics platform, a fintech dashboard, a
developer tool. Build it as a **product screenshot** — an HTML/CSS mock of the
app's own UI, sitting in a browser or device frame — not as an abstract diagram.
It belongs in the hero or in one feature section, at most one per page.

If it contains a chart:

- Real numbers with a real shape, rendered into the HTML so it survives without JS.
- One y-axis. Never two scales on one chart.
- Categorical colour assigned in fixed order, never cycled. Ordered magnitudes use
  one hue, stepped light→dark.
- Recessive grid and axes; 2px data lines; ≥8px markers.
- Label it, so a reader knows what they are looking at without hovering.

---

## 8. Icons and imagery

### Icons — a real set, never emoji

```bash
node scripts/get-icons.mjs <slug> lucide calendar phone map-pin shield check
```

Sets available: `lucide` (ISC), `tabler` (MIT), `feather` (MIT). The script writes
`icons.svg.html` — an inline `<symbol>` sprite — and `ICONS.txt` with the licence.
`node scripts/inline-sprite.mjs <slug>` pastes it just inside `<body>`, replacing
any sprite already there rather than stacking a second copy. Nothing is loaded at
runtime.

Icon names differ between sets and move between releases: lucide has `funnel`
where you expect `filter`, tabler has `movie` where you expect `film`, feather has
no `ruler` at all. `get-icons.mjs` reports what it could not find, and
`lint-templates.mjs` fails a template whose `<use>` points at a symbol the sprite
does not contain — that failure is otherwise invisible, because a missing symbol
renders as nothing at all.

```html
<svg class="icon" aria-hidden="true"><use href="#i-calendar"></use></svg>
<svg class="icon" role="img" aria-label="Opening hours"><use href="#i-clock"></use></svg>
```

One set per template, one stroke width, sized in CSS. Icons label and clarify —
they never carry meaning the text does not.

### Photography — CC0 only, downloaded, never hotlinked

Most templates should carry two or three photographs: a hero image and one or two
in the body. This is the single biggest thing that makes a page read as a real
website rather than a layout exercise.

```bash
node scripts/get-images.mjs <slug> "dental surgery interior" 3
node scripts/get-images.mjs <slug> "espresso machine cafe" 3 --probe   # search only
```

| source | good for |
|---|---|
| `commons` *(default)* | Wikimedia Commons, filtered to CC0 / public domain. The only source with photographs of ordinary working subjects. |
| `met` | The Met Open Access. Objects: textiles, ceramics, instruments, prints. |
| `artic` | Art Institute of Chicago. **Search only** — its image host refuses non-browser clients; use `--probe` to find out what exists. |
| `openverse` | Openverse, cc0 + public-domain-mark. Rate-limited hard when unauthenticated. |

Every candidate has to name the subject in its own catalogue record before it is
downloaded. **A photograph that is not of the subject is worse than no
photograph** — and a photograph that is obviously a museum scan on a page selling
software is worse still. Probe first; it downloads nothing:

```bash
node scripts/get-images.mjs <slug> "bakery bread shop" 3 --probe
```

Images are downscaled, written to `img/`, and recorded in `IMAGES.md`. Keep a
template under about **3 images and 700 KB total**. Every `<img>` needs a
descriptive `alt`, explicit `width`/`height`, and `loading="lazy"` below the fold.

Where Commons has nothing usable — most B2B software categories — do not force a
bad photograph in. Use the product screenshot from §7, icons, and a confident
typographic layout instead.

### The asset plan

`scripts/assets-plan.json` records, for every template, the search that produced
its photographs and the icon set and names it uses, so imagery can be regenerated
later without guessing at the query:

```bash
node scripts/fetch-assets.mjs                # everything still missing
node scripts/fetch-assets.mjs cafe-bakery    # just this one
node scripts/fetch-assets.mjs --force loom   # re-fetch even if present
node scripts/inline-sprite.mjs --all         # paste every sprite into its index.html
```

Each entry also carries a `hero` field naming the opening composition that
template uses. **No more than six templates may share one**, and those six have to
differ in scheme, palette and type — otherwise the library reads as one template
in fifty-six colourways, which is the failure this field exists to prevent.

### Texture

Keep it quiet. A subtle background tint, a hairline border, a soft radial wash
behind the hero. Grain, hatching, moiré and halftone belong to the old approach
and are now out of scope.

---

## 9. Accessibility

- One `<h1>`; heading levels never skip.
- Landmarks: `header`, `nav`, `main`, `footer`, with `aria-label` when repeated.
- A skip link to `#main`, visible on focus.
- Every control reachable and operable by keyboard, with a visible focus ring.
  The mobile nav traps focus while open and closes on `Escape`.
- Form fields have real `<label>`s; errors use `aria-invalid` and are announced.
- Accordions use `<button aria-expanded>`; carousels, if any, have real controls.
- Decorative SVG gets `aria-hidden="true"`; meaningful SVG gets `role="img"` and a
  `<title>`.
- `<html lang>` is set.

---

## 10. Verification — required before a template is done

```bash
node scripts/shoot.mjs <slug>
```

It must print **`clean`** for both desktop and mobile. It checks console errors,
horizontal overflow at 1440px and 390px, and text contrast, resolving
`color-mix()` / `oklab()` through a canvas.

Two more must pass before a template counts as done:

```bash
node scripts/lint-templates.mjs <slug>    # structure, placeholder copy, banned patterns
node scripts/audit-uniqueness.mjs         # no shared type pairing, no twin palette
```

Screenshots land in `.shots/`. **Look at them.** The script checks colour and
geometry, not composition. A template that passes the script and looks wrong is
not done — and for this library, "looks wrong" now includes looking like an
exhibit instead of a website somebody could ship on Monday.
