Talks
The chrome every plate-book talk shares: the rail, the cover, the full-bleed plates, the pinned stage, the ink classes and the motion. A talk keeps its figures, its signal ink and nothing else. Plain CSS and one ES module, no framework, no bundler.
Contract
Pin @dunn.dev/reference exactly in a
package.json, install it, and copy talk/
and tokens/ to dunn.dev/reference/ beside
index.html to run it locally; that directory is
gitignored. The deploy sets needs_npm_install: true
on the cloudflare-worker component, and its
build_command copies the same two directories into
the output directory, so development and production resolve one
URL and the CSP stays at 'self'. The path drops the @, which the
asset server would otherwise redirect on every request. The chrome loads no fonts;
the talk keeps its own Fraunces and Newsreader link. The talk's
own talk.css loads after the chrome and imports the
motion tokens, which is what the
design-system check
reads, so a talk carries no deviation.
npm ci && mkdir -p dunn.dev/reference
cp -r node_modules/@dunn.dev/reference/{talk,tokens} dunn.dev/reference/
<link rel="stylesheet" href="./dunn.dev/reference/talk/talk.css">
<link rel="stylesheet" href="talk.css">
<script type="module" src="talk.js"></script>
/* talk.css */
@import "./dunn.dev/reference/tokens/motion.css";
:root { --sig: #8a4b12; --sig-text: #733e0f; }
The markup is the plate book: .progress-rail,
nav.index with an empty ul.lamps,
header.cover with an svg.cover-rule,
a section.plate with an svg.plate-hatch
before each section.chapter, and
footer.out ending on a.out-link. Each
chapter holds .chapter-grid, with
.steps > .step beside
figure.stage > .stage-inner, an optional
.fig-controls and a figcaption.
Motion
Each chapter sets timeline-scope: --ch and its
.steps names view-timeline: --ch, inset
by the stage's pin. The chapter animates a registered
--raw over contain 0% to
100%, so 0 is the stage pinning and 1 is it letting
go; .chapter-grid publishes it as
--fig, and every mark reads --fig by
inheritance. The names repeat
per chapter, so any chapter count works. Where scroll-driven
animation is missing, or with ?driver=js, a rAF
driver writes the same --raw on each chapter.
Every step rests on a finished drawing. talk.js
measures where the clicker lands each step on
--raw, centred, or head then foot when the step is
taller than the window, and lays a rest schedule over it:
--fig ramps between rests and holds across each.
By default step k of n rests at
k/n, so a figure's windows are in step units;
a mount that was drawn to other rests returns them as
at.
Steps are sized by their content and one gap. The only
allowance sits under the last step, half the stage less half
that step, so the last step's middle reaches the reading line as
the stage lets go. The plate's head is printed at the pin; its
caption and controls switch on at --chrome-a. A
mark is class="ink ink-*" with --a
where it fires: scroll only decides whether
--fig has passed it, and the mark then draws once
over --dur-mark-fade (280ms) or, for strokes and
growth, --dur-mark-draw (480ms), and draws back
from wherever it is if the scroll returns. The chrome
ships draw, fade, wipe,
enter, out, close,
grow, ride and dim. At
860px and under, under reduced motion and in print, one media
block shows every plate finished.
API
import { talk } from './dunn.dev/reference/talk/talk.js';
talk({ mounts: { 'ch-i': mountFirst, 'ch-ii': mountSecond } });
// mount(host, ctrl, chapterEl) -> { states, schedule, caps, lit(i), readout(fig), still(), at }
A mount draws its figure into .stage-inner and may
return the four lamp names with their windows, their caps (a CSS
number per lamp it never lights past, such as
clamp(0, var(--gate), 1) to hold one dark until a
control opens it), a lit(i) that sets lamp
i on or off itself (1, 0, or null to leave it to its
window), a readout called
with the chapter's --fig, a still for
the static treatment and the at its steps rest on. Data a figure needs is the talk's to
load before it calls talk(). The module owns the
fallback driver, the static mode, the progress bar, the index,
the lamps, the cover rule, the plate hatching and the clicker:
PageDown and PageUp step through the cover, plates, steps and
the way out on the --ease-in-out token.
window.__talk reports the driver, each chapter's
raw and --fig numbers, its rests and the clicker's stops.
A readout that counts or reports a mark reads the mark, not the
scroll: drawn(el) from talk.js is the
mark's --e as drawn this frame, so a counter reaches
its number as the mark does and runs back with it. A readout, note
or lamp that steps changes at the --a its mark
switches on, never at the end of the mark's window. The chrome
reads a readout for as long as marks draw after a scroll; a
control that moves a mark calls moved() to keep it
reading.
A .fig-note whose text changes per step reflows the
page as it grows and shrinks. Give it every reading up front:
const show = hold(note, texts) from
talk.js renders one span per text in a single grid
cell, sized by the longest, and show(i) moves
.on. A plain-text note is untouched.
What a talk keeps
In its own :root, --sig and
--sig-text; beside them, the marks only its figures
draw and its reader-driven properties, with their finished
values in its own print, reduced-motion and narrow block. A
deliberate difference in the frame is one :root
line: --rail-left, --rail-width,
--rail-num, --rail-top,
--rail-bottom, --step-gap,
--lead (scroll before step One),
--step-last, --pin
and --chrome-a.
A talk's identity is more of the same, each token defaulting to
the chrome's own look: --paper-tooth (the body's two
7px gradients), --f-stroke (1.6; figure line weight,
rings 0.2 less), --hatch-w (2; plate hatch weight),
--rule-every and --rule-on (12 and 5; the
cover rule's signal tick), --rule-rate (.21),
--hatch-rate (.23) and --hatch-step (1.9;
the ornament phases), --course-max (none; the
course line's measure). The rail reverses to ink while a plate
passes behind it.