@hostinger/mcp 3.2.2 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/headless/SKILL.md +247 -36
- package/skills/headless/connect/SKILL.md +103 -0
- package/skills/headless/{entry → connect}/bootstrap.mjs +8 -3
- package/skills/headless/connect/tools/antigravity.md +54 -0
- package/skills/headless/connect/tools/claude-code.md +46 -0
- package/skills/headless/connect/tools/codex.md +48 -0
- package/skills/headless/connect/tools/cursor.md +109 -0
- package/skills/headless/connect/tools/devin.md +50 -0
- package/skills/headless/connect/tools/vscode.md +57 -0
- package/skills/headless/report.schema.json +149 -0
- package/skills/headless/run-log.mjs +662 -0
- package/skills/headless/steps/01-check-state/SKILL.md +193 -0
- package/skills/headless/steps/02-refine/SKILL.md +206 -0
- package/skills/headless/steps/03-provision/SKILL.md +62 -0
- package/skills/headless/steps/03-provision/ecommerce/SKILL.md +133 -0
- package/skills/headless/steps/03-provision/webapp/SKILL.md +154 -0
- package/skills/headless/steps/03-provision/wordpress/SKILL.md +100 -0
- package/skills/headless/steps/04-content-model/SKILL.md +150 -0
- package/skills/headless/steps/04-content-model/ecommerce/SKILL.md +75 -0
- package/skills/headless/steps/04-content-model/wordpress/SKILL.md +178 -0
- package/skills/headless/steps/05-design/SKILL.md +455 -0
- package/skills/headless/steps/05-design/logo-kit.mjs +201 -0
- package/skills/headless/steps/06-frontend/SKILL.md +131 -0
- package/skills/headless/steps/07-validate/SKILL.md +53 -0
- package/skills/headless/steps/08-deploy/SKILL.md +171 -0
- package/skills/headless/steps/09-done/SKILL.md +151 -0
- package/src/core/skills.js +164 -24
- package/src/core/skills.ts +164 -24
- package/src/servers/agency-hosting.js +1 -1
- package/src/servers/agency-hosting.ts +1 -1
- package/src/servers/agent-readiness.js +1 -1
- package/src/servers/agent-readiness.ts +1 -1
- package/src/servers/all.js +1 -1
- package/src/servers/all.ts +1 -1
- package/src/servers/billing.js +1 -1
- package/src/servers/billing.ts +1 -1
- package/src/servers/dns.js +1 -1
- package/src/servers/dns.ts +1 -1
- package/src/servers/domains.js +1 -1
- package/src/servers/domains.ts +1 -1
- package/src/servers/ecommerce.js +1 -1
- package/src/servers/ecommerce.ts +1 -1
- package/src/servers/horizons.js +1 -1
- package/src/servers/horizons.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/mail.js +1 -1
- package/src/servers/mail.ts +1 -1
- package/src/servers/reach.js +1 -1
- package/src/servers/reach.ts +1 -1
- package/src/servers/vps.js +1 -1
- package/src/servers/vps.ts +1 -1
- package/src/servers/wordpress.js +1 -1
- package/src/servers/wordpress.ts +1 -1
- package/skills/headless/entry/skill.md +0 -79
- package/skills/headless/references/DATABASE.md +0 -49
- package/skills/headless/references/DEPLOYMENT.md +0 -34
- package/skills/headless/references/SETUP.md +0 -35
- package/skills/headless/references/STORE.md +0 -52
- package/skills/headless/references/WORDPRESS.md +0 -51
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: headless-step-content-model-wordpress
|
|
3
|
+
description: "Push phase of Step 4 for a headless WordPress backend: open an authenticated session to the per-site WP MCP with the installation JWT, then create the content helper's Title/Description, categories, media, pages, posts and navigation through the MCP ability tools. wp/v2 REST is the public read surface, not a write path for this token. Idempotent, instance-gated."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# headless-step-content-model-wordpress
|
|
7
|
+
|
|
8
|
+
**Push phase — needs the live instance.** Run only after `provision.status == "completed"` (re-verify
|
|
9
|
+
against `hosting_websites_list-setups`). It maps the backend-agnostic `content-model.md` onto real
|
|
10
|
+
WordPress objects.
|
|
11
|
+
|
|
12
|
+
**In:** `content-model.md`, `site.json` (both prepared earlier), generated media under
|
|
13
|
+
`~/.hostinger/<domain>/media/`. **Out:** content created in WordPress; created IDs recorded back into
|
|
14
|
+
`content-model.md`.
|
|
15
|
+
|
|
16
|
+
## What creates what — and what authenticates it
|
|
17
|
+
|
|
18
|
+
The Hostinger **management** MCP (`wordpress_*`) is infra-only: it installs WordPress, plugins and
|
|
19
|
+
themes but has **no content endpoints**.
|
|
20
|
+
|
|
21
|
+
Writes go through the **per-site WP MCP** (the Hostinger AI Assistant plugin), authenticated with the
|
|
22
|
+
installation JWT. **The JWT authenticates the MCP endpoint only** — it is wired as the MCP server's
|
|
23
|
+
`authenticate_request`, and the plugin registers no global `determine_current_user` /
|
|
24
|
+
`rest_authentication_errors` filter. So a raw `POST /wp/v2/posts` with this token is **not**
|
|
25
|
+
authenticated; the admin user context (`wp_set_current_user`) exists only *inside* an MCP session.
|
|
26
|
+
|
|
27
|
+
| Surface | Base (from `site.json`) | Role |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| **Per-site WP MCP** | `urls.wp_mcp` (`mcp_url`) | **all writes** — create content through its ability tools |
|
|
30
|
+
| **WP REST API** `wp/v2` | `urls.rest_api` | **public reads only** (no auth needed) — what the Step 6 frontend consumes; do not write to it with the JWT |
|
|
31
|
+
|
|
32
|
+
> **The WordPress content model is posts, pages and categories** (plus media). For **SEO**, install
|
|
33
|
+
> **Yoast** through the connector so meta + schema come from a maintained plugin; the frontend reads each
|
|
34
|
+
> item's `yoast_head_json` (Step 4.7 below).
|
|
35
|
+
|
|
36
|
+
## Step 1 — Open the authenticated MCP session
|
|
37
|
+
|
|
38
|
+
Mint a short-lived token through the Hostinger connector:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{ "operation": "wordpress_installations_jwt-token",
|
|
42
|
+
"params": { "username": "<site.json username>", "software": "<site.json wordpress.install_id>" } }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Returns `token` (+ `mcp_url`, already stored as `urls.wp_mcp`). Connect to `urls.wp_mcp` as an MCP
|
|
46
|
+
client (`initialize` → `tools/list` → `tools/call`) and send the token on **every** request as:
|
|
47
|
+
|
|
48
|
+
- `X-Hostinger-JWT-Token: <token>` — the plugin's preferred header (checked first), **or**
|
|
49
|
+
- `Authorization: Bearer <token>` — accepted as a fallback (case-insensitive `bearer ` prefix).
|
|
50
|
+
|
|
51
|
+
The token lives ~24 h — mint on demand, **never print, log, or write it to `site.json` /
|
|
52
|
+
`content-model.md`**. If a call returns `401/403` with `token_invalid`/`invalid_api_key`, the token
|
|
53
|
+
expired or was revoked → re-mint and reconnect.
|
|
54
|
+
|
|
55
|
+
> **Transport gotchas (streamable HTTP MCP).** The endpoint is session-based: `initialize` returns an
|
|
56
|
+
> **`Mcp-Session-Id` response header** — capture it and send it on **every** subsequent request
|
|
57
|
+
> (`tools/list`, `tools/call`), or they fail with `-32600 Missing Mcp-Session-Id header`. Send
|
|
58
|
+
> `Accept: application/json, text/event-stream`. After `initialize`, post the `notifications/initialized`
|
|
59
|
+
> notification, and **tolerate a non-2xx on it** (it can answer 404 with no body — harmless; don't let
|
|
60
|
+
> it abort the session). A raw curl/HTTP client works fine for all this.
|
|
61
|
+
|
|
62
|
+
## Step 2 — Discover the ability tools
|
|
63
|
+
|
|
64
|
+
The server advertises either every ability as its own tool, or three **discovery meta-tools**
|
|
65
|
+
(discover → get-info → execute) to keep `tools/list` small. Call `tools/list` first and work with
|
|
66
|
+
what's there; with discovery, list abilities, then execute by ability name. The content tools that
|
|
67
|
+
cover this step (exact names come from discovery — don't hardcode them):
|
|
68
|
+
|
|
69
|
+
`SettingsTools` · `CategoriesTools` · `TagsTools` · `MediaUploadTools` / `MediaTools` · `PagesTools` ·
|
|
70
|
+
`PostsTools` · `MenusTools` / `NavigationTools` · `SeoTools`. A generic
|
|
71
|
+
`RestEndpointTool` can reach any `wp/v2` route *through the authenticated session* if no dedicated tool
|
|
72
|
+
fits.
|
|
73
|
+
|
|
74
|
+
Seen on a live install (Hostinger AI Assistant, discovery mode): the meta-tools are
|
|
75
|
+
`hostinger-ai-discover-abilities`, `hostinger-ai-get-ability-info { name }` and
|
|
76
|
+
`hostinger-ai-execute-ability { name, parameters }`. The abilities are named
|
|
77
|
+
`hostinger-ai/<thing>-<action>`, e.g. `posts-create`, `posts-update`, `media-upload-from-url`,
|
|
78
|
+
`media-delete`. Call `get-ability-info` for each schema before using it. Results come back as
|
|
79
|
+
`{ ability, result: { data, … } }` for lists and `{ ability, result: {…} }` for single objects.
|
|
80
|
+
|
|
81
|
+
## Step 3 — Idempotency rule (makes resume safe)
|
|
82
|
+
|
|
83
|
+
Every create is **list/get-by-slug first, then create only if absent** — a re-run must not duplicate.
|
|
84
|
+
Use the matching read ability (or `RestEndpointTool` → `GET /wp/v2/posts?slug=…&status=any`): a hit →
|
|
85
|
+
reuse its `id`; empty → create. Capture each returned `id` and record the id map back into
|
|
86
|
+
`content-model.md` so later references resolve without re-querying.
|
|
87
|
+
|
|
88
|
+
## Step 4 — Create in dependency order
|
|
89
|
+
|
|
90
|
+
0. **Remove WordPress's default content first.** A fresh install ships a **"Hello world!" post**
|
|
91
|
+
(id 1), a **"Sample Page"**, and a default comment — they will otherwise show up on the live site.
|
|
92
|
+
Delete the default post (`posts-delete { id: 1, force: true }`) and, if present, the sample page
|
|
93
|
+
(find it via `pages-search` for slug `sample-page`, then `pages-delete`). This is idempotent: if
|
|
94
|
+
they're already gone, skip. Leave the default `Uncategorized` category — it's harmless once no post
|
|
95
|
+
uses it (the frontend only lists categories with posts).
|
|
96
|
+
1. **Site Title & Description** (the helper's global part) → the Settings ability.
|
|
97
|
+
2. **Categories / tags** — one per term in the helper → Categories / Tags abilities. Keep `name → id`.
|
|
98
|
+
3. **Media.** Upload every image in the helper, set `alt_text`, and keep `filename → id`. See
|
|
99
|
+
*Media upload* below for both sources: a generated file in `media/`, or an Unsplash URL.
|
|
100
|
+
4. **Pages** — each static/index page → Pages ability (`title`, `slug`, block/HTML `content`,
|
|
101
|
+
`status: publish`). Keep `slug → id`.
|
|
102
|
+
5. **Posts (articles)** — each → Posts ability (`title`, `slug`, `content`, `status: publish`,
|
|
103
|
+
`categories: [id]`, `tags: [id…]`, `featured_media: <id>`, `date`). (The plugin block-converts post
|
|
104
|
+
content on insert via `rest_pre_insert_post`, so HTML bodies are fine.)
|
|
105
|
+
6. **Navigation** — Menus / Navigation abilities: create the menu, then an item per nav entry
|
|
106
|
+
referencing a page `id` (or a custom URL for listing routes). **Headless note:** the Step 6
|
|
107
|
+
frontend renders nav from `content-model.md`, so the WP menu is for owners editing in wp-admin, not
|
|
108
|
+
what the live site reads — create it, but it isn't the source of truth.
|
|
109
|
+
7. **SEO** — prefer **Yoast SEO**, installed through the connector:
|
|
110
|
+
`wordpress_plugins_install { username, software, plugins: ["wordpress-seo"] }` (async), poll
|
|
111
|
+
`wordpress_plugins_list-installed { username, software }` until `wordpress-seo` appears, then
|
|
112
|
+
`wordpress_plugins_activate { username, software, plugin: "wordpress-seo" }`. It generates meta +
|
|
113
|
+
schema, and the frontend reads `yoast_head_json` per post/page. Set each post's Yoast
|
|
114
|
+
title/description if an ability exposes it; otherwise the excerpt + title are a fine fallback. Don't
|
|
115
|
+
fail the push over SEO fields.
|
|
116
|
+
|
|
117
|
+
### Media upload
|
|
118
|
+
|
|
119
|
+
**From a URL (Unsplash or any direct file URL).** Use `hostinger-ai/media-upload-from-url`.
|
|
120
|
+
WordPress downloads the file and stores its own copy, so the site never depends on the external URL
|
|
121
|
+
staying up:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{ "name": "hostinger-ai/media-upload-from-url",
|
|
125
|
+
"parameters": {
|
|
126
|
+
"url": "https://images.unsplash.com/photo-<…>?w=1600&q=80&fm=jpg&fit=max",
|
|
127
|
+
"filename": "<site>-<subject>.jpg",
|
|
128
|
+
"title": "<short title>",
|
|
129
|
+
"alt_text": "<what the photo shows>",
|
|
130
|
+
"caption": "Photo by <Photographer> on Unsplash",
|
|
131
|
+
"description": "Source: https://unsplash.com/photos/<id> (Unsplash License)",
|
|
132
|
+
"post_id": <post id, if the post already exists> } }
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
- `url` must return 200 with an image content type. A photo *page* URL fails.
|
|
136
|
+
- The result carries the attachment `id` and `source_url`. WordPress also generates the smaller
|
|
137
|
+
sizes (`300x200`, `768x…`, `1024x…`, …) under `media_details.sizes`, which the frontend can use
|
|
138
|
+
for `srcset`.
|
|
139
|
+
- Set `filename` yourself, so files in `wp-content/uploads/` have readable names.
|
|
140
|
+
|
|
141
|
+
**From a local file in `media/`.** Use the Media-upload ability that discovery lists for local
|
|
142
|
+
files.
|
|
143
|
+
|
|
144
|
+
**Set the featured image.** Pass `featured_media: <id>` when creating the post (Step 4.5). If the
|
|
145
|
+
post already exists (on resume, or when images are added later), use
|
|
146
|
+
`hostinger-ai/posts-update { id, featured_media }`.
|
|
147
|
+
|
|
148
|
+
**Idempotency trap: don't look up earlier uploads by filename.** `media-search` matches titles and
|
|
149
|
+
content, not filenames or the slug you passed. A search for `<site>-lisbon-tram` finds nothing even
|
|
150
|
+
though the file exists, so a re-run would upload the image again. WordPress then stores the duplicate
|
|
151
|
+
as `…-1.jpg` and silently repoints the post at it. Check this way instead:
|
|
152
|
+
|
|
153
|
+
1. `posts-get { id }`. If `featured_media` is already set, skip that post. Its image exists.
|
|
154
|
+
2. Otherwise, search by the **title** you set on upload (`media-search { search: "<title>" }`),
|
|
155
|
+
and reuse a hit.
|
|
156
|
+
3. Only upload when both checks come up empty.
|
|
157
|
+
|
|
158
|
+
If duplicates do slip in, point each post back at the original with `posts-update`, then delete the
|
|
159
|
+
duplicates with `media-delete { id, force: true }`.
|
|
160
|
+
|
|
161
|
+
## Step 5 — Verify and record
|
|
162
|
+
|
|
163
|
+
- Read a couple of created objects back through `wp/v2` (public GET, no auth): e.g.
|
|
164
|
+
`GET {urls.rest_api}/posts?per_page=1` and check the `X-WP-Total` header against the planned article
|
|
165
|
+
count; fetch one post with `_embed` to confirm `featured_media` and `categories` resolved.
|
|
166
|
+
- Media: `GET {urls.rest_api}/media?per_page=1` → `X-WP-Total` equals the number of images planned
|
|
167
|
+
(more means duplicates). `GET {urls.rest_api}/media?include=<ids>` confirms `alt_text` and
|
|
168
|
+
`caption` on each image. Each `source_url` must return 200 with an image type. Each post's
|
|
169
|
+
`featured_media` must point at its image.
|
|
170
|
+
- Write the created-id map into `content-model.md` (a `## Created in WordPress` section: object → slug
|
|
171
|
+
→ id) so the frontend data phase and any re-run resolve references directly. Include one row per
|
|
172
|
+
media item: filename, credit and source page, the post it's featured on, and its id.
|
|
173
|
+
|
|
174
|
+
## Step 6 — Hand off
|
|
175
|
+
|
|
176
|
+
Update `state.json` (`provision` stays `completed`; advance `stage` toward the frontend **data**
|
|
177
|
+
phase) with a fresh `updated_at`. The backend now holds the content; Step 6's data wiring points the
|
|
178
|
+
frontend at `urls.rest_api`.
|
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: headless-step-design
|
|
3
|
+
description: "Step 5 (design) of the Hostinger Headless flow. Derives three distinct design directions from brief.md (plus any reference sites and brand assets), lets the user pick one in a single question, checks the chosen one against the 8-point grid and WCAG 2.2 AA, designs the logo as outlined SVG (wordmark, icon mark, favicon), then writes design.md — tokens, type, logo, components, per-page composition, accessibility and imagery — as the contract the frontend implements (the framework/stack is chosen later by the frontend step). Prepare work: runs during the provision wait and never touches the backend."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# headless-step-design
|
|
7
|
+
|
|
8
|
+
Turn the brief into a look the user has chosen, and write it down so the frontend can't drift from
|
|
9
|
+
it. **One question, three options, one spec.** The output is **files** (`design.md` plus the logo),
|
|
10
|
+
not a conversation: the frontend step implements `design.md`, not the chat.
|
|
11
|
+
|
|
12
|
+
**In:** `brief.md` (required), `content-model.md` (if it exists yet), reference sites and assets the
|
|
13
|
+
user gave. **Out:** `~/.hostinger/<domain>/design.md` and the logo files in
|
|
14
|
+
`~/.hostinger/<domain>/brand/`.
|
|
15
|
+
|
|
16
|
+
This is **prepare work**: it runs while provision settles (see *Background provision & the instance
|
|
17
|
+
barrier* in the top-level `SKILL.md`). Nothing here calls the Hostinger connector or the backend.
|
|
18
|
+
|
|
19
|
+
**Keep it short.** Refine already asked about tone and look; don't ask again. The step asks the user
|
|
20
|
+
**one** question: either the keep/change question (Step 1) or the three directions (Step 4), never
|
|
21
|
+
both. "You decide" is a valid answer.
|
|
22
|
+
|
|
23
|
+
**Talk like a person, not a designer.** Everything the user reads describes how the site will
|
|
24
|
+
**look and feel**: colours by name ("hot pink, butter yellow"), lettering by feel ("big chunky
|
|
25
|
+
rounded headlines"), and the details they'll notice ("photos tilted like stickers on a scrapbook").
|
|
26
|
+
No hex codes, font names, or design jargon (palette, accent, tokens, shape language, contrast).
|
|
27
|
+
|
|
28
|
+
**Two hard rules apply to every direction, however loud or quiet it is:**
|
|
29
|
+
|
|
30
|
+
- **The 8-point grid.** Every spacing, sizing and layout value is a multiple of 8 px (details in
|
|
31
|
+
5.2).
|
|
32
|
+
- **WCAG 2.2 Level AA.** Colour, focus, motion, target size and reflow are decided here, so the
|
|
33
|
+
frontend can't build them wrong (details in 5.1 and 5.3).
|
|
34
|
+
|
|
35
|
+
These rules are for you, not the user. **Never mention them**: no grid, contrast, WCAG or
|
|
36
|
+
accessibility talk in anything the user reads, and no "all options follow…" reassurances. The user
|
|
37
|
+
judges the look in the preview; the rules just make sure it works.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Step 1 — Read the workspace, then the resume check
|
|
42
|
+
|
|
43
|
+
Read `brief.md`, `state.json`, and `content-model.md` if it exists.
|
|
44
|
+
|
|
45
|
+
- **`design.md` already exists and this is not a redesign** → don't overwrite it. Describe the
|
|
46
|
+
current look in two or three plain sentences and ask: *keep it, or change something?* That is this
|
|
47
|
+
step's one question.
|
|
48
|
+
- **Keep** → leave the file alone and go straight to the `state.json` update at the end of Step 7.
|
|
49
|
+
- **Change** → edit only what they asked for, re-run the 5.x checks on what changed, then save.
|
|
50
|
+
- **The site is already deployed** (`deploy.json` exists, or `state.json` has `deployed_at`) and the
|
|
51
|
+
user asked for a different look → this is a **redesign**. Tell them plainly: their content stays
|
|
52
|
+
as it is, only the look changes, and they'll see a preview before it goes live. Then run Steps 2–7
|
|
53
|
+
as normal. The current `design.md` becomes one more signal in Step 2, so carry over what the user
|
|
54
|
+
wants to keep and move away from the rest.
|
|
55
|
+
|
|
56
|
+
## Step 2 — Collect the signals
|
|
57
|
+
|
|
58
|
+
Before inventing anything, gather what already constrains the design:
|
|
59
|
+
|
|
60
|
+
| Signal | Where | How it constrains |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| Tone, purpose, audience | `brief.md` → *Tone*, *Purpose*, *Audience* | Sets the mood all three directions must fit |
|
|
63
|
+
| Brand assets | `brief.md` → *Assets* | Logo, brand colours and fonts are **fixed** in every direction. Never recolour a supplied logo; Step 6 uses it instead of designing one |
|
|
64
|
+
| Pages and entities | `content-model.md` → *Navigation*, *Pages*, *Articles*; if it doesn't exist yet, `brief.md` → *Pages* | Drives the component inventory and page composition |
|
|
65
|
+
| Reference sites | URLs in the brief or the conversation | Cues to borrow (see below) |
|
|
66
|
+
| Images already sourced | `content-model.md` → the image lines under *Articles* (skip if none yet) | Glance at a few at small size; keep accents from clashing with their dominant colours |
|
|
67
|
+
| Current design (redesign only) | existing `design.md` | What to keep, what to move away from |
|
|
68
|
+
|
|
69
|
+
**Reference sites.** Open each one in a real browser if you have one. Static fetches are often
|
|
70
|
+
blocked or rate-limited; without a browser, fetch the HTML and its linked CSS instead. If neither
|
|
71
|
+
works, say so in one line and continue from the brief alone. Pull **cues**:
|
|
72
|
+
|
|
73
|
+
- **Fonts:** the computed `font-family` of `h1`/`h2` and of body `p`.
|
|
74
|
+
- **Colours:** background colours weighted by painted area (element width × height), the top 5–8.
|
|
75
|
+
Skip transparent and semi-transparent backgrounds and overlays (cookie banners, modals, chat
|
|
76
|
+
widgets). Text colours come from headings, paragraphs and links.
|
|
77
|
+
- **Feel:** look at the first screen and one further down (screenshots shown to you, not saved in
|
|
78
|
+
the workspace) and note what creates the feel: type weight, colour blocking, shapes, density.
|
|
79
|
+
|
|
80
|
+
**Borrow cues, never assets.** Don't use its logo, copy, images or exact layout. **The brief wins**
|
|
81
|
+
over the live site when they disagree (the user's words are what they want). Note the mismatch in
|
|
82
|
+
the direction.
|
|
83
|
+
|
|
84
|
+
**Brand colours that fail contrast** are still fixed, but only in roles where they pass. A pale brand
|
|
85
|
+
yellow can be a background band or a large decorative shape, never body text. Say so in the
|
|
86
|
+
direction.
|
|
87
|
+
|
|
88
|
+
## Step 3 — Derive three directions
|
|
89
|
+
|
|
90
|
+
Write three directions that **all fit the brief** but are **genuinely different**. Vary at least two
|
|
91
|
+
of: palette (temperature, saturation, light vs. dark), type personality (geometric, grotesk, serif,
|
|
92
|
+
display), and shape language (sharp vs. round, flat vs. outlined, airy vs. dense). Three shades of
|
|
93
|
+
one idea is not a choice. Neither is a wildcard that ignores the brief.
|
|
94
|
+
|
|
95
|
+
Order them: **A is the recommendation**, the closest reading of the brief and references. B and C
|
|
96
|
+
push in different directions.
|
|
97
|
+
|
|
98
|
+
Each direction carries exactly this:
|
|
99
|
+
|
|
100
|
+
- **Name** — 2–3 evocative words ("Sticker-book maximal", "Quiet gallery").
|
|
101
|
+
- **Mood** — one line on how the site feels.
|
|
102
|
+
- **Palette** — 5–7 hex colours with roles (base, text, primary accent, secondary accents, surface).
|
|
103
|
+
These are the core colours the user sees; Step 5 may add derived shades (link, muted, error,
|
|
104
|
+
focus) as tokens.
|
|
105
|
+
- **Type** — a display font and a body font, both self-hostable through `@fontsource`.
|
|
106
|
+
- **Shape language** — corners, borders, shadows and density in one phrase ("chunky 3 px ink
|
|
107
|
+
outlines, hard 8 px offset shadows, tilted cards").
|
|
108
|
+
- **Signature element** — the one memorable move (a scrolling marquee, stickers, oversized numerals,
|
|
109
|
+
a duotone photo treatment). If it moves on its own, it gets a pause control (5.3).
|
|
110
|
+
- **Logo idea** — one line on how the name becomes a logo, using a move from Step 6 ("letters
|
|
111
|
+
bounce in different colours and the O is a smiley").
|
|
112
|
+
|
|
113
|
+
**Only offer what can pass.** Before asking, run two quick gates on **all three**:
|
|
114
|
+
|
|
115
|
+
1. **Contrast gate.** Every colour that will carry text (base, surfaces, and any accent used as a
|
|
116
|
+
text block) has a palette text colour reaching **4.5:1** on it. Two text colours are fine (dark on
|
|
117
|
+
light blocks, light on dark ones). Accents that only ever hold large text need 3:1.
|
|
118
|
+
2. **Font gate.** Each font resolves on npm, and the display font also has a static `@fontsource`
|
|
119
|
+
package, because the logo is built from it (5.4).
|
|
120
|
+
|
|
121
|
+
A direction that fails either gate gets adjusted, not offered as-is. Don't offer a look that only
|
|
122
|
+
works by breaking the rules, such as body text on a neon accent or a marquee that can't be stopped.
|
|
123
|
+
Loud is fine; inaccessible is not.
|
|
124
|
+
|
|
125
|
+
**Avoid the template look** unless the brief asks for neutral: Inter or Roboto everywhere, a
|
|
126
|
+
blue-to-purple gradient hero, centred hero with three feature cards, grey cards on white. Every
|
|
127
|
+
direction should look like *this* site, not a starter theme.
|
|
128
|
+
|
|
129
|
+
## Step 4 — Ask once
|
|
130
|
+
|
|
131
|
+
Ask **one** question, with A marked as the recommendation. With a structured question UI (multiple
|
|
132
|
+
choice plus an "other" field), each option's **label** is the direction's name and its
|
|
133
|
+
**description** is one or two plain sentences, under ~160 characters. Without one, write each
|
|
134
|
+
option as one line in the same shape.
|
|
135
|
+
|
|
136
|
+
The description says what the site would feel like, the main colours by name, what the lettering
|
|
137
|
+
is like, the logo idea, and the one detail they'll remember. Hex codes and font names stay in your
|
|
138
|
+
notes.
|
|
139
|
+
|
|
140
|
+
> **Sticker-book maximal** — Loud and playful, like a scrapbook: candy pink, butter yellow and lilac,
|
|
141
|
+
> chunky headlines, tilted sticker photos and a bouncy logo with a smiley O.
|
|
142
|
+
|
|
143
|
+
> **Midnight disco** — A dark, glowing night-out feel: deep purple with neon pink, yellow and
|
|
144
|
+
> turquoise, heavy rounded headlines and a logo like a neon sign.
|
|
145
|
+
|
|
146
|
+
Then:
|
|
147
|
+
|
|
148
|
+
- **Pick** → use it as written.
|
|
149
|
+
- **"You decide" / skipped** → use A, and tell them in one line which look you went with.
|
|
150
|
+
- **A mix** ("A's colours with C's fonts") → merge them. Don't ask again.
|
|
151
|
+
- **Something else** → build a direction from what they said, run Step 5, and use it. Don't send
|
|
152
|
+
three new options.
|
|
153
|
+
|
|
154
|
+
Don't ask about anything else in this step.
|
|
155
|
+
|
|
156
|
+
## Step 5 — Check the chosen direction
|
|
157
|
+
|
|
158
|
+
Fix any problem found here by adjusting the direction, not by asking the user. Don't report these
|
|
159
|
+
checks or their fixes; the user sees the result in the preview.
|
|
160
|
+
|
|
161
|
+
### 5.1 Contrast — WCAG 2.2 SC 1.4.3 and 1.4.11
|
|
162
|
+
|
|
163
|
+
**Which pairs count:** walk the component inventory you're about to write (Components table). For
|
|
164
|
+
each component, list every foreground/background combination in every state: default, hover,
|
|
165
|
+
focus, active, error. Include text on coloured bands (footer links on dark, marquee text, chips),
|
|
166
|
+
placeholder text and photo credits. Compute the WCAG contrast ratio for each:
|
|
167
|
+
|
|
168
|
+
| Kind | Examples | Minimum |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| Body text | paragraphs, links, footer text, input text, placeholder text, captions | **4.5:1** |
|
|
171
|
+
| Large text | ≥ 24 px regular, or ≥ 18.66 px (14 pt) bold | **3:1** |
|
|
172
|
+
| Non-text | input and button boundaries (when the outline is the only edge), focus indicator against each adjacent colour, icons that carry meaning, chip outlines | **3:1** |
|
|
173
|
+
|
|
174
|
+
A pair that fails gets a darker or lighter shade of the **same hue**; never drop a brand colour. A
|
|
175
|
+
new shade becomes a token (for example `--color-link` or `--color-muted`). Disabled controls and
|
|
176
|
+
pure decoration are exempt, but say which elements those are. **Text on photos** needs a scrim or
|
|
177
|
+
solid backing that gets the text to 4.5:1 against the lightest part of the image. If you can't
|
|
178
|
+
guarantee that, don't put text on photos. Record every pair and its ratio in `design.md`.
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
// node -e: relative luminance + contrast ratio, hex in → ratio out
|
|
182
|
+
const L = h => { const c = h.match(/\w\w/g).map(x => parseInt(x, 16) / 255)
|
|
183
|
+
.map(v => v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
|
|
184
|
+
return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2]; };
|
|
185
|
+
const ratio = (a, b) => { const [x, y] = [L(a), L(b)].sort((m, n) => n - m); return (x + 0.05) / (y + 0.05); };
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### 5.2 The 8-point grid
|
|
189
|
+
|
|
190
|
+
Values are checked in px at the default 16 px root (`0.5rem` = 8 px).
|
|
191
|
+
|
|
192
|
+
| What | Rule |
|
|
193
|
+
|---|---|
|
|
194
|
+
| Spacing (padding, margin, gap) | Multiples of 8: `8 16 24 32 48 64 96 128`. **4 px** is the only half-step, and only inside small components (chip padding, icon-to-label gap) |
|
|
195
|
+
| Component sizes | Heights and fixed widths are multiples of 8. Buttons, nav items and inputs are **48 px** tall minimum |
|
|
196
|
+
| Radii | `8 16 24 32`, or `999px` for pills. 4 px only on elements under 32 px tall |
|
|
197
|
+
| Line heights | Fixed-size text: multiples of 8 (16/24, 18/32, 24/32), or of 4 for text ≤ 14 px (12/16, 14/20). Fluid headings (`clamp()` sizes) use a unitless line-height (1.0–1.2) and are exempt, because their line box can't stay on the grid at every size |
|
|
198
|
+
| Fluid spacing | `clamp()` for spacing or sizes uses grid values at both ends. Fluid **font sizes** are exempt |
|
|
199
|
+
| Shadow offsets | 4 or 8 multiples |
|
|
200
|
+
| Layout | Content width, gutters and breakpoints are multiples of 8 (e.g. `1200`, `768`, `1024`, `1280`) |
|
|
201
|
+
| Exempt | Border and outline widths (1–4 px), focus offset, font sizes, the `999px` pill radius, the 4-multiple line heights above, and anything that isn't a length (angles like a `-2deg` tilt, durations, opacity) |
|
|
202
|
+
|
|
203
|
+
Every value in the **Tokens** block must pass this table.
|
|
204
|
+
|
|
205
|
+
### 5.3 WCAG 2.2 AA — what the design must decide
|
|
206
|
+
|
|
207
|
+
These criteria are settled at design time. Write each answer into the **Accessibility** section of
|
|
208
|
+
`design.md`.
|
|
209
|
+
|
|
210
|
+
| Criterion | The design must specify |
|
|
211
|
+
|---|---|
|
|
212
|
+
| 1.4.1 Use of colour | Links in running text are underlined, not colour-only. States (active nav, selected, error) also change shape, weight or an icon, not only colour |
|
|
213
|
+
| 1.4.4 Resize text | Font sizes in `rem`; nothing breaks at 200 % zoom |
|
|
214
|
+
| 1.4.10 Reflow | Works at **320 CSS px** wide with no horizontal scroll. Every component has a stated ≤ 320 px behaviour; decorative pieces never widen the page |
|
|
215
|
+
| 1.4.12 Text spacing | No fixed-height boxes around text, so nothing clips when a user overrides spacing (line-height 1.5, paragraph spacing 2×, letter-spacing 0.12 em, word-spacing 0.16 em). Fixed line-height tokens are fine; clipping is not |
|
|
216
|
+
| 2.2.2 Pause, Stop, Hide | Anything that moves on its own for more than 5 s (marquee, carousel, animated sticker) has a **visible pause control**, and doesn't move at all under `prefers-reduced-motion`. Hover-to-pause alone doesn't count |
|
|
217
|
+
| 2.3.1 Three flashes | Nothing flashes more than 3 times a second |
|
|
218
|
+
| 2.4.7 Focus visible | One focus style (e.g. 3 px outline, 2 px offset) whose colour reaches 3:1 against **every** surface it can appear on. Name a second colour for surfaces where the first fails |
|
|
219
|
+
| 2.4.11 Focus not obscured *(new in 2.2)* | Sticky headers, cookie bars and chat bubbles never cover the focused element. Either the header isn't sticky, or `scroll-padding-top: var(--header-h)` |
|
|
220
|
+
| 2.5.8 Target size *(new in 2.2)* | Interactive targets ≥ 24 × 24 px with spacing. On this grid: **48 px** for buttons, nav and inputs, ≥ 24 px for inline icon buttons |
|
|
221
|
+
| 3.2.6 Consistent help *(new in 2.2)* | The contact or help link sits in the same place on every page (e.g. last nav item and footer) |
|
|
222
|
+
| 3.3.2 Labels | Every form field has a visible label; placeholders are never the only label |
|
|
223
|
+
| 1.1.1 Text alternatives | Content images get alt text describing what they show; purely decorative images and shapes get empty alt or `aria-hidden` |
|
|
224
|
+
|
|
225
|
+
### 5.4 Fonts resolve
|
|
226
|
+
|
|
227
|
+
For each family, prefer the variable package and fall back to the static one:
|
|
228
|
+
`npm view @fontsource-variable/<slug> version`, then `npm view @fontsource/<slug> version`. One must
|
|
229
|
+
answer with a version. If neither does, swap to the closest family that does. Record the exact
|
|
230
|
+
package names. The **display** family must also resolve as a static package
|
|
231
|
+
(`npm view @fontsource/<slug> version`): the static packages ship `.woff` files the logo kit can
|
|
232
|
+
read; the variable ones ship only `.woff2`.
|
|
233
|
+
|
|
234
|
+
## Step 6 — Design the logo
|
|
235
|
+
|
|
236
|
+
**The user supplied a logo** → copy it into `brand/` unchanged, note it in the **Logo** section, and
|
|
237
|
+
skip the rest of this step. Never redraw or recolour it.
|
|
238
|
+
|
|
239
|
+
Otherwise, turn the name into a **designed** logo: shaped, coloured and composed. Typing the name in
|
|
240
|
+
the display font with an emoji or symbol next to it doesn't count. Build the **logo idea** from the
|
|
241
|
+
chosen direction with one or two of these moves:
|
|
242
|
+
|
|
243
|
+
| Move | What it is | Suits |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| **Letter swap** | One letter becomes a simple shape tied to the subject: O → smiley, sun, loaf, plate, eye; I → candle, pencil, brush; A → roof, tent. Swap **one character** only, even if the letter appears twice, and the word must still read | Playful, friendly brands |
|
|
246
|
+
| **Bounce** | Letters tilt ±3–6° and bob off the baseline, each in a palette colour, with an ink outline and hard shadow | Loud, maximal directions |
|
|
247
|
+
| **Stack** | The name split over 2–3 lines on coloured tiles, or set as a tight block | Long names, bold directions |
|
|
248
|
+
| **Tag** | The main word large; a short second word on a pill, ribbon or tab | Two-part names ("Dopamine Den") |
|
|
249
|
+
| **Seal** | The name around a ring, with a monogram or symbol in the middle | A secondary mark (footer, hero sticker, social avatar), never the header logo |
|
|
250
|
+
| **Typographic contrast** | Weight, style or case contrast between words (a light italic word next to a heavy upright one), custom tight spacing, one shared stroke or cut between letters | Quiet, refined, editorial directions |
|
|
251
|
+
|
|
252
|
+
No emoji, clip-art, gradients, or details that vanish at small sizes. Colours come only from the
|
|
253
|
+
palette (base and surface colours count).
|
|
254
|
+
|
|
255
|
+
### Build it with the logo kit
|
|
256
|
+
|
|
257
|
+
The kit (`logo-kit.mjs`, next to this file) does the error-prone parts: it outlines letters into
|
|
258
|
+
vector paths, sets text on a circle, draws the outline-and-shadow treatment, measures, and validates
|
|
259
|
+
the output. You write the composition. Copy the kit into `brand/` so the build is self-contained
|
|
260
|
+
and a redesign can re-run it:
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
mkdir -p ~/.hostinger/<domain>/brand && cd ~/.hostinger/<domain>/brand
|
|
264
|
+
cp <the folder this SKILL.md is in>/logo-kit.mjs .
|
|
265
|
+
npm init -y >/dev/null && npm i opentype.js @fontsource/<display-slug>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Write `brand/logo-build.mjs`, import from `./logo-kit.mjs`, and always run it from `brand/`
|
|
269
|
+
(packages resolve from the working directory):
|
|
270
|
+
|
|
271
|
+
| Kit function | Use |
|
|
272
|
+
|---|---|
|
|
273
|
+
| `loadFont(slug, weight, style)` | Loads a static `@fontsource` weight across all its subsets (`latin`, `latin-ext`, …), so accented letters like Ė or Ł resolve; `style` is `'normal'` or `'italic'`. Throws if that weight or style isn't in the package, so pick another |
|
|
274
|
+
| `layout(font, text, size)` | Kerned glyph positions, total width, `capHeight`, `xHeight`; place letters one by one for bounce or swap. Throws if a character isn't in the font |
|
|
275
|
+
| `glyphPath(glyph, x, baselineY, size)` / `textPath(font, text, x, baselineY, size)` | Outlined path data for one letter or a whole word. For typographic contrast, load two weights or styles and set each word with `textPath` |
|
|
276
|
+
| `pathBox(font, text, x, baselineY, size)` | Tight box around a word's outlines; size the viewBox from it plus room for outlines, shadows and tilt |
|
|
277
|
+
| `arcText(font, text, { cx, cy, r, size })` | Letters around a circle, for seals |
|
|
278
|
+
| `sticker(d, { fill, ink, strokeWidth, shadow, shadowColor, transform })` | Ink outline + hard offset shadow, for outlined directions. On dark backgrounds give the outline and shadow a palette colour that shows (`shadowColor` defaults to `ink`) |
|
|
279
|
+
| `svgDoc({ width, height, title, body })` | Root SVG with `role="img"` and a `<title>` |
|
|
280
|
+
| `check(svg)` | Throws on `NaN`, live `<text>`, a missing title or viewBox. Run it on every file before writing |
|
|
281
|
+
|
|
282
|
+
Never call opentype's `path.toPathData()` directly; the kit's serializer exists because opentype.js
|
|
283
|
+
2.0 writes `NaN` into some paths, and browsers silently stop drawing at the first `NaN`.
|
|
284
|
+
|
|
285
|
+
### Deliver four files
|
|
286
|
+
|
|
287
|
+
| File | What | Must read clearly at |
|
|
288
|
+
|---|---|---|
|
|
289
|
+
| `brand/logo.svg` | The primary, mostly horizontal logo for the header | Its header display heights (desktop and small screens) |
|
|
290
|
+
| `brand/logo-mark.svg` | A square icon from the same idea (initial, monogram or the swapped symbol) on a solid rounded tile (corner radius ≈ ¼ of the side) in a palette colour | 32 px |
|
|
291
|
+
| `brand/favicon.svg` | The simplest form of the mark: same tile, no shadow, no thin details, outlines thickened if it has any | 16 px, on both light and dark browser tab bars |
|
|
292
|
+
| `brand/logo-build.mjs` | The script that built them, with `package.json`, kept so a redesign can adjust it | — |
|
|
293
|
+
|
|
294
|
+
Add `brand/logo-seal.svg` only if the direction uses a seal as a secondary mark. Delete
|
|
295
|
+
`brand/node_modules` when you're done; a redesign runs `npm i` in `brand/` first.
|
|
296
|
+
|
|
297
|
+
**Check the sizes.** Display heights are multiples of 8, with one for desktop and one for small
|
|
298
|
+
screens (they may be equal). Work them out from the viewBox:
|
|
299
|
+
|
|
300
|
+
- **Letter size:** letter height on screen = letter height ÷ viewBox height × display height, and it
|
|
301
|
+
must be ≥ 16 px. Use `xHeight` when the logo is mostly lowercase and `capHeight` when it's mostly
|
|
302
|
+
capitals; for mixed fonts, take the smaller word. A two-line lockup usually needs 64 px.
|
|
303
|
+
- **Headroom:** `pathBox` covers the letters, not what you add. Leave room above for accents (Ė, Ž, Å sit above cap height) and below for descenders, plus whatever bounce, tilt, outline and shadow add, so nothing touches the viewBox edge.
|
|
304
|
+
- **Width:** width on screen = viewBox width ÷ viewBox height × display height. At the small-screen
|
|
305
|
+
height it must be **≤ 192 px**, which leaves room for the menu button in a 320 px header. If it
|
|
306
|
+
doesn't fit, lower the small-screen height (letters still ≥ 16 px), simplify the logo, or show
|
|
307
|
+
`logo-mark.svg` with the name beside it on small screens.
|
|
308
|
+
- **Look at it.** If you have a browser, render the files at both header heights, 32 px and 16 px,
|
|
309
|
+
the favicon on a light and a dark background, and fix anything that reads wrong. Without a
|
|
310
|
+
browser, rely on the numbers and list "logo not visually checked" under gaps in the final report.
|
|
311
|
+
|
|
312
|
+
The logo is exempt from text-contrast minimums, but the header link around it needs `alt` = the site
|
|
313
|
+
name and a target of at least 24 px.
|
|
314
|
+
|
|
315
|
+
Don't show the logo to the user in this step; they see it in the preview.
|
|
316
|
+
|
|
317
|
+
## Step 7 — Write `design.md` and hand off
|
|
318
|
+
|
|
319
|
+
Write `design.md` from the template below. Fill every section. The **Tokens** block is copied
|
|
320
|
+
verbatim into the frontend's global stylesheet, so it must be valid CSS. Every colour, font, type
|
|
321
|
+
size, line height, space, radius and shadow used elsewhere in the file must exist in it as a token.
|
|
322
|
+
The template's token list is the **minimum**: add what the design needs (`--color-error`,
|
|
323
|
+
`--color-on-accent`, `--leading-h3`…) under the same naming scheme.
|
|
324
|
+
**Breakpoints are the one exception**: CSS custom properties don't work inside `@media`, so they are
|
|
325
|
+
listed in *Grid, shape & motion* as plain px values.
|
|
326
|
+
|
|
327
|
+
Then update `state.json`: set `"stage": "frontend"` (also for a redesign: restyle, then validate and
|
|
328
|
+
deploy) and `updated_at` to the current UTC time (ISO 8601). Don't touch `provision`, `run` or
|
|
329
|
+
`history` (`run-log.mjs` owns those), or add other keys.
|
|
330
|
+
|
|
331
|
+
If content prepare hasn't sourced images yet, it now follows `design.md` → **Imagery** (see
|
|
332
|
+
`04-content-model/SKILL.md`).
|
|
333
|
+
|
|
334
|
+
## `design.md` template
|
|
335
|
+
|
|
336
|
+
````markdown
|
|
337
|
+
# Design — <domain>
|
|
338
|
+
|
|
339
|
+
> Direction **<name>**, selected on <YYYY-MM-DD> from three options (others offered: <B name>,
|
|
340
|
+
> <C name>). <"Picked by the user." | "User said 'you decide'; recommendation A used." |
|
|
341
|
+
> "Merged by the user: <what>.">
|
|
342
|
+
|
|
343
|
+
## Direction
|
|
344
|
+
<2–3 sentences: the mood, what it borrows from the references (and any brief/reference mismatch),
|
|
345
|
+
the signature element.>
|
|
346
|
+
|
|
347
|
+
## Stack
|
|
348
|
+
**<chosen by the frontend step>** — design doesn't decide the stack. The frontend step (06) picks and
|
|
349
|
+
confirms the framework (default Next.js SSR; ecommerce must be SSR) and records it in
|
|
350
|
+
`state.json → frontend`. Leave this as a placeholder for the frontend step to fill in.
|
|
351
|
+
|
|
352
|
+
## Tokens
|
|
353
|
+
```css
|
|
354
|
+
:root {
|
|
355
|
+
/* colour — semantic names, never "--pink-2" */
|
|
356
|
+
--color-base: #…; --color-text: #…; --color-surface: #…; --color-muted: #…;
|
|
357
|
+
--color-primary: #…; --color-accent-1: #…; --color-accent-2: #…; --color-link: #…;
|
|
358
|
+
--color-focus: #…; --color-focus-alt: #…; /* for surfaces where --color-focus is < 3:1 */
|
|
359
|
+
/* type */
|
|
360
|
+
--font-display: '<Family> Variable', <fallback stack>;
|
|
361
|
+
--font-body: '<Family> Variable', <fallback stack>;
|
|
362
|
+
--text-body: …rem; --leading-body: …rem; /* 8-multiple in px */
|
|
363
|
+
--text-small: …rem; --leading-small: …rem; /* 4-multiple in px */
|
|
364
|
+
--text-h1: clamp(…rem, …, …rem); --text-h2: clamp(…); --text-h3: …rem;
|
|
365
|
+
--leading-display: 1.05; /* unitless; fluid headings are grid-exempt */
|
|
366
|
+
/* 8-point grid */
|
|
367
|
+
--space-half: 4px; --space-1: 8px; --space-2: 16px; --space-3: 24px; --space-4: 32px;
|
|
368
|
+
--space-6: 48px; --space-8: 64px; --space-12: 96px; --space-16: 128px;
|
|
369
|
+
--target-min: 48px;
|
|
370
|
+
/* shape */
|
|
371
|
+
--radius-sm: 8px; --radius-card: …; --radius-pill: 999px;
|
|
372
|
+
--border: …; --shadow: …; --shadow-hover: …;
|
|
373
|
+
--focus-ring: 3px solid var(--color-focus); --focus-offset: 2px;
|
|
374
|
+
/* layout */
|
|
375
|
+
--content-max: …px; /* multiple of 8 */
|
|
376
|
+
--header-h: …px; /* header band height, multiple of 8; scroll-padding-top uses it if sticky */
|
|
377
|
+
--logo-h: …px; /* header logo height on desktop, multiple of 8 */
|
|
378
|
+
--logo-h-sm: …px; /* on small screens; logo ≤ 192 px wide at this height */
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Palette roles
|
|
383
|
+
| Token | Hex | Used for |
|
|
384
|
+
|---|---|---|
|
|
385
|
+
|
|
386
|
+
## Contrast
|
|
387
|
+
| Foreground | Background | Kind (body text / large text / non-text) | Ratio | Needs | Passes |
|
|
388
|
+
|---|---|---|---|---|---|
|
|
389
|
+
|
|
390
|
+
## Type
|
|
391
|
+
- **Display:** <family> (`@fontsource-variable/<slug>@<version>`), weights …, tracking …; sizes `--text-h1…h3`, `--leading-display`
|
|
392
|
+
- **Body:** <family> (`@fontsource-variable/<slug>@<version>`); `--text-body` / `--leading-body`, measure ≤ 68ch
|
|
393
|
+
|
|
394
|
+
## Logo
|
|
395
|
+
- **Concept:** <the move(s), and what any swapped letter or symbol stands for | "Supplied by the user">
|
|
396
|
+
- **Built from:** `@fontsource/<display-slug>@<version>`, weight(s)/style(s) …
|
|
397
|
+
- **Files:** `brand/logo.svg` (header, `--logo-h` desktop / `--logo-h-sm` small screens),
|
|
398
|
+
`brand/logo-mark.svg`, `brand/favicon.svg`<, `brand/logo-seal.svg`>; source `brand/logo-build.mjs`
|
|
399
|
+
- **Measured:** letters … px tall at `--logo-h-sm`; width … px at `--logo-h-sm`
|
|
400
|
+
- **Use:** the frontend copies `brand/*.svg` into `public/`. Header: `<a href="/"><img src="/logo.svg"
|
|
401
|
+
alt="<Site name>" style="height: var(--logo-h)"></a>` (switch to `--logo-h-sm` on small screens).
|
|
402
|
+
Favicon: `<link rel="icon" href="/favicon.svg" type="image/svg+xml">`
|
|
403
|
+
- **Don't:** recolour, stretch, add effects, or retype the logo as live text.
|
|
404
|
+
|
|
405
|
+
## Grid, shape & motion
|
|
406
|
+
<section padding desktop/mobile (space tokens), gutters, breakpoints in px (multiples of 8), borders,
|
|
407
|
+
radii, shadows, hover behaviour, every animation and its pause control, and "no motion under
|
|
408
|
+
prefers-reduced-motion".>
|
|
409
|
+
|
|
410
|
+
## Accessibility (WCAG 2.2 AA)
|
|
411
|
+
- **Focus:** <style; which colour on which surface>
|
|
412
|
+
- **Targets:** <48 px controls; inline exceptions>
|
|
413
|
+
- **Links & states:** <underline rule; how active/error states differ beyond colour>
|
|
414
|
+
- **Motion:** <what moves, where its pause control is>
|
|
415
|
+
- **Sticky elements:** <none | header uses --header-h + scroll-padding-top>
|
|
416
|
+
- **Help:** <where the contact link sits on every page>
|
|
417
|
+
- **Text on images:** <none | scrim spec and resulting ratio>
|
|
418
|
+
- **Reflow:** <anything special at 320 px>
|
|
419
|
+
- **Exempt from contrast:** <disabled controls / decorative shapes, named>
|
|
420
|
+
|
|
421
|
+
## Components
|
|
422
|
+
| Component | Renders (from content model) | Look | ≤ 320 px |
|
|
423
|
+
|---|---|---|---|
|
|
424
|
+
| Header + nav | Navigation items | … | … |
|
|
425
|
+
| Post card | Article: image, category, title, excerpt | … | … |
|
|
426
|
+
| … | | | |
|
|
427
|
+
|
|
428
|
+
## Page composition
|
|
429
|
+
- **<Page>** — <sections top to bottom, each naming its component and band colour>
|
|
430
|
+
- …
|
|
431
|
+
|
|
432
|
+
## Imagery
|
|
433
|
+
<subjects, mood and colour fit for photos; search keywords; treatment (frame, crop ratio, radius,
|
|
434
|
+
duotone…); alt text written from what the photo shows.>
|
|
435
|
+
|
|
436
|
+
## Don'ts
|
|
437
|
+
- <3–6 site-specific rules, e.g. "no grey cards", "never put body text on the primary accent">
|
|
438
|
+
````
|
|
439
|
+
|
|
440
|
+
## Invariants
|
|
441
|
+
|
|
442
|
+
- **One question.** Refine owns discovery; this step only chooses among directions (or keeps the
|
|
443
|
+
existing one).
|
|
444
|
+
- **8-point grid, always.** No spacing, size, radius or breakpoint off the grid except the listed
|
|
445
|
+
exemptions.
|
|
446
|
+
- **WCAG 2.2 AA is the floor.** No direction is offered, chosen or written that fails it. Brand
|
|
447
|
+
colours keep their hue but only appear in roles where they pass.
|
|
448
|
+
- **Brand assets are fixed.** A supplied logo, colour or font appears in every direction, unchanged.
|
|
449
|
+
- **A logo is designed, not typed.** Outlined SVG paths built with the logo kit, never live text,
|
|
450
|
+
emoji or clip-art, and always readable at its smallest use.
|
|
451
|
+
- **Self-hosted fonts only** (`@fontsource`). No Google Fonts or other font CDN links in the output.
|
|
452
|
+
- **The tokens block is the only source of values** (breakpoints excepted). Components and pages
|
|
453
|
+
reference tokens, never raw hex codes, pixel values or font names.
|
|
454
|
+
- **No backend calls.** Design never waits on provision and never writes to the instance.
|
|
455
|
+
- **Never overwrite an existing `design.md` silently.** Keep or change, as in Step 1.
|