# Making a style

Write a style by hand, from its folder, style.json, style.css and the kit, taste.md, and refs, to proving, checking, and packing it.

From an empty folder to a proved, checked zip. Every field is in [package files](/docs/style-json); the built-in styles under `styles/` (`bubble`, `conspiracy`, `launch`, `poster`, `swiss`, `terminal`) are complete examples.

## 1. The folder

```text
my-style/
  manifest.json
  style.json
  style.css        the kit's classes, styled
  taste.md
  refs/            three to eight stills (optional, for reviewers)
  vectors/         the style's own marks, as SVG (optional)
  fonts/           the style's own faces, each folder with its licence (optional)
  components/      working scenes for the style's own pieces, to copy (optional)
  proof/           scene.html, and what rapid style proof renders from it
```

Allowed file types: `.json`, `.css`, `.html`, `.md`, `.txt`, `.svg`, `.png`, `.jpg`, `.jpeg`, `.webp`, `.mp4`, `.wav`, `.woff2`, `.woff`, `.ttf`, `.otf`. The packed zip is at most 25 MiB.

## 2. manifest.json

Start with the identity; leave `files` empty and let the check fill it.

```json
{
  "kind": "style",
  "name": "my-style",
  "version": "0.1.0",
  "schema_version": 1,
  "description": "Big tight type on a warm field, one accent, exits off the edge.",
  "tags": ["editorial", "minimal"],
  "formats": ["reel", "square"],
  "fonts": [
    { "family": "Helvetica Neue", "weight": 700, "substitutes": ["Helvetica", "Arial"] }
  ],
  "files": []
}
```

A name is lowercase `a-z`, `0-9`, and `-`, starting with a letter, 2 to 48 characters. A version is semantic (`1.2.0`). Every format in `formats` must also appear in `style.json`'s `formats`.

## 3. style.json

`styles/swiss/style.json`:

```json
{
  "schema_version": 1,
  "tokens": {
    "palette": {
      "field": "#F5F4F0",
      "paper": "#FFFFFF",
      "ink": "#0E0E0E",
      "accent": "#E3241B",
      "red": "#E3241B",
      "cool": "#C9C8C3",
      "screen": "#C9C8C3",
      "grey": "#6B6A66",
      "white": "#FFFFFF"
    },
    "fonts": {
      "display": { "family": "Helvetica Neue", "weight": 700, "substitutes": ["Helvetica"] },
      "ui": { "family": "Helvetica Neue", "weight": 400, "substitutes": ["Helvetica"] },
      "label": { "family": "Helvetica Neue", "weight": 500, "substitutes": ["Helvetica"] },
      "voice": { "family": "Helvetica Neue", "weight": 700, "substitutes": ["Helvetica"] }
    },
    "type_scale": { "hero": 360, "title": 120, "body": 38, "small": 32 },
    "leading": { "min": 0.8, "max": 1.27 },
    "margins": { "x": 72, "y": 72 }
  },
  "motion": {
    "exit": "off-edge",
    "easing": {
      "in": "outExpo",
      "settle": "outQuart",
      "out": "cubic-bezier(0.7, 0, 0.84, 0)",
      "drift": "linear"
    },
    "transitions": ["cut", "columns", "slide"],
    "pacing": { "line_hold": 0.7, "shot": 2.4, "words_per_second": 3.4, "chars_per_second": 36 }
  },
  "sound": {
    "cues": {
      "rule": "hat",
      "grid": "click",
      "word": "click",
      "land": "kick",
      "block": "kick",
      "number": "sub",
      "count": "click",
      "columns": "whoosh",
      "plate": "rim",
      "form": "kick",
      "horizon": "pad",
      "enter": "whoosh",
      "exit": "whoosh",
      "settle": "pad"
    },
    "colors": { "red": "kick", "accent": "kick", "ink": "rim", "cool": "keys", "screen": "keys", "grey": "keys", "paper": "room", "white": "room", "field": "room" },
    "tempo": 104
  },
  "components": {
    "rule": { "heavy": 16, "hair": 2 },
    "columns": { "stagger": 0.07, "dur": 0.55, "edge": 2 },
    "display": { "tracking": -0.05, "lead": 0.86 }
  },
  "formats": {
    "reel": {},
    "square": { "type_k": 0.8 },
    "wide": { "type_k": 0.8, "margins": { "x": 96, "y": 72 }, "grid": { "columns": 12, "gutter": 24, "baseline": 8 } }
  },
  "grid": { "columns": 6, "gutter": 24, "baseline": 8 },
  "video": { "steps": { "hero": 360, "number": 640, "title": 120, "caption": 52, "label": 32 }, "hero_ratio": 3 }
}
```

- **palette**: colours by name. Name them by their job; the kit expects `field`, `ink`, `accent`, `paper` (a card's ground), and `cool` (a second colour), and you may add more. Scenes see each as `--rapid-color-<name>`, and a cue's `color` names one.
- **fonts**: faces by role (`display`, `voice`, `ui`, and any more you name, such as `label`), spelled as the system names them or as your `@font-face` declares them (see [fonts in a style](#fonts-in-a-style)); list only substitutes you'd accept in a finished video. Scenes see each as `--rapid-font-<role>`.
- **type_scale**: pixels at the format's base width; the kit expects `hero`, `title`, `body`, and `small`, as `--rapid-size-<step>`. **margins**: the insets everything sits inside. **leading**: the range of line heights.
- **motion**: how things leave, eases by role (as `--rapid-ease-<role>`), transitions, and the pacing every scene times its lines by.
- **sound**: cue kinds to instruments, palette names to instruments, and an optional tempo. Cue kinds are what scenes emit; the instruments are `kick`, `hat`, `pad`, `sub`, `keys`, `rim`, `room`, `click`, and `whoosh`, and `rapid style check` refuses any other.
- **components**: free-form parameters a scene's scripts may read from `window.rapidStyle`; most styles leave it empty and put the look in `style.css`.
- **formats**: every format the style supports, with optional `margins`, `type_k` (a factor on the type scale), and `grid`. A format not listed isn't supported.
- **grid**: columns between the margins, their gutter, and an optional baseline step, as `--rapid-grid-*`; the `grid` check holds key-frame text to it.
- **video**: a type scale for video (`steps`, as `--rapid-video-<step>`) and `hero_ratio`, the least the hero may be over the body text, which the `scale` check holds key frames to (default 3).

## 4. style.css

Every style styles [the kit](/docs/styles-and-formats#the-kit): `field`, `field-dark`, `display`, `title`, `body`, `small`, `voice`, `card`, `card-tile`, `card-name`, `card-sub`, and `accent`. Style them from the custom properties, so the type scale and margins follow the format. Part of `styles/terminal/style.css`:

```css
.field {
  background-color: var(--rapid-color-field);
  background-image: repeating-linear-gradient(180deg, rgba(0, 0, 0, 0.22) 0 1px, transparent 1px 4px);
  color: var(--rapid-color-ink);
  font-variant-ligatures: none;
  font-feature-settings: "zero" 1, "tnum" 1;
  text-shadow: 0 0 10px color-mix(in oklab, currentColor var(--tc-glow), transparent);
}
.display {
  margin: 0;
  font-family: var(--rapid-font-display);
  font-weight: 800;
  font-size: var(--rapid-size-hero);
  line-height: 0.9;
  letter-spacing: -0.04em;
  white-space: pre;
  font-feature-settings: "zero" 1, "tnum" 1;
}
.voice::before { content: "$ "; color: var(--rapid-color-accent); font-weight: 700; }
.accent { background: var(--rapid-color-accent); color: var(--rapid-color-field); text-shadow: none; box-decoration-break: clone; -webkit-box-decoration-break: clone; }
```

The kit sets the look: colour, face, weight, size, leading, tracking, case, corners, borders. Scenes set layout, so never position anything here. A `url()` names one of the style's own files, which is inlined; `@import` and outside addresses are refused. `rapid style check` refuses a `style.css` that leaves a kit class unstyled.


### Fonts in a style

A style may carry its own faces, so it renders the same on every machine. Put each family's files in its own folder under `fonts/`, with the full text of its licence beside them, and declare each face in `style.css` with `url("rapid:style/<path>")`:

```text
fonts/
  display/
    display-semibold.woff2
    OFL.txt
```

```css
@font-face {
  font-family: "Display Serif";
  font-weight: 600;
  src: url("rapid:style/fonts/display/display-semibold.woff2") format("woff2");
}
```

- **Only free licences.** The SIL Open Font License, Apache 2.0, or the Ubuntu Font Licence, in full: `OFL.txt` (or `LICENSE.txt`, `LICENCE.txt`, `LICENSE.md`, `LICENCE.md`, `UFL.txt`) in the same folder as the font. `rapid style check` refuses a font with no licence beside it, or one whose licence file is not a free licence's full text. A face you may not redistribute stays on your machine and out of the style.
- **Declared, not installed.** The page inlines each file as a `data:` address, so nothing is fetched. Every face is loaded before frame 0, and the fonts check counts it as loaded like an installed one.
- **Name it in `style.json`** as the `@font-face` names it, with its weight and style, and in the manifest's `fonts`.
- **Keep it light:** subset to the characters videos use and ship `.woff2`. The built-in `poster` carries six faces in under 140 KB.

## 5. taste.md

The judgment rules, in plain words. Reviewers read it beside the contact sheet; builders read it before they draw. The opening of the swiss style's:

```markdown
# swiss: taste

International Typographic Style as printed in the eighties and nineties: cultural posters,
annual reports, exhibition and signage systems. Its look is a method: the grid is decided before
anything is drawn, then every block is dropped onto grid lines, so each position looks like the
only possible one. When a frame looks retro, the surface was copied and the method was not.

Three moves make it:
1. **One enormous thing and a small exact column of text,** tied by a shared grid line, with the
   white space between them as the third element.
2. **Type as image.** A numeral or a word so big it is a shape: cropped by the sheet's edge,
   or turned upright to run the height of the columns.
3. **The visible system.** A heavy rule opens the sheet, a colophon names it, sections and
   sheets are numbered (`03 / 06`), and on information frames the grid itself is drawn.

This is a starting point, not a cage. The pieces in `components/` (display type set by its ink,
the bleed numeral, the column wipe, the ruled table, block chart, plate, flat form, horizon,
colophon, end bar) are meant to be copied, retuned, and recombined to fit what a video has to
say. Keep the method; vary the compositions.

## Checkable
- One family, Helvetica Neue: bold for display, regular for text, medium for labels and
  figures. No italic, no second family.
- Flush left, ragged right. Never centred, never justified.
```

Write rules a reviewer can see broken in a still. "Feels premium" isn't a rule; "type sits on a field of one colour, never on a gradient" is. Say what the style never does as plainly as what it does, and keep it to a page.

## 6. components/

A look usually needs pieces the kit doesn't have: red string between pins, a wave that floods
the frame, a lid that opens, a word built from block cells. A style can't carry scripts, so each
piece ships as a working scene, `components/<name>.html`: it links `rapid:style`, shows the piece
alone, and keeps its behaviour in one marked `<script>` block that starts with a `SETTINGS`
object. A builder copies that block into a scene and changes the settings, or writes a variation.
Its look lives in `style.css` (classes with a short prefix of the style's) and its pictures in the
style's folders, loaded as `rapid:style/<path>`. List them in `components/README.md`, one line
each. Write them as starting points, not a cage: the style's rules in `taste.md` are what a variation must keep.

## 7. refs/

A handful of stills that show the look at its best: a type frame, a busy one, a quiet one, a transition mid-way. Reviewers compare contact sheets against them. Use frames you made or have the rights to.

## 8. Prove it

The proof is the style's sample and its regression test. A style needs one to be shared in rapid's repository; installing and using a style don't.

Write `proof/scene.html`: an HTML scene, as in [writing scenes](/docs/writing-scenes), that links `rapid:style` and uses every kit class. Copy `styles/swiss/proof/scene.html` to start, or a piece from a built-in style's `components/`. Then render it:

```sh
rapid style proof ./my-style
```

It renders the scene in the style, in the first format the manifest lists (`--format` picks another), and writes:

```text
proof/example.mp4     the example, as a phone copy capped at 4 MB
proof/poster.png      the poster: the scene's last frame
proof/golden-1.png    golden frames at a quarter, half, three quarters, and 95% of the scene
proof/golden-2.png    (--times 0.5,1.2 picks the seconds instead)
…
```

Then it records them in the manifest's `proof` and refreshes its file list:

```json
"proof": {
  "scene": "proof/scene.html",
  "video": "proof/example.mp4",
  "poster": "proof/poster.png",
  "golden": [{ "t": 1, "path": "proof/golden-1.png" }, { "t": 2, "path": "proof/golden-2.png" }]
}
```

To see whether the style still draws the same, render the golden frames again and compare:

```sh
rapid style proof ./my-style --check
```

It prints a line per frame, `ok   proof/golden-1.png at 1s: 0.00`, with the mean difference per channel from 0 to 255, and changes nothing. A frame that differs by more than 1.5 is `DIFF`, and the command exits 1. Run it after an engine update, when nothing should change. After you change `style.css` or `style.json` on purpose, run `rapid style proof` again to make new golden frames.

## 9. Check it

```sh
rapid style check ./my-style --write
```

`--write` recomputes the manifest's `files` list (every file's path, sha256, and size), then checks the package: the manifest and `style.json` against their schemas, `style.css` styling the kit, every file listed and matching its hash, no file of a type that isn't allowed, no stylesheet reaching outside the package, the size limit, and the formats agreeing. It prints `my-style@0.1.0 ok <sha256>`, or every reason it failed and exits 1. Run it again after any edit.

To check as a pull request is checked, with the proof required:

```sh
rapid style check ./my-style --publish
```

## 10. Pack, install, use

```sh
rapid style pack ./my-style                  # writes my-style-0.1.0.zip
rapid style add ./my-style                   # installs it in ~/.rapid/styles/my-style/0.1.0/
rapid style use my-style --project ~/videos/launch   # pins it in the project's rapid.lock
rapid video ~/videos/launch --style my-style
```

A version, once installed, never changes: to change the style, raise its version. To share it, see [sharing styles](/docs/sharing-styles).

## Formats

A format is packaged the same way, with `kind: "format"` in its manifest and a `format.json` (see [Format](/docs/style-json#format)), through `rapid format check`, `pack`, `add`, and `use`. A format has no stylesheet and needs no proof. The built-in ones in `formats/` are complete examples.
