# The deck format

One presentation is one JSON document.
The editor, the API and your AI all read and write this same document.

The complete, always current list of every layout, question type and field
is generated by the server itself:

- [`/api/v1/catalog`](/api/v1/catalog) as JSON
- [`/api/v1/guide`](/api/v1/guide) as a readable guide

This page explains the ideas.

---

## The whole shape

```json
{
  "format": "stoa.deck/1",
  "title": "AI for Creative People",
  "aspect": "16:9",
  "theme": { "preset": "aurora", "accent": "#4FE3CE" },
  "settings": { "reactions": ["heart", "laugh", "wow", "mindblown", "fire"] },
  "sources": { "src_x1": { "kind": "pdf", "name": "talk.pdf", "pages": 18 } },
  "interactions": {
    "mood": {
      "type": "choice",
      "prompt": "How are you feeling?",
      "style": "stickers",
      "options": [
        { "id": "sleepy", "label": "Sleepy", "glyph": "sleepy" },
        { "id": "party", "label": "Party", "glyph": "party" }
      ]
    }
  },
  "slides": [
    { "id": "s_cover", "stage": { "type": "image", "src": "/a/u1/abc.webp" } },
    {
      "id": "s_mood",
      "stage": { "type": "results", "interaction": "mood", "view": "stickers" },
      "phone": { "type": "ask", "interaction": "mood" },
      "notes": "Watch the pit fill. Point at the biggest pile."
    }
  ]
}
```

Only what you chose is stored. Everything else has a default.

Two settings keep the room safe. `"filter": true` (the default, "Filter
bad words") filters a name, a word, a question or a custom screen's answer
with a swear word or slur: it waits until the presenter shows it, and a
filtered name shows as "Guest 12". Each open question, Q&A and custom
question has its own `"filter"` too, on by default: its answers are filtered
only while both are on. `"blocked": ["acme"]` ("Also filter these words")
filters anything starting with words of the deck's own, even with a filter
off. Photos and voice notes wait for approval by default (`"moderate": true`
on the question).

`"mouse": true` lets coded slides take the mouse on the big screen (off by
default: a click on the edge moves the show). A coded slide's own
`stage.mouse` says `true` or `false` for that slide.

---

## Slides

Each slide has:

| Field | What it is |
| --- | --- |
| `id` | Stable. Letters first. Made for you if missing. |
| `stage` | The big screen: a layout and its fields. |
| `phone` | What phones show. Missing means "look up". |
| `phoneByStep` | Change what phones show from a later beat: `{ "1": {...} }`. |
| `overlay` | Live results on top of any slide: `{ "interaction", "place", "view" }`. |
| `notes` | Speaker notes. Only the presenter sees them. |
| `title` | A label for the slide list. |
| `transition` | How this slide arrives, in place of the deck's: `{ "type": "zoom" }`, or a design of its own (see How slides change). |
| `skip` | Keep it, never show it. |

### Layouts (`stage.type`)

`title`, `statement`, `takeaway`, `bullets`, `quote`, `number`, `split`,
`image`, `qr`, `join`, `results`, `leaderboard`, `embed`, `countdown`,
`video`, `custom` (your own HTML, sandboxed, with the library kit), `blank`,
and `html` (a page of an imported coded deck: `source`, `index`, `steps`,
and `mouse` to take the mouse on the big screen or not, where the deck's
`settings.mouse` says otherwise).

A `custom` slide is your own HTML and script on the big screen: three.js,
GSAP, Motion and more from the kit, boxed in but free. It builds in beats
(`data-step`), draws the room's answers (`Stage.on('data')`, the big screen's
public roll-up, in the console too) and sends cues to the phones
(`Stage.broadcast`). It opens no new windows and saves no files, and neither
does a coded deck. See [Coded decks](CODED-DECKS.md).

Slides build one beat at a time where it makes sense: a `statement` reveals
one line per arrow press, a `bullets` list one item per press, a `video` one
part per press (it plays on to its next pause point).

A `video` can be one part of a longer file, so a deck exported as a video
can be a slide per part:

```json
{ "type": "video", "src": "/a/u/deck.mp4", "from": 4.8, "to": 12.1,
  "poster": "/a/u/part-1.webp", "thumb": "/a/u/part-2.webp" }
```

| Field | Meaning |
| --- | --- |
| `src` | The video file. |
| `stops` | Pause points, in seconds. Only those between `from` and `to` count. |
| `from`, `to` | The part of the file this slide plays, in seconds. Missing: the start, the end. |
| `poster` | A picture of how it starts, shown before it plays. |
| `thumb` | A picture of how it ends, for thumbnails and phones. |
| `muted`, `loop`, `fit` | No sound; loop the last part; `contain` or `cover`. |

The big screen keeps one player per file, so parts of the same file follow
each other without reloading.

Text can use two marks: a new line, and `*emphasis*`, which lights the word
in the theme's accent.

### Phone views (`phone.type`)

`follow`, `ask`, `card`, `mirror`, `results`, `custom`.

A `card` holds blocks, top to bottom: `text`, `image`, `button`, `social`, `file`.

A `custom` view is your own HTML, run in a sealed sandbox on every phone, with
the library kit. `Room.send(value)` in it saves an answer (any JSON, up to
4 KB) to its `interaction`, which is a question of type `custom`
(`"pick": { "type": "custom" }` in `interactions`). An answer with an
bad word in any string, keys too, is filtered: it waits for the presenter.
`Room.emit(name, data)` sends a live cue to the big screen,
and `Room.on('data', fn)` hears the room's answers when phones may see them.
`Room.me.name` is the person's own name, as the room shows it: empty until
they give one, and while the room keeps it hidden. A screen never hears
anybody else's.
`fill: true` uses the whole screen; `source` and `src` run a page from an
uploaded folder instead of inline HTML. Phone screens are sealed: they reach
only the kit, the deck's own files and a font service, and are never allowed
to ask for a password, a card number or a one-time code. No downloads, no
popups, no dialogs; people long-press a picture to save it.

### How slides change

`settings.transition` sets the deck's default move between slides; a slide's
own `transition` overrides it. Each names a transition, with an optional
duration in milliseconds: `{ "type": "zoom", "duration": 600 }`.

Built in: `fade` (the default), `none` (a cut), `slide` (a push), `rise`,
`zoom`, `blur`, `iris`, `wipe`, `curtain`, `flip`, `spin`, `drop`, `glide`,
`morph`. `push`, `dissolve`, `cut` and `magic-move` work as names too.

`morph` is magic move: the pieces two slides share (title with title, the
big number, the picture, line with line) glide from one place and size to
the other while the rest dissolves. To pair other pieces, give them the same
`view-transition-name` in the deck's CSS:

```css
[data-slide="s_intro"] .L-title, [data-slide="s_big"] .L-big { view-transition-name: hero; }
```

### Transitions of your own

`settings.transitions` holds designs of the deck's own, by name. Use a name
the way you use a built-in. A design is data, checked on every save: only
visual properties animate, nothing in it can load anything, and every time is
clamped, so a presenter's AI can design whatever it likes.

```json
"settings": {
  "transition": { "type": "hero-zoom" },
  "transitions": {
    "hero-zoom": {
      "label": "Hero zoom", "duration": 1300, "easing": "cubic-bezier(.7,0,.2,1)",
      "out": { "keyframes": [{ "opacity": 1, "transform": "scale(1)" },
                             { "opacity": 0, "transform": "scale(1.4)", "filter": "blur(14px)" }], "at": [0, 0.55] },
      "in":  { "keyframes": [{ "opacity": 0, "transform": "scale(.7) rotate(-3deg)" },
                             { "opacity": 1, "transform": "none" }], "at": [0.35, 1] },
      "parts": [
        { "select": "title", "from": { "opacity": 0, "transform": "translateY(8cqh)" }, "delay": 500, "duration": 800 },
        { "select": "items", "from": { "opacity": 0, "transform": "translateX(-4cqw)" }, "delay": 700, "stagger": 90 }
      ],
      "phone": { "from": { "opacity": 0, "transform": "scale(.9)" } }
    },
    "quick-zoom": { "base": "hero-zoom", "duration": 500 }
  }
}
```

| Field | Means |
| --- | --- |
| `label`, `help` | What the editor shows. |
| `base` | Start from a built-in or another of yours; whatever you list replaces its part. |
| `duration`, `easing` | Milliseconds (to 6000), and any CSS easing: `ease-out`, `cubic-bezier(...)`, `steps(...)`, `linear(...)`. |
| `out`, `in` | The leaving and the arriving slide: Web Animations keyframes, or `{ "keyframes", "at": [start, end], "easing", "style" }`. `at` is when, as parts of the duration. `style` holds for the move: a mask, a 3D origin. |
| `parts` | Pieces of the arriving slide, one after another: `select` a role (`title`, `eyebrow`, `sub`, `lines`, `items`, `number`, `quote`, `image`, `results`, `qr`, `tag`, `join`, `all`) or a CSS selector inside the slide, with `from`, `to` or `keyframes`, `delay`, `duration`, `stagger`, `easing`. |
| `morph` | `true`, or `{ "select": ["title", "image"], "duration": 900, "delay": 0 }`: magic move. |
| `back` | `{ "out", "in", "parts", "duration", "easing" }` for going back. Without it, going back plays the same transition backwards in time, so a push goes the other way and an iris closes. |
| `phone` | `{ "from", "duration", "easing" }`: how a phone's screen arrives on the slide. |

Keyframes animate opacity, transform, translate, rotate, scale, filter,
backdropFilter, clipPath, transformOrigin, colours, boxShadow, textShadow,
borderRadius, letterSpacing, wordSpacing, visibility, mixBlendMode, zIndex,
and five properties that animate smoothly for a layer's `style` to use:
`--t-r`, `--t-x`, `--t-y` (lengths), `--t-a` (an angle), `--t-n` (a number).
The built-in `iris` opens a mask with `--t-r`. Values are CSS text; `url()`,
braces and semicolons are refused.

Slides are see-through over one shared background, so a good design keeps the
two from sitting in the same place at full strength: stagger them with `at`,
or move them apart. People whose devices ask for less motion get a short
dissolve instead.

A slide can also carry a design written in place:
`"transition": { "base": "slide", "duration": 500, "parts": [...] }`.

### Questions that depend on who answers

**Only some people.** Any phone view can carry `when`: only people whose
answer to a choice question is one of `in` see it. Everybody else sees
`otherwise`, which can carry its own `when` (up to four deep), or looks up
at the screen without one. Each phone picks for itself, and the room checks
the same way before it keeps an answer, so nobody else can answer it.

```json
{ "type": "ask", "interaction": "fee",
  "when": { "interaction": "craft", "in": ["photo", "film"] },
  "otherwise": { "type": "ask", "interaction": "drink" } }
```

**In their own words.** `{craft}` in a question's prompt, a card's text or a
"look up" line puts in what that person answered to `craft`: an option's
label, a number with its unit, or a few words. `{craft|Friends}` reads
"Friends" on the big screen, where there is no one person, and for anybody
who has not answered yet. "{craft|Friends}, how was your year?" reads
"Photographer, how was your year?" on a photographer's phone.

**Their name.** `{name}` is always the person's own name: write
`{name|friend}`. People give a name when the deck asks (`settings.names`:
`"always"` asks when they join, `"quiz"` only for quizzes and Q&A), or by
answering a `text` question with `"asName": true`. That answer becomes
their name on the leaderboard, in Q&A and in `{name}`, and the question
can be shown on the big screen as a wall of names.

**Split by an answer.** A `results` slide with `"by": "craft"` shows its
question's answers split by what the same people answered to `craft`, one
group per option. A sticker poll becomes a ball pit per group (moods by
profession), another poll a split bar per group, a rating or a number an
average per group. Only people who answered both count. The room keeps
these results under `"mood@craft"`.

---

## Questions (`interactions`)

Questions live apart from slides, keyed by an id you choose.
That lets you ask on one slide and show the answers three slides later,
or ask the same thing twice.

| Type | Is | Shows as |
| --- | --- | --- |
| `choice` | Polls. A quiz when it has `correct`. Stickers when options have a `glyph`. | `bars`, `columns`, `donut`, `grid`, `stickers` |
| `scale` | Ratings, stars, sliders. | `average`, `bars` |
| `text` | Open answers and word clouds. | `wall`, `cloud` |
| `number` | Guesses and estimates. Closest wins when it has `correct`. | `average`, `histogram` |
| `photo` | The room's camera. Approved first by default. | `wall` |
| `audio` | Voice notes. Approved first by default. | `list` |
| `qa` | Questions from the room, voted up. | `top`, `wall` |
| `form` | Sign-ups, with a consent tick box. | `count` |
| `custom` | Anything a custom phone view sends. | `raw` |

Common fields: `prompt`, `hint`, `view`, `reveal` (`live` or `click`),
`phones` (`never`, `after`, `reveal`), `open` (`slide` or `always`).

Choice options can be written as plain strings. Ids are made for you:
`"options": ["Tea", "Coffee"]`.

---

## Stickers

Option `glyph`s and reaction names are stickers. They animate, using
[Noto Emoji Animation](https://googlefonts.github.io/noto-emoji-animation/)
by Google (CC BY 4.0), unless `theme.stickers` is `"drawn"`.
Any sticker can be replaced in one deck: `theme.glyphs.heart = "/a/..."`.

The names are in [`/api/v1/catalog`](/api/v1/catalog) under `glyphs`.

---

## Themes

`theme.preset` is `aurora` (the house style: carved stone and light),
`marble`, `ink` or `paper`.
Then override any token: `ground`, `surface`, `ink`, `dim`, `faint`,
`accent`, `accent2`, `accent3`, `gold`, `line`, `display`, `body`,
`aurora` (0 to 1), `radius`, `scheme` (`dark` or `light`).

## Styling anything

`theme.css` is your own CSS, and every CSS property works: sizes, spacing,
fonts, colours, borders, animations. It styles the big screen and the phones
completely, and only the slide previews in the editor and the console, so it
can never hide the controls you need to fix it.

`:root` means the deck itself. Variables set there win over the theme:

```css
:root { --accent: #FFB703; --display: "Playfair Display", serif; }
```

The deck itself is `.deck-scope`. On the big screen and on phones that is
the page's `body`: `body.stage-page` on the big screen, `body.phone` on
phones. So one rule can aim at one of them:

```css
body.stage-page .L-title { font-size: 9cqw; }
body.phone .q { font-size: 2rem; }
```

Fonts can come from a font service: Google Fonts, Bunny Fonts, Fontshare or
Adobe Fonts, over https. Put the `@import` at the top, write its address
plainly, and end it with `;`. A `;` inside the address is fine:

```css
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap");
```

Any other `@import` is ignored, because what it brings in would not stay
inside the deck.

The hooks that stay stable:

| On | Hook | Means |
| --- | --- | --- |
| Big screen | `[data-slide="s_id"]` | one slide |
| Big screen | `[data-layout="results"]` | every slide of a layout |
| Big screen | `[data-mode="stage"]` | only the big screen, not previews |
| Big screen | `.L-title`, `.L-sub`, `.L-line`, `.L-tk` | titles, lines, takeaways |
| Results | `.vrow`, `.vlabel`, `.vfill`, `.vnum` | an answer, its label, bar, count |
| Results | `.lead`, `.right`, `.wrong` | the leader; quiz answers after reveal |
| Phones | `#main[data-kind="ask"]` | while a question is asked |
| Phones | `#main[data-iid="mood"]` | one question |
| Phones | `.q`, `.sub` | the question and its hint |
| Phones | `.opts`, `.opt`, `.opt.on` | the options, one option, a picked one |
| Phones | `#top`, `#bar` | the header and the reaction bar |

```css
[data-kind="ask"] .q { font-size: 2.4rem; }
[data-kind="ask"] .opt { border-radius: 999px; padding: 1.2rem 1.4rem; }
.v-bars .vfill { background: linear-gradient(90deg, var(--accent), #FB8500); }
```

The full list, with every view and variable, is in the [AI guide](/api/v1/guide)
under Styling. Every save is versioned, so any change can be undone.

---

## Saving

- Problems that would break a slide are **errors**. Nothing is saved;
  you get each one with the exact path.
- Unfinished work (a question with no words yet) is a **warning**.
  It is saved and flagged, so typing is never blocked.
- Every save is a version. Any version can be restored.

## Editing with patches

[JSON Patch](https://www.rfc-editor.org/rfc/rfc6902), with two additions:

- `/slides/@s_mood/notes` finds a slide by its id, wherever it is.
- `merge` sets several fields at once; `null` removes one.

```json
{ "ops": [
  { "op": "merge", "path": "/slides/@s_mood/stage", "value": { "view": "bars" } },
  { "op": "add", "path": "/slides/-", "value": { "stage": { "type": "takeaway", "lines": ["Ask early"] } } }
] }
```
