<!--
  Generated by scripts/build-skill.mjs from skill/vdesign/. Do not edit.
  Use this where a skill directory cannot be loaded; it is the same content,
  flattened, and costs its full length on every turn.
-->

# Authoring vDesign documents

A vDesign document is a tree of boxes whose geometry is **declarative**. A property holds
either a number or an expression, and expressions are stored as written and resolved on
read. `w=(100% - $gap * 2)` stays that formula in the file forever; change `$gap` and
everything bound to it moves.

That is the entire point of the format, and it is what you are being asked for. A document
full of literal coordinates is a worse version of a Figma file. A document of expressions
and tokens can be edited by changing one number.

## The output contract

Emit **only DSL lines**. No prose before or after, no explanation, no JSON. A fenced block
is tolerated but unnecessary.

## The grammar in one screen

```vd
version 4
kind design
title "Launch hero"
$gap = 24
$accent = #5b5bd6
$paper = #ffffff
$highlight = #ffd166

font display = Inter
  fallback "system-ui, sans-serif"

frame page w=1280 h=720 background=paper
  rect hero.bg x=$gap y=24 w=(100% - $gap * 2) h=260 fill=accent radius=16
  text hero.title x=(@hero.bg.left + 32) y=64 w=560 h=82 size=34 weight=700 fill=paper
    "An open substrate for the "
    "agent era" fill=highlight
  group cards
    rect card.1 x=$gap y=(@hero.bg.bottom + $gap) w=((100% - $gap * 4) / 3) h=200
```

- **The first lines are the header.** `version 4` and `kind design`. Write both. `design`
  is the free-form canvas and is the only kind that exists today; a document that does not
  say which it is leaves every reader to guess from its contents.
- **Then `title "…"`** — a short human name for what you are making, two to five words
  ("Coffee shop landing", not "Document"). It is what the tab shows, so write it before the
  first page: the name should appear while the design is still arriving.
- **Two spaces of indent is one level.** Indentation is the only nesting.
- A line is `<type> <id> key=value key=value …`.
- A **quoted line** under a `text` node is one run of its content.
- A **boolean is a bare flag**: `italic`, never `italic=true`.
- **There are no comments.** `#` already starts a colour. Anything trailing a value is an
  error rather than something to ignore, so do not annotate the document.

### The one rule worth memorising

> A value is an **expression** if it starts with `$`, `@`, or `(`. Everything else is a
> literal.

So `x=$gap` and `x=@logo.right` need no parentheses, and everything else that is an
expression does: `y=(prev.bottom + 12)`, `w=(50%)`, `y=(parent.h / 2)`. A quoted value is
always a literal, which is how you write a string that starts with a sigil: `fill="$brand"`.

## Rules that decide whether the output is any good

**1. Write relationships, not coordinates.** This is the difference between a vDesign
document and a screenshot. Before writing a number, ask what it is derived from.

```text
✗ rect card.2 x=148 y=308 w=395 h=200
✓ rect card.2 x=(@card.1.right + $gap) y=(prev.y) w=@card.1.w h=200
```

**2. Put every repeated value in a token first.** A colour used twice, a spacing used
three times, a type scale — all tokens. `$gap` used eleven times is the compression, and it
is also the edit surface: "make it roomier" becomes one line.

**3. Only reference ids you have already written.** References resolve against the
document, and a consumer may be rendering your output as it streams. Write `logo` before
the headline that sits beside it.

**4. Give ids a role, not a number.** `hero.title`, `card.1`, `nav.logo` — an id should be
guessable by someone who has not read the file. Never `node1`, `rect5`.

**5. Text height is authored — estimate it.** Nothing measures glyphs for you. For a box
of width `w` at font size `size`:

```text
characters per line ≈ w / (size * 0.5)
lines               = ceil(length of the text / characters per line)
h                   = ceil(lines * size * 1.2)
```

Round up. Too small is reported as a `text-overflow` problem; too large just leaves space.

**6. `fill` takes a token name or a CSS colour, never an expression.** `fill=accent`,
`fill=#5b5bd6`. Sizes and positions bind; colours do not.

**7. A group has no geometry.** It holds children and nothing else — no `x`, `y`, `w`, `h`.
Its children keep the coordinate space of whatever contains the group, and `parent`, `prev`
and `next` see straight through it.

**8. A page is a `frame`, and more than one page is more than one frame.** Start the
document with a frame; you do not write a `canvas` line at all. Each frame is its own
coordinate space, so `x=$gap` means `$gap` from _that page's_ left edge and `100%` means
that page's width. `parent.cx` is that page's centre too, and `parent.left` is `0`, so the
same line centres something on every page. Lay several out left to right with a gap between
them — nothing stops one page being drawn over another, so do not overlap them.

```vd
$gap = 24

frame page w=1280 h=720 background=paper
  rect hero.bg x=$gap y=$gap w=(100% - $gap * 2) h=320 fill=accent

frame pricing x=1380 w=1280 h=720 background=paper
  text pricing.title x=$gap y=$gap w=600 h=48 size=34 weight=700
    "Pricing"
```

**9. Centre a label by giving it the box it sits in, never an offset.** A button's label,
a badge's number, a chip's text: same `x y w h` as the shape behind it, then
`align=center valign=middle`. The renderer centres the line inside the box, so it stays
centred at any size or font. A guessed `y=(@cta.top + 14)` is off by a few pixels and
drifts the moment the button or the type changes.

```vd
rect cta x=$gap y=400 w=180 h=48 fill=accent radius=10
text cta.label x=@cta.x y=@cta.y w=@cta.w h=@cta.h align=center valign=middle fill=paper
  "Get started"
```

## Editing an existing document

When given a document and asked to change it, reply with **statements**, not a new
document. Five verbs, plus `title` to rename the document:

```vd-patch
title "Pricing page"
set $gap = 40
set hero.title size=($display * 1.2) weight=800
set hero.sub content="A shorter subhead."
unset hero.sub opacity
insert rect card.4 after card.3 x=(@card.3.right + $gap) y=(prev.y) w=@card.1.w h=200
insert frame pricing x=(@page.right + 100) w=@page.w h=@page.h background=paper
move logo before hero.bg
delete hero.tagline
```

An `insert` with no placement lands on the canvas, which in a multi-page document is
_beside_ the pages rather than on one. Say `after <id>` or `in <frame>` whenever the new
node belongs to a page.

Copy is changed with `set … content=`, or with an indented body when the marks matter.
Never `delete` and `insert` a text node to reword it — it loses its place and every `@`
reference to it.

Prefer changing a **token** over changing the nodes that use it — that is usually what the
request actually means. "More breathing room" is `set $gap = 40`, not eleven `set`
statements.

See `reference/edits.md` for placement, and for what happens when you delete something
another node references.

Two flags come from the person more often than from you: `hidden` (not drawn, export
included) and `locked` (their pointer passes over it). Keep them as you find them. Hide with
`set card.2 hidden` only when asked to hide — when asked to remove something, `delete` it,
because a hidden node is still in the file for the next reader to wade through. `unset
card.2 hidden` shows it again.

A top-level frame is a slide when the document is played. For a backup or reference slide
that should stay in the deck but out of the talk, write `skip` on its frame (`set appendix
skip`) — don't hide it, which takes it off the canvas and out of every export too, and don't
delete it.

## Motion

Transitions between slides, parts of a slide appearing in order, and buttons that go to
another slide are a `motion` section after the last page. Only Play reads it: the canvas and
every export show each page as written, so write the page as it looks when finished and let
motion say how the presenter gets there.

```vd
motion intro enter=fade
  intro.title enter=rise
  intro.chart enter=fade step
  intro.cta enter=pop go=#pricing
```

Add it when the person is making something to present or click through, and keep it light.
Never copy a page to animate between the copies. A web page taller than a screen gets
`motion <page> scroll`, so Play scrolls it instead of shrinking it to fit. `reference/motion.md`
has the rest.

## Icons

Never write an icon's path from memory: it is slow, and it comes out roughly right at best.
Icons come from the Lucide set, about 1,900 line icons, by name.

- **Over MCP**, find one with `vd_search_icon` (`"settings"`, `"notification"`) and place it
  with `vd_add_icon`, which writes the `vector` for you — the icon's path, Lucide's stroke,
  round caps. Its `x`, `y` and `size` take expressions, so place an icon by relationship like
  anything else: `x="(@cta.right - 32)"`.
- **In the app**, find one with `search_icons` and get its line with `use_icon`, before you
  write any DSL; copy that `vector` line into your reply as it comes back.
- **Where no icon tool is offered**, draw it as a `vector` (`reference/nodes.md`).
- **Lucide has no brand logos** — GitHub, X, YouTube. A logo is an image the person supplies:
  leave it as a slot (see Images).

A placed icon is an ordinary vector named `lucide:<name>`. Recolour it with
`set <id> stroke=…`; resize it with `w` and `h`, and scale `strokeWidth` with them — 2 at 24.

## Images

A picture is an `image` node that names an asset: `image hero.logo x=40 y=32 w=160 h=48
src=logo fit=contain`. The asset's registry line (`asset logo = image/png hash=…`) and its
bytes are **never yours to write** — hashes come from the app. You only ever write `src=`.

- **In the app**, images the person attached are offered by name at the top of the request
  ("Images you can put in the design"). Place one with `src=<that name>`; an edit uses
  `insert image <id> in <frame> …`. Size it to the image's aspect ratio, and prefer
  `fit=contain` for a logo.
- **Over MCP**, a picture at a public URL goes in with `vd_add_asset` first, then you place
  it. `vd_assets` lists what a document already has.
- **A picture you cannot send** — one the person gave you in chat, or has on their machine —
  becomes a **slot**. You cannot pass on an image you can see, and no tool reads file paths,
  so do not try: write the image anyway, with a `src` nothing supplies yet and a `name`
  saying what goes there:

  ```vd
  image nav.logo x=40 y=24 w=120 h=40 src=logo fit=contain name="Your logo"
  ```

  The canvas shows it as a labelled box the person drops the file into, whenever they like.
  Size and place it as if the picture were there, carry on with the design, and tell them
  where it is ("drop your logo onto the box in the top-left"). Every image with the same
  `src` fills together. `vd_assets` lists the slots still waiting.

A `src` naming nothing reports `missing-asset`. That is what a slot is, so do not "fix" it by
deleting the image; do fix a `src` you simply misspelled.

## Looking at your work, and exporting

**Over MCP (desktop and CLI), `vd_export` is how you see what you made.** `vd_read` is text, and a layout that
reads fine can still look wrong — text overflowing, two cards overlapping. After a build or
a large edit, export the frame as `png` with `save: false` and look at the picture that comes
back; fix what you see, then look again.

When the person asks for a file — "export it as a PDF", "give me a PNG of the cover" — call
`vd_export` with `save: true`. Formats: `png`, `jpg`, `webp`, `svg`, `pdf`, `ai` (Illustrator:
one artboard of the whole canvas, or `artboards: "frames"` for a file per frame) and `html`.
In the app the person chooses where it goes, and may cancel; if they do, do not try again
unasked. Only save when asked — a look is `save: false`.

## Reference

Each file is named by its path in this skill. Over MCP, `vd_skill` returns any of them by that
path; in the flattened system prompt they follow below.

- `reference/grammar.md` — the expression language in full: edges, percentages, relative
  references, precedence; plus tokens, paint (colours and gradients), fonts and the asset
  registry.
- `reference/nodes.md` — every node type and every property it accepts: `frame`,
  `canvas`, `rect`, `ellipse`, `vector`, `image`, `text`, `group`.
- `reference/edits.md` — the mutation dialect in full.
- `reference/motion.md` — motion for Play: page transitions, reveals and buttons.
- `examples/` — complete documents: `hero.vd`, `cards.vd`, `slide.vd`, and `deck.vd`, three
  pages with motion.

---

## The expression language

Expressions are stored as written and resolved when the document is read. They form a
one-way graph: a value may read other values, and nothing may read back. A cycle is
reported as an error rather than solved.

### What an expression can contain

|                     |                                                                |
| ------------------- | -------------------------------------------------------------- |
| `12`, `-4`, `1.5`   | a number                                                       |
| `50%`               | a percentage of the containing box — see below                 |
| `$gap`              | a design token                                                 |
| `@hero.title.right` | another node's edge, by id                                     |
| `prev.bottom`       | a relative reference: `parent`, `prev`, `next`, `self`, `root` |
| `+ - * /`           | arithmetic, with the usual precedence                          |
| `( )`               | grouping                                                       |
| unary `-`           | `(-$gap)`                                                      |

There are **no functions** — no `min`, `max`, `clamp`, `abs`. If you need one, the value is
a literal or the layout needs rethinking.

There are **no units** either: a value is a number, a percentage, or an expression —
`h=480`, not `h=480px`; `w=(100%)`, not `w=100vw`. The one place a unit appears is inside a
quoted gradient, which is CSS spelling: `"linear-gradient(135deg, …)"`.

### Edges

Any reference names an edge of the box it points at:

| edge        | is                      |
| ----------- | ----------------------- |
| `x`, `left` | the left edge           |
| `y`, `top`  | the top edge            |
| `w`, `h`    | width, height           |
| `right`     | `x + w`                 |
| `bottom`    | `y + h`                 |
| `cx`, `cy`  | the centre on each axis |

```text
x=(@logo.right + $gap)          beside the logo
y=(@hero.title.bottom + 12)     under the headline
x=(parent.cx - self.w / 2)      centred horizontally
```

### Percentages

A bare percentage measures against the **containing box** — the nearest enclosing frame, or
the canvas when it has a size — on the axis the property lives on:

- `x` and `w` measure against the parent's width
- `y` and `h` measure against the parent's height
- anywhere else — `opacity=(50%)` — it is simply `0.5`

```text
w=(100% - $gap * 2)             full width inside a gutter
w=((100% - $gap * 4) / 3)       one of three columns with gaps between
```

A group is transparent here: a percentage inside a group measures the frame around the
group, not the group.

A percentage on a frame itself, or on anything sitting directly on an unsized canvas, has no
box to measure against and is reported as `percent-without-parent`.

### Relative references

`parent`, `prev`, `next`, `self` and `root` need no `@` and take an edge the same way.

```text
y=(prev.y)                      line up with the previous sibling
y=(prev.bottom + $gap)          stack under it
w=(parent.w / 2)                half the container
```

`prev` and `next` are siblings **in layout order**, which sees through groups: a group
carries no geometry, so its children behave as though the group were not there. A frame is
the opposite — it does carry geometry, so `parent` inside one is the frame.

Every reference is read in the coordinates of the node that wrote it. Inside a frame,
`parent.left` and `parent.top` are `0`, `parent.cx` is half the frame's width, and
`parent.right` is its width — wherever the frame sits on the canvas. A reference to a node
in another frame means the same point on the page, carried into this frame's coordinates.

### Which properties accept an expression

Positions, sizes and numeric style properties:

- `frame` — `x`, `y`, `w`, `h`
- `canvas` — `w`, `h`
- `rect` — `x`, `y`, `w`, `h`, `strokeWidth`, `radius`, `opacity`
- `text` — `x`, `y`, `w`, `h`, `size`, `weight`, `lineHeight`, `letterSpacing`, `opacity`

`fill`, `stroke`, `background` and `font` are **names, not numbers**. They take a token name
or a literal value; an expression there is reported as `expression-not-supported`.

Marks inside a text run — `weight=700` on one span — are always literals. There is no
address for "run 2's size", so there is nothing an expression there could resolve against.

### Writing values

| you mean                          | you write             |
| --------------------------------- | --------------------- |
| the number 24                     | `x=24`                |
| the token `gap`                   | `x=$gap`              |
| an expression                     | `x=(@a.right + $gap)` |
| the string `accent`               | `fill=accent`         |
| a string with a space             | `name="Hero heading"` |
| a string starting with `$` or `(` | `fill="$literal"`     |
| true                              | `italic`              |
| false                             | omit it               |

Quoting uses JSON's rules, so `\"` and `\n` mean what they do in JSON.

### Tokens

```vd
$gap = 24
$accent = #5b5bd6
$display = 34
$label = "Section heading"
```

A bare numeric token is a number; quote it to keep it a string. A token may not hold an
expression — it is a value, not a formula.

### Paint: colours and gradients

`fill`, `stroke`, a frame's `background` and a run's `fill` take a **paint**: a colour, a
token naming one, or a gradient in CSS spelling. Quote a gradient — it has spaces.

```vd
$accent = #2563EB
$glow = "linear-gradient(135deg, accent, #7C3AED 60%, transparent)"

rect hero.bg x=0 y=0 w=(100%) h=480 fill=glow
ellipse hero.orb x=600 y=40 w=240 h=240 fill="radial-gradient(circle at 30% 30%, #fff, accent)"
text hero.title x=64 y=120 w=560 h=64 size=48 weight=700 fill="linear-gradient(90deg, #F43F5E, #F59E0B)"
  "Ship faster"
```

- `linear-gradient(<angle>, <stops>)` — the angle in `deg` (0 points up, 90 right) or
  `to right`, `to bottom left`…; omitted, it runs top to bottom.
- `radial-gradient([circle|ellipse] [at X% Y%], <stops>)` — centred where asked, reaching
  the farthest corner.
- A stop is a colour or a token name, with an optional `%`. `transparent` fades the
  neighbouring colour out, as in CSS.
- A gradient spans the node's box; on text it sweeps across the whole box, not each run.
- Not supported: `conic-gradient`, `repeating-*`, sizes such as `closest-side`, and a token
  holding a gradient used as a stop of another gradient.

### Fonts

```vd
font display = Inter
  fallback "system-ui, -apple-system, sans-serif"
  face fonts/Inter-Regular.woff2 weight=400
  face fonts/Inter-Bold.woff2 weight=700 style=italic
```

`font=display` on a node or a run names the entry. A name with no entry passes straight
through as a CSS font stack, so `font="Georgia, serif"` works with no registry at all.

`fallback` is what draws while a face loads, or if it never does.

Each `face` is one font file, with the `weight` (a number) and `style` (`normal` or
`italic`) it draws.

### Assets

```vd
asset logo = image/png hash=sha256-9f2c4e8a… bytes=24567
asset hero = image/jpeg hash=sha256-1a77b0c3… bytes=412330 credit="Jane Doe / Unsplash" source="https://unsplash.com/photos/abc" license=Unsplash
```

One asset, one line, including its provenance. `hash` and `bytes` are required:
they identify the block of bytes further down the file that carries the picture.
`credit`, `source` and `license` are optional in the grammar but **not optional
for an image you found** — the bytes end up inside a file someone re-shares.

A `.vd` file is a **text head and a payload tail**, separated by a line reading
`--- payload base64 ---`. Everything above it is the design, and is all you ever
read or write. Everything below it is one line per asset: its hash, a space, and
its bytes as base64. You never write that half by hand, and a tool reading a
document for you will not show it to you.

This is why the registry declares `bytes` and a media type up front: a reader
knows what a document depends on, and how large each of those things is, before
any of the bytes arrive.

---

## Node types

Eight. Every line is `<type> <id> key=value …`, and children are indented two spaces under
their container.

Every node also accepts `name="…"`, a human label with no effect on layout.

Every node but the canvas also accepts two bare flags, written last on the line:

- `hidden` — not drawn anywhere, export included. It still resolves, so `@badge.right`
  keeps working while `badge` is hidden. Hiding a container hides what is in it.
- `locked` — the person's pointer passes over it on the canvas. It changes nothing for you:
  you can still edit it, and you may add or remove the flag when asked.

### `frame` — a page

The node you almost always start with. A frame is a box with **its own coordinate space**:
everything inside it is positioned from its top-left corner, and `%` inside it measures the
frame rather than the document.

A document may hold as many as you like, side by side — that is how it holds several pages,
slides or artboards. Frames also nest, which is how you get a card with a boundary of its
own.

| property     | type                 |                                      |
| ------------ | -------------------- | ------------------------------------ |
| `x`, `y`     | number or expression | where the page sits; both default 0  |
| `w`, `h`     | number or expression | the page size                        |
| `background` | token or colour      | drawn behind everything on this page |
| `clip`       | flag                 | hide what overflows; off by default  |
| `skip`       | flag                 | left out of Play; top-level only     |

```vd
$gap = 24

frame page w=1280 h=720 background=paper
  rect hero.bg x=$gap y=$gap w=(100% - $gap * 2) h=320 fill=accent

frame notes x=1380 w=1280 h=720 background=paper
  text agenda x=$gap y=$gap w=600 h=48
    "What we agreed"
```

Lay several pages out **left to right with a gap between them**, as above: they must not
overlap, because nothing stops one page being drawn on top of another.

### `canvas` — the surface

The surface the frames sit on, and the outermost node. **You almost never write it.** A
document that begins with frames gets one automatically, which is why the example above has
no `canvas` line and no extra indent level.

Write it only for a single-page document with shapes directly on it, where it is the page:

```vd
$gap = 24

canvas page w=1280 h=720 background=paper
  rect hero.bg x=$gap y=24 w=(100% - $gap * 2) h=260 fill=accent
```

| property     | type                 |                                    |
| ------------ | -------------------- | ---------------------------------- |
| `w`, `h`     | number or expression | optional; absent means unbounded   |
| `background` | token or colour      | only meaningful when it has a size |

A canvas must be the **only** top-level node. Writing one and then a frame beside it is an
error; put the frames inside it, or leave it out entirely.

### `rect` — a box

| property      | type                 | default        |
| ------------- | -------------------- | -------------- |
| `x`, `y`      | number or expression | required       |
| `w`, `h`      | number or expression | required       |
| `fill`        | token or colour      | `#CBD5E1`      |
| `stroke`      | token or colour      | none           |
| `strokeWidth` | number or expression | 1 when stroked |
| `radius`      | number or expression | 0              |
| `opacity`     | number or expression | 1              |

```vd
rect hero.bg x=$gap y=24 w=(100% - $gap * 2) h=260 fill=accent radius=16
```

### `ellipse` — an oval, or a circle

The ellipse inscribed in its box. A circle is `w` equal to `h`.

| property      | type                 | default   |
| ------------- | -------------------- | --------- |
| `x`, `y`      | number or expression | required  |
| `w`, `h`      | number or expression | required  |
| `fill`        | token or colour      | `#CBD5E1` |
| `stroke`      | token or colour      | none      |
| `strokeWidth` | number or expression | 0         |
| `opacity`     | number or expression | 1         |

```vd
ellipse avatar.ring x=24 y=24 w=48 h=48 fill=none stroke=accent strokeWidth=2
```

### `vector` — any shape: icons, arrows, dividers, logos

An SVG path, drawn into a box.

| property      | type                               | default   |
| ------------- | ---------------------------------- | --------- |
| `x`, `y`      | number or expression               | required  |
| `w`, `h`      | number or expression               | required  |
| `d`           | SVG path data, quoted              | required  |
| `view`        | `"minX minY width height"`, quoted | `0 0 w h` |
| `fill`        | token, colour or `none`            | `#CBD5E1` |
| `stroke`      | token or colour                    | none      |
| `strokeWidth` | number or expression               | 0         |
| `opacity`     | number or expression               | 1         |
| `rule`        | `nonzero`, `evenodd`               | `nonzero` |
| `cap`         | `butt`, `round`, `square`          | `butt`    |
| `join`        | `miter`, `round`, `bevel`          | `miter`   |

```vd
vector nav.arrow x=320 y=20 w=24 h=24 view="0 0 24 24" d="M5 12h14M12 5l7 7-7 7" fill=none stroke=ink strokeWidth=2 cap=round join=round
```

`d` is exactly SVG's path syntax — `M L H V C S Q T A Z`, absolute or relative. `view`
is the path's own coordinate space, spelled the way SVG spells a `viewBox`, and it is
**stretched onto `x y w h`**: an icon drawn on a 24-unit grid keeps its numbers and is
sized by `w` and `h` alone.

- **Over MCP, place an icon with `vd_add_icon`** rather than writing one: it knows the
  Lucide set and writes this node for you. Otherwise, **copy the icon's `d` and its
  `viewBox` verbatim** into `d` and `view`.
- The stroke is in page units and does not grow with `view`: an icon drawn with
  `strokeWidth=2` at 24 wants 4 at 48. A dot drawn as a tiny circle needs a stroke at
  least its diameter to read as solid.
  Do not rescale the numbers; change `w` and `h`.
- A line icon wants `fill=none` and a `stroke`. Without `fill=none` the shape is filled
  grey by default.
- Without `view`, the path draws at 1:1 inside its box: `vector rule x=0 y=100 w=400 h=0
d="M0 0 H400" stroke=ink strokeWidth=1` is a divider.
- A path that cannot be read draws up to its first error and reports `bad-path`.

To turn a `rect` or `ellipse` into a free-form shape, replace its line with a `vector` of
the same id — same `x y w h`, `view="0 0 <w> <h>"`, and a `d` that traces it. The person
can do the same with **Convert to vector** (⇧⌘O) and then drag its points.

### `image` — a picture

| property  | type                       | default  |
| --------- | -------------------------- | -------- |
| `x`, `y`  | number or expression       | required |
| `w`, `h`  | number or expression       | required |
| `src`     | a name from `asset`        | required |
| `fit`     | `cover`, `contain`, `fill` | `fill`   |
| `radius`  | number or expression       | 0        |
| `opacity` | number or expression       | 1        |

```vd
asset hero = image/jpeg hash=sha256-1a77b0c3… bytes=412330

image cover.photo x=0 y=0 w=(100%) h=480 src=hero fit=cover
```

`src` names an entry in the document's asset registry — **never a path or a
URL**. `cover` fills the box and crops; `contain` fits the whole picture inside
it; `fill` stretches.

**You cannot invent an asset.** A registry line needs the content hash and byte
count of bytes that actually exist in the file, so `asset` lines are written by
whatever put the bytes there — a paste, an import, or an `add-image` tool. What
you can do is refer to one that is already declared, move it, resize it, or
change its `fit`. Referring to a name with no `asset` line reports
`missing-asset` and, in the editor, draws a **slot** — a box labelled with the
node's `name` that the person drops a picture into. That is how you leave room for
a picture you cannot send; see "Images" in the overview.

### `text` — a box of text

| property           | type                              | default                                    |
| ------------------ | --------------------------------- | ------------------------------------------ |
| `x`, `y`, `w`, `h` | number or expression              | required                                   |
| `font`             | registry name or CSS stack        | `system-ui, …`                             |
| `size`             | number or expression              | 16                                         |
| `weight`           | number or expression              | 400                                        |
| `lineHeight`       | number or expression              | 1.2 — a **multiple of size**, not a length |
| `letterSpacing`    | number or expression              | 0                                          |
| `align`            | `left` `center` `right` `justify` | `left`                                     |
| `valign`           | `top` `middle` `bottom`           | `top`                                      |
| `italic`           | bare flag                         | off                                        |
| `fill`             | token or colour                   | `#0F172A`                                  |
| `opacity`          | number or expression              | 1                                          |

**`h` is authored.** Nothing measures the glyphs for you, and text that does not fit is
drawn overflowing and reported as `text-overflow`. Estimate:

```text
characters per line ≈ w / (size * 0.5)
lines               = ceil(length / characters per line)
h                   = ceil(lines * size * lineHeight)
```

#### Content

One quoted line per run, indented under the node:

```vd
text hero.title x=64 y=64 w=560 h=44 size=34 weight=700
  "An open substrate for the "
  "agent era" fill=highlight
```

A single unformatted run is just one quoted line. Split into more runs only where the
formatting actually changes — and mind the spaces, which belong to whichever run they
follow.

A run may carry `font`, `size`, `weight`, `italic`, `underline`, `strike`, `fill`,
`letterSpacing`, `link`. All literals: a run is not addressable, so nothing there can be an
expression.

`link` makes the run a link: `link="https://…"`, `link="mailto:…"`, or `link="#pricing"` to
jump to the frame `pricing` in the same document. Nothing else is followed — `http:`,
`javascript:` and file paths are refused and drawn as plain text. Only link to a URL the
person gave you; never invent one. A link is not underlined for you, so add `underline`
when it should look like one:

```vd
text cta.body x=64 y=480 w=400 h=24
  "Read the "
  "docs" link="https://example.com/docs" underline
  " or see "
  "pricing" link="#pricing" underline
```

### `group` — structure, and nothing else

```vd
group cards
  rect card.1 x=$gap y=0 w=200 h=120
  rect card.2 x=(@card.1.right + $gap) y=(prev.y) w=@card.1.w h=120
```

A group has **no `x`, `y`, `w` or `h`**, and it is not a mistake that it does not. A frame
derived from its children would close a loop — a child's `w=(100%)` would read the group's
width, which would read the child's — so a group is transparent to layout. Its children keep
the coordinate space of whatever contains the group, and `parent`, `prev`, `next` and `%`
see straight through it.

Group things because they belong together, and because grouping breaks no expression.

### Rotation

`rect`, `ellipse`, `vector`, `image` and `text` take `rotation=` — degrees, clockwise,
about the centre of the box. `x y w h` stay the **unrotated** box, so `@card.right` still
means the right edge of the box as written; a rotated node is placed by its box and then
turned. Frames and groups do not rotate. `rotation` is a number, so `rotation=$tilt` works.

```vd
rect sticker x=520 y=60 w=160 h=48 radius=24 fill=accent rotation=-8
text sticker.label x=520 y=72 w=160 h=24 align=center fill=#fff rotation=-8
  "New"
```

Give things that should turn together the same box centre, or the same rotation and
matching boxes, as above.

### Effects: blend and shadow

`frame`, `rect`, `ellipse`, `vector`, `image` and `text` also take:

- `blend=` — how the node mixes with what is under it: `multiply`, `screen`, `overlay`,
  `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`,
  `difference`, `exclusion`, `hue`, `saturation`, `color`, `luminosity`. Omit it for
  normal. A node blends only with the document beneath it. Groups take no blend.
- `shadow=` — drop shadows in CSS `box-shadow` spelling, quoted: `x y blur spread colour`,
  several separated by commas, the first on top. The colour may be a token. `inset` is not
  supported. A shadow follows the shape — round for an ellipse, glyph-shaped for text — and
  never changes the box, so layout and `@` references ignore it. Put a shadow you reuse in
  a token.

```vd
$lift = "0 1 2 0 #0F172A14, 0 8 24 -4 #0F172A29"

image hero.photo x=0 y=0 w=(100%) h=480 src=city fit=cover
rect hero.tint x=0 y=0 w=(100%) h=480 fill=accent blend=multiply opacity=0.8
rect card x=64 y=520 w=320 h=200 radius=16 fill=#FFFFFF shadow=lift
```

### Ids

An id is how everything else refers to a node, so make it guessable: `hero.title`,
`card.1`, `nav.logo`, `footer.legal`. Dots are ordinary characters and read as structure.

Ids must be unique across the document.

---

## Editing a document

Given a document and a request to change it, reply with **statements**, not a new document.
An edit is usually a handful of lines against hundreds, and each statement is applied as an
operation — so it is undoable as one step and nothing else in the document is disturbed.

Statements are never indented, except for the body of an `insert`.

### `set`

```vd-patch
set $gap = 40                              a token
set hero.title size=34 weight=700          one or more properties
set card.2 w=(@card.1.w * 2)               an expression, same rules as anywhere
```

**Reach for the token first.** If four cards are laid out with `$gap`, "more breathing
room" is `set $gap = 40` — one line, and the relationships survive. Writing four `set`
statements with new coordinates destroys exactly what the document was for.

#### Text

`content` is the one thing a node line does not carry, so `set` reaches it two ways:

```vd-patch
set hero.title content="A new headline"
```

```vd-patch
set hero.title
  "Open tooling for the "
  "agent era" fill=highlight
```

The first is for plain copy and is what you want almost always. The indented body is for
when the runs and their marks matter — the same shape a `text` node declares in a document.
Use one or the other, never both in a statement.

Rewording is always `set`. Never `delete` and `insert` to change copy: the node loses its
place in the document and every `@` reference to it breaks.

### `unset`

Removes a property so its default comes back.

```vd-patch
unset hero.sub opacity
unset card.1 stroke strokeWidth
```

For a token, use `delete $name` — a token is a thing, not a property.

### `insert`

```vd-patch
insert rect card.4 after card.3 x=(@card.3.right + $gap) y=(prev.y) w=@card.1.w h=200
insert text caption before footer x=$gap y=0 w=400 h=20
insert group cards in page
  rect inner x=0 y=0 w=10 h=10
  text label x=0 y=12 w=40 h=10
    "A caption"
```

Placement comes before the properties:

|                |                                                                                                                             |
| -------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `after <id>`   | next to that node, following it                                                                                             |
| `before <id>`  | next to that node, ahead of it                                                                                              |
| `in <id>`      | appended inside a frame, canvas or group                                                                                    |
| `in <id> at 0` | at a specific position inside it                                                                                            |
| _omitted_      | appended to the canvas — **name the frame** in a multi-page document, or the node lands beside the pages rather than on one |

Children and text runs go indented underneath, exactly as in a document.

### `move`

Takes the same placements.

```vd-patch
move logo before hero.bg
move card.3 in cards at 0
```

### `delete`

```vd-patch
delete hero.tagline
delete $unused
```

### `title`

Renames the document. It is the same line a whole document opens with.

```vd-patch
title "Pricing page"
```

### `motion`

Replaces one page's whole motion block with the lines under it; a bare `motion <page>`
clears it. The format is in the motion reference.

```vd-patch
motion intro enter=fade
  intro.title enter=rise
  intro.chart enter=fade step
```

### Things worth knowing

**Statements apply in order, against a document that grows as they do.** So this works:

```vd-patch
insert rect a after card.2 x=0 y=0 w=10 h=10
insert rect b after a x=0 y=0 w=10 h=10
```

**A bad statement fails alone.** The rest of the patch still lands, and the failure comes
back naming the line. Fix that line; do not resend the whole patch.

**Deleting leaves dangling references.** Nothing rewrites expressions for you, so removing
`logo` while `hero.title` reads `x=(@logo.right + $gap)` produces an `unknown-reference`
error on the next read. Before deleting, check what points at it, and either repoint those
first or delete them too:

```vd-patch
set hero.title x=$gap
delete logo
```

**Moving a node changes what `prev` and `next` mean** for its neighbours. If siblings are
chained with `y=(prev.y)`, reordering them is fine — that is the chain doing its job — but
a `prev` reference on the node that used to be first now points somewhere new.

**You cannot delete or move the canvas**, and you cannot give a group geometry. A frame is
an ordinary node: insert, move and delete it like any other, and everything on it travels
with it.

---

## Motion

Motion is how Play gets to a page, never what the page is. Write every page as it looks when
finished; motion says how a presenter arrives there. Only Play reads it: the canvas always
shows the finished page, and no export carries motion — a PDF prints each page as drawn.

Add motion when the person is making something to present or to click through, and keep it
light. A transition per page and a reveal or two is a talk; everything flying in is noise.

### Where it goes

After the last page, one block per page that has any:

```vd
motion intro enter=fade
  intro.title enter=rise
  intro.chart enter=fade step
  intro.note enter=fade together
  intro.cta enter=pop go=#pricing

motion pricing enter=push duration=600
  pricing.back go=back
```

A header names a page — a top-level frame — and says how Play arrives on it. Each line under
it names a node on that page and says what the node does. Never copy a page to animate
between the copies: each page is written once.

### The header

`motion <page> [enter=…] [duration=…] [scroll]`

| `enter` | Play arrives on the page by…                     |
| ------- | ------------------------------------------------ |
| `cut`   | nothing at all — the default                     |
| `fade`  | fading in over the page before                   |
| `push`  | pushing the page before out, sideways            |
| `slide` | sliding in over the page before, which stays put |

You never write a direction. Going forward runs the new page's `enter`; going back runs the
leaving page's `enter` in reverse.

#### `scroll`: a page taller than a screen

Play fits each page whole into the window, which shrinks a long web page to a thin strip.
`scroll` fits the page to the window's width instead, and the presenter scrolls it — wheel,
trackpad, Space, Page Down, ↓ — the way a browser shows a page. At the bottom the same key goes
on to the next page; → still goes there at once. A header with only `scroll` is a whole block:

```vd
motion home scroll
```

Give it to a web page, a landing page or a long article taller than one screen. Leave it off
slides, posters and phone screens, which are meant to be seen whole. A `go=#pricing` to a frame
inside a page that scrolls lands with that frame at the top of the window. The canvas and every
export are unchanged: they show the whole page.

### A line

`<node> [enter=…|exit=…] [duration=…] [delay=…] [step|together] [go=…]`

Effects, for `enter` and for `exit`:

| effect | entering                       | leaving          |
| ------ | ------------------------------ | ---------------- |
| `cut`  | appears at once                | vanishes at once |
| `fade` | fades in                       | fades out        |
| `rise` | rises from a little below      | rises away       |
| `drop` | drops from a little above      | drops away       |
| `pop`  | grows in from slightly smaller | shrinks away     |

A node that `enter`s is hidden when Play arrives on its page, until its turn; one that
`exit`s is gone after its turn. A node may have one `enter` line and one `exit` line — two
lines, never both on one.

**Order.** Lines build in the order written, each starting when the one above finishes,
unless it says:

- `together` — start with the line above;
- `step` — wait for the presenter to press → or click. What follows runs by itself up to the
  next `step`.

`delay=300` waits that long after the line would otherwise start.

**Time is milliseconds, as a plain number**: `duration=600`, never `600ms` or `0.6s`. The
default is 400 and the most is 10000.

### Buttons: `go=`

Any line can say where a click on its node goes in Play. A click anywhere inside the node
counts, so put `go=` on the group or frame that _is_ the button, not on its label.

- `go=#pricing` — that page. A frame inside a page goes to its page.
- `go=back` — wherever the presenter came from.
- `go=next` — the next page, whatever is left to reveal on this one.
- `go=https://…` or `go=mailto:…` — only a link the person gave you.

A button can animate as well: `intro.cta enter=pop go=#pricing`. A `go=` works only in Play.
A link that should also work in an exported PDF is a text run's `link=` (see nodes).

### Editing motion

A patch replaces a page's whole block: write `motion <page>`, its header (`scroll` included),
and every line as it should end up. A bare `motion <page>` clears it.

```vd-patch
motion intro enter=fade
  intro.title enter=rise
  intro.chart enter=fade step
```

Deleting a node removes its lines, and moving it to another page takes them along. The
`vd_read` outline ends with the pages that have motion; `mode: "full"` shows the lines.

### What is checked

An **error** refuses the write: an effect that does not exist, a unit on a time, `enter` and
`exit` on one line, `step` with `together`, a line that says nothing, a second `enter`, `exit`
or `go` for one node, or a line naming the page itself. In a patch, naming a node that is not
on the page is an error too.

A **warning** keeps the line, and Play skips it: in a whole document, a node that is not
there, a node on another page, a header that is not a page, or a `go=#` to no frame.

---

# Examples

## cards.vd

```vd
version 4
kind design
title "Three feature cards"
$gap = 24
$paper = #ffffff
$muted = #eef2f7
$ink = #0f172a
$heading = 20
$body = 14

frame page w=1040 h=520 background=paper
  text section.title x=$gap y=$gap w=400 h=24 size=$heading weight=700 fill=ink
    "Three ways in"
  group cards
    rect card.1 x=$gap y=(@section.title.bottom + $gap) w=((100% - $gap * 4) / 3) h=220 fill=muted radius=12
    rect card.2 x=(@card.1.right + $gap) y=(prev.y) w=@card.1.w h=@card.1.h fill=muted radius=12
    rect card.3 x=(@card.2.right + $gap) y=(prev.y) w=@card.1.w h=@card.1.h fill=muted radius=12
  text card.1.label x=(@card.1.left + 16) y=(@card.1.top + 16) w=(@card.1.w - 32) h=17 size=$body weight=600 fill=ink
    "As a format"
  text card.2.label x=(@card.2.left + 16) y=(@card.1.label.top) w=(@card.1.label.w) h=17 size=$body weight=600 fill=ink
    "As an editor"
  text card.3.label x=(@card.3.left + 16) y=(@card.1.label.top) w=(@card.1.label.w) h=17 size=$body weight=600 fill=ink
    "As a protocol"
```

## deck.vd

```vd
version 4
kind design
title "Launch plan"
$ink = #101014
$paper = #fafafa
$muted = #ececf1
$accent = #d64545
$gap = 64

frame intro x=0 y=0 w=1600 h=900 background=paper
  text intro.title x=$gap y=240 w=1100 h=116 size=96 weight=800 fill=ink
    "Launch plan"
  text intro.sub x=$gap y=(@intro.title.bottom + 24) w=900 h=39 size=32 fill=ink opacity=0.6
    "What ships, when, and what it costs"
  group intro.cta
    rect intro.cta.bg x=$gap y=620 w=280 h=72 fill=accent radius=36
    text intro.cta.label x=@intro.cta.bg.x y=@intro.cta.bg.y w=@intro.cta.bg.w h=@intro.cta.bg.h size=24 weight=700 align=center valign=middle fill=paper
      "See pricing"

frame plan x=1700 y=0 w=1600 h=900 background=paper
  text plan.title x=$gap y=$gap w=1000 h=58 size=48 weight=800 fill=ink
    "Three steps"
  rect plan.one x=$gap y=240 w=((100% - $gap * 4) / 3) h=480 fill=muted radius=24
  rect plan.two x=(@plan.one.right + $gap) y=(prev.y) w=@plan.one.w h=@plan.one.h fill=muted radius=24
  rect plan.three x=(@plan.two.right + $gap) y=(prev.y) w=@plan.one.w h=@plan.one.h fill=muted radius=24

frame pricing x=3400 y=0 w=1600 h=900 background=ink
  text pricing.title x=$gap y=$gap w=1000 h=58 size=48 weight=800 fill=paper
    "Pricing"
  text pricing.price x=$gap y=300 w=800 h=154 size=128 weight=800 fill=accent
    "$49 a month"
  text pricing.back x=$gap y=780 w=200 h=34 size=28 fill=paper
    "← Back"

motion intro enter=fade
  intro.title enter=rise
  intro.sub enter=fade delay=150 together
  intro.cta enter=pop go=#pricing

motion plan enter=push
  plan.title enter=fade
  plan.one enter=rise step
  plan.two enter=rise step
  plan.three enter=rise step

motion pricing enter=slide
  pricing.price enter=pop
  pricing.back go=back
```

## hero.vd

```vd
version 4
kind design
title "Launch hero"
$gap = 24
$paper = #ffffff
$accent = #5b5bd6
$highlight = #ffd166
$display = 40
$body = 16

font display = Inter
  fallback "system-ui, -apple-system, Segoe UI, sans-serif"

frame page w=1280 h=720 background=paper
  rect hero.bg x=$gap y=$gap w=(100% - $gap * 2) h=320 fill=accent radius=20
  rect logo x=(@hero.bg.left + 40) y=(@hero.bg.top + 40) w=72 h=72 fill=paper radius=16
  text hero.title x=(@logo.right + $gap) y=@logo.top w=680 h=96 font=display size=$display weight=700 fill=paper
    "An open document substrate for the "
    "agent era" fill=highlight
  text hero.sub x=@hero.title.left y=(@hero.title.bottom + 12) w=560 h=20 font=display size=$body fill=paper opacity=0.8
    "The document is the artifact, not the code that made it."
  rect cta x=@hero.title.left y=(@hero.sub.bottom + $gap) w=180 h=48 fill=paper radius=10
  text cta.label x=@cta.x y=@cta.y w=@cta.w h=@cta.h font=display size=$body weight=600 align=center valign=middle fill=accent
    "Read the spec"
```

## slide.vd

```vd
version 4
kind design
title "Title slide"
$ink = #101014
$paper = #fafafa
$accent = #d64545
$title = 56
$sub = 22

frame slide w=1600 h=900 background=ink
  text slide.title x=(parent.cx - self.w / 2) y=300 w=1100 h=68 size=$title weight=700 align=center fill=paper
    "The document is the artifact"
  rect rule x=(parent.cx - self.w / 2) y=(@slide.title.bottom + 40) w=120 h=4 fill=accent radius=2
  text slide.sub x=(parent.cx - self.w / 2) y=(@rule.bottom + 40) w=900 h=27 size=$sub align=center fill=paper opacity=0.7
    "vDesign — an open substrate for the agent era"
```
