# Donation Router — Brand & Asset System

Everything in `.wordpress-org/` is generated. `icon.svg` and
`resources/brand/banner.html` are the only files edited by hand — never touch
the PNGs directly, they are overwritten on every render.

---

## 1. Asset inventory

| File | Dimensions | Purpose | Source |
|---|---|---|---|
| `icon.svg` | vector | Canonical icon. WordPress.org consumes this directly when present. | Authored here (master) |
| `icon-256x256.png` | 256×256 | Directory hero icon (retina) | Rasterised from `icon.svg` |
| `icon-128x128.png` | 128×128 | Directory listing grid | Rasterised from `icon.svg` |
| `icon-1024x1024.png` | 1024×1024 | Channels outside the directory that want a large mark | Rasterised from `icon.svg` |
| `banner-1544x500.png` | 1544×500 | Desktop directory banner (retina) | `resources/brand/banner.html` |
| `banner-772x250.png` | 772×250 | Mobile / non-retina banner | Same markup, narrow variant |
| `social-card-1280x640.png` | 1280×640 | Open Graph / social preview | Same markup, social variant |
| `screenshot-*.png` | 1200×900 | Listing screenshots, branded frame | Live site capture, composed |

Only the 128/256 icons and the 772×250 / 1544×500 banners are used by the plugin
directory itself; the other sizes exist for other channels and cost nothing to
keep in step now that they are generated rather than drawn.

---

## 2. Design tokens

### 2.1 Colour

**The palette lives in `tests/assets/brand.js`, and only there.** The icon, the
banners and the screenshot frames all read it. Do not copy a hex out of that
file into another one: the previous `icon.svg` and `resources/branding/*.svg`
each carried their own list, and two copies of a palette agree only until
somebody edits one.

`icon.svg` is the exception the format forces — SVG cannot import. Its values are
duplicated deliberately and its header names `brand.js` as canonical.

| Token | Hex | wp-admin stop | Usage |
|---|---|---|---|
| `ink` | `#01263a` | blue-90 | Ground, darkest stop |
| `inkMid` | `#043959` | blue-80 | Ground, middle stop |
| `inkLift` | `#0a4b78` | blue-70 | Ground, far corner |
| `royal` | `#2271b1` | blue-50 | Accent; the pool low in the field |
| `royalLight` | `#3582c4` | blue-40 | Accent, lighter |
| `sky` | `#72aee6` | blue-20 | Frames' kicker; the second terminal on the mark |
| `accent` | `#c5d9ed` | blue-10 | "Router" in the wordmark |
| `shadow` | `1, 38, 58` | blue-90 | Shadow under white cards, so shadows read blue |
| `markCrown` / `markMid` / `markBase` | `#ffffff` / `#eaf2fa` / `#c5d9ed` | — | The glyph's fill, crown to base |

#### Why this blue

Because it is the plugin's own blue. `src/admin/admin.scss` and
`src/frontend/country-selector.scss` use `#2271b1` and `#135e96` — `$blue-50`
and `$blue-60` from `@wordpress/base-styles` — alongside the core greys, because
the UI was deliberately restyled onto the WordPress admin palette.

The brand had not followed. It was a hand-picked navy / cream / ochre set with
nothing in common with the interface it was advertising, so a reader scrolling
the listing saw an ochre-on-cream banner above screenshots of a blue-and-white
admin screen: two products photographed together.

A wp-admin palette is, by construction, not distinctive — every plugin that
behaves itself draws from the same ten hexes. What separates this listing at the
128px the directory grid actually uses is the mark, not the hue.

#### The field

Three layers, painted bottom up, identical in the icon's SVG and in `field()`:

1. **Ground** — deep blue, ramping toward the far corner
2. **Pool** — `royal` light gathering low and to the right, as in a thick pane
3. **Specular** — a white sweep off the top edge, spent by the top third

**No rim stroke along the top edge.** At listing sizes it does not read as a lit
edge — it reads as a hairline across the artwork, and once visible as a line it
is the first thing the eye finds. The specular layer already does that work.

### 2.2 Typography

Inter, falling back to the system stack. The wordmark is 800 weight at −2.5%
tracking; the tagline is 400; capability pills are 600.

### 2.3 Geometry

- **Squircle radius**: 22% of the canvas (256 → 56px), matching the modern
  platform icon grid.
- **Glyph**: heart from y 47 to its tip at 105; fork to terminals at y 196,
  r 17. Stroke 20 at the 256 master, round caps and joins.

---

## 3. Icon direction

**A heart that forks into two terminals.**

The product routes a donation to one of several legal entities based on the
donor's country, so the glyph carries both halves: the heart is the donation,
the fork is the routing. The two end nodes are deliberately different colours,
because the entire point is that they are different accounts — a single hue
there draws a fork to nowhere in particular.

The previous mark drew the fork alone, in three off-palette colours, on a cream
card inset in a dark card, with 2px corner registration ticks. At 128px the
ticks vanished, the card-in-card read as a rendering fault, and a bare fork says
"org chart".

Judge an icon at 128px and 32px, never at 512px.

---

## 4. Banner composition

A wordmark block on the left, the mark at hero scale on the right. One
composition, three sizes, driven entirely by CSS custom properties on `.banner`
— the narrow and social variants are the same design re-measured, not separate
files that drift apart, which is what `resources/branding/*.svg` was.

The wordmark reads "Donation Router", not the full directory title. That title
is written for search and is already printed above the banner on the listing
page; setting it again at 68px wraps to three lines and leaves no room for the
mark.

Nothing meaningful touches an edge: WordPress.org crops banners on some views.

---

## 5. Screenshot composition

The screenshots are not raw captures. Each is the plugin's real UI, photographed
from a running site, inset on a white card inside a branded 1200×900 frame:
field, icon and wordmark top left, a kicker plus title naming the screen, and a
footer line.

Raw admin captures make a listing look like seven pictures of WordPress with
some of this plugin in them, and nothing in the carousel says whose they are.
The frame is what makes seven images read as one product.

Three rules the capture step exists to enforce:

- **The capture viewport is wider than the card** (1600×1000 against 1108×604).
  The card fills with `object-fit: cover`, so the capture is clipped to the
  plugin's own element and padded out to the card's ratio — spending the whole
  card on the plugin rather than on wp-admin's margins.
- **Chrome that is not this plugin is hidden** — the admin bar and menu, admin
  notices, and the theme's own header and footer.
- **Every address is scrubbed to example.com.** These images are published where
  anyone can read them and anything can scrape them.

### The data behind the screenshots

`resources/dev/seed.php` builds the demo site in scopes:

```sh
wp eval-file resources/dev/seed.php              # everything
wp eval-file resources/dev/seed.php -- list      # what the scopes are
wp eval-file resources/dev/seed.php -- only=donations
wp eval-file resources/dev/seed.php -- remove
```

| Scope | What it creates |
|---|---|
| `form` | A published GiveWP donation form, if the site has none |
| `accounts` | Three PayPal Business accounts (US, CA, GB), one deactivated |
| `routing` | Country routing rules mapping US and CA to their entities |
| `page` | A landing page carrying `[ador_country_selector]` |
| `donors` | Six GiveWP donors, split between the two entities |
| `donations` | Completed donations per donor |
| `settings` | Plugin options: selector popup, geolocation, receipts |

It does **not** write `_ador_processing_account_id`, `_ador_donor_country` or
`_ador_receipt_number`. It calls GiveWP's own `give_insert_payment()`, which
fires the hooks this plugin listens on, so that meta — including the atomic
receipt-sequence allocation — is produced by the same code a real donation runs
through. A seeder that wrote it directly would produce a convincing receipt
screen sitting on top of a numbering path that had never executed.

The scope reports what the plugin's hooks actually produced:

```
created 18 donation(s); 18 carry a routed account, 18 carry a receipt number
```

A mismatch there is a real fault, surfaced when the data is built rather than
discovered in a published screenshot.

---

## 6. Building the assets

```sh
yarn install

# icon-*.png, banner-*.png, social-card-*.png — renders local markup only
yarn assets:brand

# screenshot-*.png — seeds the site, then drives a real WordPress admin
WP_LOGIN_URL="$(wp login create admin --url-only)" yarn assets:shots
```

| File | Role |
|---|---|
| `playwright.assets.config.js` | The `brand`, `setup` and `shots` projects |
| `tests/assets/brand.js` | The palette, the field, and the glyph — canonical |
| `tests/assets/auth.setup.js` | Signs in once, stores the session for `shots` |
| `tests/assets/wporg-brand.spec.js` | Icon PNGs from `icon.svg`; banners from `banner.html` |
| `tests/assets/wporg-shots.spec.js` | The listing screenshots |

Deliberately separate from `playwright.config.js`, which is the end-to-end suite
with its own reporters and failure behaviour. Asset rendering shares none of
that and should not be able to break it.

`channel: 'chrome'` is intentional: it drives the installed browser instead of
requiring `npx playwright install`. Set `PLAYWRIGHT_CHANNEL=` to fall back to
the bundled Chromium.

### Authentication

`auth.setup.js` takes either a one-time magic link (`WP_LOGIN_URL`, e.g. from
`wp login create admin --url-only`) or `WP_USERNAME` + `WP_PASSWORD`. No
credential is written to the repository; the session lands in `artifacts/`,
which is git-ignored.

### After a UI change

Any change to the admin screens or the country selector invalidates the
screenshots. Re-run `yarn assets:shots` and commit what changed — the captions
in `readme.txt` are positional, so adding or removing a shot means editing that
list too.

---

## 7. Forbidden

- Hand-editing any PNG in this folder.
- A second icon direction living alongside the master.
- A hex in `icon.svg` or `banner.html` that is not in `tests/assets/brand.js`.
- Text smaller than ~14px at the 772×250 banner size; it is illegible on the
  listing page and unreadable when the directory crops it further.
- Anything meaningful within 24px of a banner edge.
