---
name: web-design
description: Build a finished, scrollable single-page website for any business from the placeholder template, a brand pack, and the operator's curated photos and copy. Produces a self-contained site/ directory — index.html (hero rotator, about, what-we-do, how-we-work, optional founder section, testimonial, services, key facts, FAQ, CTA, footer, back-to-top) plus an llms.txt — with structured data (JSON-LD) whose type is operator-supplied. Domain-neutral, so no business category is baked in. Use after brand-extract, or directly when the brand pack and copy already exist. Trigger phrases include "build the site", "lay out the landing page", "turn this brand plus photos into a one-page site".
allowed-tools:
  - Bash
  - Read
  - Write
  - Edit
  - Glob
  - Grep
---

# web-design

Renders the canonical single-page site from `templates/index.html` and
`templates/llms.txt` by substituting every `{{ PLACEHOLDER }}`. The template is
the spec: copy it, fill it, never rebuild it from scratch from this prose.

## Canonical template — the only starting point

`templates/index.html` ships with this skill. It encodes the entire site —
structure, CSS, the JS hero rotator + drawer + sticky sub-nav + back-to-top, the
responsive rules, and the JSON-LD — with every business-specific value as a
`{{ TOKEN }}`. Copy it to `<output>/site/index.html` and substitute. Do the same
with `templates/llms.txt`. The full token list and what fills each is in
[`references/placeholders.md`](references/placeholders.md); the section catalogue
and the image-naming convention are in [`references/sections.md`](references/sections.md).

A run that hand-builds the HTML from this prose, or starts from a previous
client's rendered `index.html`, is a defect — it drifts every cycle. Copy the
canonical and substitute.

## Substitution contract

- After substitution, `grep "{{" site/index.html site/llms.txt` returns **zero**
  matches. Any surviving `{{ }}` is a bug.
- Palette tokens come from the brand pack's ten values. `ACCENT_TEXT` is the
  deepened, legible form of `ACCENT` — not the same value.
- `SCHEMA_TYPE` is operator-supplied. Default to `LocalBusiness`; use
  `Organization` for a business with no premises, or a more specific schema.org
  subtype when one fits. Never fall back to a trade or industry type.
- Copy tokens are written in the plain register: no em-dashes in body text, no
  inflation vocabulary, no marketing cadence. A real client testimonial is used
  **verbatim**; never reword it.
- Never invent a fact to fill a token. If intake did not collect it, go back and
  ask. An empty contact detail or a guessed year is a defect.

## Images — curate by content, name by convention

The template references images by the fixed convention in
[`references/sections.md`](references/sections.md) so the operator can tell what
each slot is from the filename. Curate by **content, verified by eye**, never by
filename: lead the hero with the strongest "arrive here" shot, put the best
result in the testimonial, distribute the rest. A person's photo is founder-only;
it never goes in the rotator as a landscape crop. Every `alt` token is filled
from the real photo it sits on.

### The 2000px rule

Never `Read` an image whose longest edge exceeds 2000px — measure first, preview a
downscaled `/tmp` copy if larger, delete it. Optimise to webp: body tier ~1300px
q82; hero tier source-resolution q88; the portrait kept tall.

## Verify every page state

Serve `site/` over a local HTTP server and check, at a 1280px desktop and a 390px
mobile viewport: no horizontal overflow, no broken images, the hero rotates, the
drawer opens with every link, the sticky sub-nav reveals on scroll, the
back-to-top appears and works, and both JSON-LD blocks are valid JSON once
substituted. Bottom-align paired images and equal-height cards where the layout
calls for it.

## Completion criteria

- `site/index.html` and `site/llms.txt` exist with zero surviving `{{ }}`.
- Every referenced image resolves; alts describe the real photos.
- Desktop + mobile verified; hero, drawer, sub-nav, back-to-top all work.
- Both JSON-LD blocks (the business, typed by the operator's `SCHEMA_TYPE`, and
  `FAQPage`) parse and match the visible copy.

## Deploy hand-off

The build ends at a self-contained `site/` tree. To go live, the tree becomes the
canonical Cloudflare Pages source at `<root>/pages/<project>/`, where `<root>`
comes from the resolver — never hardcode it:

```bash
ROOT=$(sh "$(dirname "$0")/../../bin/resolve-cf.sh" site-root)   # from the skill dir
# assemble <ROOT>/pages/<project>/ with the built site as its output dir
```

Then follow [`references/deploy.md`](references/deploy.md) to publish, attach the
custom domain, wire any form to D1, and gate "done" on a live `200`. For a page or
document that must sit behind a login, follow [`references/gating.md`](references/gating.md).
For a document that must be signed, follow [`references/e-sign.md`](references/e-sign.md).

This skill runs in the admin/main session. It dispatches no specialist worker and
writes to no graph or memory store.
