> ## Documentation Index
> Fetch the complete documentation index at: https://docs.decktalk.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Write a page by hand

> Implement the DeckTalk page contract without decktalk-runtime.js, so a page built with any framework honors scene, beats, t0=signal, __sceneReady, and window.__decktalk.

The runtime is one file with no dependencies, and most pages should include it. A page
built with a framework of its own can implement the contract instead. This page lists
what the recorder and the screenshot tool need, and shows a small page that honors all
of it.

<Warning>The recorder opens a page with `t0=signal` and starts the narration clock by
calling `window.DeckTalk.startClock()`. A page that starts its own clock at load fires
every cue early by the settle time and the recorder's lead, and the video's reveals land
before their words. Wait for the call.</Warning>

## What the recorder needs

| The recorder                                                                         | Your page                                                                                    |
| ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| Opens `page.html?scene=N&beats=id@s,…&t0=signal` plus any `params` from the section. | Reads `scene` and `beats`, and schedules cue `id` at `s` seconds after the clock starts.     |
| Awaits `document.fonts.ready` and then `window.__sceneReady` when it is a Promise.   | Sets `window.__sceneReady` to a Promise that resolves when the page is ready to be recorded. |
| Calls `window.DeckTalk.startClock()` once, when narration t=0 falls.                 | Starts the clock in that call and nowhere else.                                              |
| Reads `window.__decktalk.warnings` after the recording and logs each string.         | Pushes a string for anything the page could not honor, such as an unknown cue id.            |
| Records a viewport of `[video]` width by height.                                     | Draws at 1920 by 1080, or scales a stage of that size to fit.                                |

The magenta cover that hides the page until t=0 is the recorder's, so a page needs no
cover of its own. [How it works](/concepts/how-it-works#why-the-cuts-are-exact) explains
what the cover is for.

## What the screenshot tool needs

`decktalk shots` opens the page with no query and reads `window.__decktalk.catalog`, a
list of `{ scene, name, steps }` objects, and then opens `?step=ID` for each step id. A
page that has no catalog is skipped with a warning. Frame mode, `shots --section N --at S`,
opens the page as the recorder does and reads `window.__decktalk.fired` for the log.

## A minimal page

This page honors the whole contract in about forty lines. It has one scene with one
step, and each cue reveals the element with the matching `data-cue`.

```html deck/plain.html theme={null}
<!doctype html>
<meta charset="utf-8">
<style>
  html, body { margin: 0; background: #0b0f14; color: #f4f6f8; font: 44px/1.3 Inter, sans-serif; }
  #stage { position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; padding: 100px 160px; box-sizing: border-box; }
  [data-cue] { opacity: 0; transition: opacity .35s; }
  [data-cue].on { opacity: 1; }
</style>
<div id="stage">
  <h1 data-cue="1.1a">A page by hand</h1>
  <p data-cue="1.1b">It honors the contract without the runtime.</p>
</div>
<script>
  const params = new URLSearchParams(location.search);
  const state = { mode: "index", scene: params.get("scene"), step: "1.1", cues: [], fired: [], warnings: [], origin: null,
                  catalog: [{ scene: "1", name: "By hand", steps: ["1.1"] }], now: () => state.origin === null ? -Infinity : (performance.now() - state.origin) / 1000 };
  window.__decktalk = state;
  window.__sceneReady = document.fonts.ready;

  const cues = (params.get("beats") || "").split(",").filter(Boolean).map((tok) => {
    const i = tok.lastIndexOf("@");
    return { id: tok.slice(0, i), t: parseFloat(tok.slice(i + 1)) };
  });
  for (const c of cues) if (!document.querySelector(`[data-cue="${c.id}"]`)) state.warnings.push(`unknown cue id ${c.id}`);

  function fire(id) {
    state.fired.push(id);
    document.querySelectorAll(`[data-cue="${id}"]`).forEach((el) => el.classList.add("on"));
  }
  function startClock() {
    if (state.origin !== null) return;
    state.origin = performance.now();
    state.mode = "cues";
    for (const c of cues) setTimeout(() => fire(c.id), c.t * 1000);
  }
  window.DeckTalk = { startClock };

  if (params.has("step")) { state.mode = "frozen"; document.querySelectorAll("[data-cue]").forEach((el) => el.classList.add("on")); }
  else if (params.get("t0") !== "signal") startClock();   // a browser preview starts at once
</script>
```

Point a section at it with `page = "deck/plain.html"` and `scene = 1`, and cue `1.1a`
and `1.1b` in `cues.json`. The page has no autoplay timing, so a browser preview with
`?scene=1` fires nothing until beats are given, and `?step=1.1` shows everything.

## What the runtime adds

The runtime also gives a page an index of scenes and steps, autoplay from `hold` and
`data-at`, the reveal effects, count-ups, typewriting, KaTeX typesetting with a wait for
the library, a camera push, and the warning list. [The runtime reference](/reference/runtime)
lists all of it, and a page by hand can grow any of it as needed.
