The deck format
Updated ยท Markdown
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/catalogas JSON/api/v1/guideas a readable guide
This page explains the ideas.
The whole shape
{
"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.
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:
{ "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:
[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.
"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.
{ "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 glyphs and reaction names are stickers. They animate, using 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 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:
: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:
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:
@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 |
[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 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, with two additions:
/slides/@s_mood/notesfinds a slide by its id, wherever it is.mergesets several fields at once;nullremoves one.
{ "ops": [
{ "op": "merge", "path": "/slides/@s_mood/stage", "value": { "view": "bars" } },
{ "op": "add", "path": "/slides/-", "value": { "stage": { "type": "takeaway", "lines": ["Ask early"] } } }
] }