# Pattern: agent spec

Twin of `/pattern`. Source wins: `app/utils/pattern-generator.ts`, `PianoRoll.vue`, `MotifSquare.vue`. Their upstream is `scripts/piano-roll-generator-v10.js` and `scripts/piano-roll-generator-square-v1.js` in the brand’s `.pen` file. Keep the ports in step.

## Cover pattern

`generatePatternBlocks({ cols, rows, seed })`, then `placePhoto({ blocks, cols, rows, seed, size = 3, edges? })`, then `patternGradient({ block, primary, secondary })`.

1. Cells are square. A 1440×630 (16:7) cover is 16 × 7 cells at 90px. The cover exports on `/pattern` are 2880×1260 (2×). For other sizes, scale the cell, not the ratio.
2. The bottom row is clear. Usable rows = `rows - 1`. It holds the wordmark and caption.
3. The path starts on a random usable row and walks left to right.
4. Block width is 1 or 2 columns: 55% chance of 1 when 2 fits. The last column takes a 1-column block.
5. After each block, the row steps exactly ±1 (50/50), never 0. At the top or bottom usable row the step is forced inward. Blocks join edge to edge or corner to corner, never with a gap.
6. Maximum 3 consecutive steps in one direction. After 3, the next step reverses.
7. Each block picks its gradient direction (`invert`) at random, 50/50. `patternGradient({ block, primary: ink, secondary: surface })` returns `linear-gradient(to top, from, to)`: `invert: false` is surface at the foot to ink at the head, `invert: true` is ink at the foot to surface at the head.
8. Photo: a square of `size` × `size` cells on the path: 3 × 3 by default, 4 × 4 in the live demo. It replaces the blocks in its columns and splits blocks that cross its edges.
9. The path enters one side of the photo and leaves the other, edge to edge or corner to corner.
10. The photo keeps at least one column of path on each side: `col` is in `1 … cols - size - 1`. With `edges`, `col` is picked from `edges` instead (the composition grid on `/layout`, which may put the square against a margin).
11. The photo spans the rows its covered blocks use, grown to `size` one row at a time: up or down at random (50/50), up when the bottom edge is on the last usable row, down when the top edge is on row 0. It never enters the bottom row.
12. Background is `brand/surface`. Gradients run between `brand/surface` and `brand/ink` (`/agent/colour.md`).
13. PRNG: `mulberry32`. Blocks seed `max(1, floor(seed × 1000))`. Photo seeds `max(1, floor(seed × 7919))`. One seed reproduces one pattern.
14. Photo on a cover, rendered: `<img alt="">`, `object-cover`, over the photo’s `size` × `size` cells.

`.pen` v10 inputs: `{ density, seed, palette, layout, inkStyle: "Theme", showImage, imageSpan, title, caption, mark: "Motive Unknown", showIcon, icon, image }`. `title` and `caption` accept `\n`. `PianoRoll.vue` does not port `density`, `inkStyle`, `imageSpan`, `title`, `caption`, `mark`, `showIcon` or `icon`.

## Live demo: `::piano-roll`

20 × 10 square cells on a 2:1 canvas, the content's full width, no tile. The photo is a 4 × 4 box (`size` 4) holding the mark, `role="img"`, `aria-label="Motive Unknown"`. Colours: `var(--grey)` (background, the tile grey) and `var(--ui-primary)`. Gradients run between the two. The mark box: `bg-primary`, `LogoMark` in `var(--ui-bg)` at `100cqi`. No palette or layout props. Seed: `usePatternSeed()` (`useState('pattern-seed')`, starts at 1, Regenerate adds 1), shared with the Regenerate button that frontmatter `regenerate: true` puts in the page header. No wordmark, title or caption.

## Cover examples

`/pattern`'s Covers section shows three `::placeholder-image` blocks (`/placeholder.jpg`, 3:2), no captions.

## Square motif (1:1)

`MotifSquare.vue`, `::motif-square{title}`, on `/colour`. Default `title`: `"Motive\nUnknown"`. Colours: `var(--ui-bg)` and `var(--ui-primary)`. Square canvas, centred in a 3:2 `bg-elevated` tile with `p-6` (24px) padding. It has its own Regenerate button (neutral outline, above the tile) and a local seed (starts at 1, Regenerate adds 1). Blocks seed `max(1, floor(seed × 1000))`.

1. The canvas is square. Fractions of W and H are interchangeable.
2. One PRNG stream generates two bands in order: top, then bottom.
3. Top band: `5 × density` columns, 3 row slots. Notes are 1 or 2 columns wide (55% chance of 1 when 2 fits). `invert` is random per note.
4. Bottom band: `10 × density` columns, 3 row slots. Notes are 1 column wide. `invert` starts random, then flips every note.
5. Row step per note: one of `{0, -1, +1}`, clamped to the band. Maximum 3 consecutive non-zero steps in one direction. Step 0 resets the count.
6. Middle band height: `titleGapAbove + titleFontSize × titleLineHeight × titleLines + titleGapBelow`. `titleLines` wraps the title at `floor(titleMaxWidth / (titleFontSize × 0.44))` characters.
7. Bottom band height: `min(3 / bottomCols, space left)`. The top band takes the rest.
8. Ratios of W: `marginX = 0.052`, `titleFontSize = 0.105`, `titleLineHeight = 0.95`, `titleMaxWidth = 0.82`, `titleGapAbove = 0.03`, `titleGapBelow = 0.05`.
9. Title: Overused Grotesk, weight 500 (the site’s one weight), ink colour, `letterSpacing = -titleFontSize × 0.01`, left at `marginX`, `titleGapAbove` from the top of the middle band, `\n` breaks kept. Sizes are `cqw` of the square.
10. Gradient: `linear-gradient(to top, …)` as on the cover. `invert: false` is surface at the foot to ink at the head.
11. The demo omits the `.pen` image layer (`showImage`, `image`, `imageSpan`, over the top band) and fixes `density = 1`.

## Rules

1. Always generate the pattern with the seeded generator. Never draw it by hand or replace it with a static gradient.
2. Use the pattern behind a title on a cover or a motif. A report carries it once, on its hero (`::report-cover`); a break between parts is a plain block of colour or a photo. Never put body copy or a table on it.
3. Keep a cover’s bottom row clear for the wordmark and caption. On a square motif, keep the title in its own band. Placement: `/agent/logo.md`.
4. A cover photo is 3 × 3 cells, sits on the path, keeps one column of path on each side and stays off the bottom row.
5. Paint the pattern with the active surface and ink only. On the site, use `var(--ui-bg)` and `var(--ui-primary)`.

## Related

- `/agent/colour.md`: surface and ink, site theme.
- `/agent/logo.md`: wordmark placement on covers.
