# Rich figures on Dwarves Memo: the figkit contract

memo renders a guest post's interactive figures natively. It keeps each `figure.fig` block from the post, rebuilds it from the allowlist below, and loads its own copy of figkit, which turns declarative markup into tooltips, highlights, controls and motion. memo runs no script from the writer's site.

**Version.** memo vendors figkit 1.5.0. That vendored copy is the source of truth for what memo renders; this page describes it. A figure written for a newer figkit still renders as static markup, and features past 1.5.0 stay inert until memo upgrades.

## Rule zero: the static figure is the figure

Every figure must read completely without figkit: in RSS, with scripts off, and with reduced motion. figkit only adds behavior. Nothing is hidden at rest. Controls live in a `[data-fk-controls]` placeholder marked `hidden`, which figkit fills and reveals.

## What memo takes from a post

- **Figures.** Every `figure` with class `fig`, and every `table`, keeps its markup. The rest of the post converts to Markdown.
- **Figure CSS.** Up to 3 same-host `https` stylesheets linked with `<link rel="stylesheet">` whose path contains `fig` (for example `/figures.css`) or whose tag carries a `data-figkit` attribute. 512 KB in total. A missing or failing stylesheet leaves the figures unstyled; it never blocks the post.
- **Nothing else.** memo ignores inline `<script>` and `<style>` on the page, and every other stylesheet.

## Allowed markup

- **Elements.** The HTML subset is `figure figcaption div span p h4 h5 h6 ol ul li dl dt dd strong em b i small sub sup br code a`, plus table markup (`table caption colgroup col thead tbody tfoot tr th td`). The SVG set is `svg g defs marker symbol use path line polyline polygon rect circle ellipse text tspan title desc linearGradient radialGradient stop clipPath pattern`.
- **Attributes.** `id`, `class`, `role`, `aria-*`, `hidden`, `tabindex`, `data-tip`, `data-group`, `data-animate`, `data-step`, `data-axis`, `data-from`, and `data-fk*`. Inline `style` is allowed when it holds custom properties and plain declarations only. SVG geometry and presentation attributes are allowed, including `viewBox`, `preserveAspectRatio`, `marker-*`, and `transform`. Inside a figure, `href` is allowed only as a same-document `#fragment`.
- **Dropped.** `script`, `style`, `foreignObject`, `iframe`, `object`, `embed`, images, `mask`, `textPath`, form controls (`button`, `input`, `select`, `textarea`), and SMIL animation (`animate`, `set`, `animateMotion`, `animateTransform`). Any `on*` attribute. `url()` inside `style` other than `url(#id)`. An external `href` or `xlink:href`. `javascript:` URLs. Any other `data-*` attribute.
- **Class names.** Write classes as usual. memo prefixes each guest class with `g-` in both markup and CSS, so a figure never reaches memo's own classes. figkit's classes keep their names: `fig`, `fig-body`, `num`, `is-hot`, `is-dim`, and `fk-*`.

## CSS rules memo enforces

The figure stylesheet is guest CSS. memo sanitizes and scopes it before it reaches the page.

- **Scope.** memo prefixes every selector with the note's own scope class. A leading `html`, `body`, `:root` or `.body` is replaced by that class, so no rule leaves the note.
- **At-rules.** Only `@media`, `@supports`, `@container` and `@keyframes` survive. `@import`, `@font-face`, `@layer`, `@property` and every other at-rule are dropped.
- **Declarations.** A declaration is dropped when its value loads or runs anything: `url()` other than `url(#id)`, `image-set()`, `expression()`, `javascript:`, `data:`, or bindings. `position: fixed` is dropped.
- **Keyframes.** memo prefixes `@keyframes` names, so a guest `spin` never shadows memo's own.
- **Dark mode.** A `prefers-color-scheme` block follows memo's theme toggle, not the reader's OS setting.
- **The figure box is memo's.** A `.fig` starts at memo's text column and, on a wide screen, grows to the right up to 60rem or the room left in the main area, never past the left sidebar (a phone keeps the column width): memo overrides its width, min and max width, horizontal margins, position, offsets, and transform, so a breakout rule from your site has no effect. Wide content scrolls in its own box: wrap the intrinsically wide part in `<div class="fk-scroll">`, and the rest of the figure keeps the plate width. A figure with no `.fk-scroll` falls back to scrolling `.fig-body`.
- **Type is yours, fonts are memo's.** memo's article styles (heading sizes, uppercase labels, paragraph and list spacing, inline-code chips) do not reach inside a figure, so your figure CSS sets its own type. `--font-body`, `--font-data`, `--fk-font` and `--fk-mono` map to memo's sans and mono.

## Attributes

| Attribute | Behavior |
|---|---|
| `data-tip="text"` | Tooltip on hover, focus, and tap. Escape or a tap elsewhere hides it. It stays inside the viewport and is wired with `aria-describedby`. A bare SVG shape takes the text as its `aria-label`. Thin lines get an invisible 14px hit halo. |
| `data-group="k1 k2"` | Hover, focus, or tap marks every member sharing a key `.is-hot`, and the figure's other members `.is-dim`. |
| `data-animate` + `data-step="n"` | The figure rests on its finished state. figkit adds a Play button to the figure's top right, which runs the motion with delay `n x --fk-gap` (default 0.6s), reads "Playing" while it runs, and reads "Play" again after. Nothing plays on its own. Values: `sequence` (rise in), `count` (count the first number from `data-from`, default 0), `draw` (stroke along its length; a `marker-end` arrowhead appears only when its own line finishes), `grow` (scale from the left, or from the base with `data-axis="y"`), `pop` (scale from the centre), and `travel` (on a `<line>`: a marker rides from x1,y1 to x2,y2). Any other value is a hook for figure CSS under `.fk-play`. An element without `data-step` inherits the step of its animated ancestor. |
| `data-fk="graph"` on an `svg` | Nodes carry `data-fk-node="id"` (every piece of one node shares the id). Edges carry `data-fk-edge="from to"`, and the pieces of one edge (arrow, label) share `data-fk-part="id"`. Hovering a node lights it, its edges, and its neighbours. Hovering an edge lights its pieces and both ends. Other elements in a graph may use `data-group`, for example UML fragments and swimlanes. |
| `data-fk-zoom` on the figure | Expand button and full-screen viewer. Absent: automatic, shown only while a `.fk-scroll` part is wider than its box. `touch`: automatic on a fine pointer, always shown on a coarse one (a phone or tablet), for a dense figure that fits a tablet plate but reads better full screen. Present (empty or any other value but `off`): always shown. `off`: never shown. The viewer shows the first overflowing `.fk-scroll` part, else the first `.fk-scroll`, else the whole `.fig-body`, as a clone with no tips, highlights or Play state. It fits first, then zooms with pinch, the wheel, the - and + buttons and a double tap; one finger or the mouse pans. Keys: + and - zoom, 0 fits, the arrows pan, Escape closes and focus returns to the expand button. The page does not scroll while it is open. |
| `data-fk-area` | Makes an unfilled shape, such as a UML fragment box, hoverable over its whole area. Anything else under the pointer wins over it. |
| `data-fk-controls` | Placeholder (`hidden`) where figkit builds controls. Its children are specs. |
| `data-fk-toggle="key"` | Spec. figkit builds a pressed-state button (`data-fk-label`). While pressed, the figure rests highlighted on group `key` and shows `data-fk-note`. |
| `data-fk-walk="idA idB ..."` | Spec. figkit builds a step button (`data-fk-label`, default "Step through"). Each press rests the figure on the next node and the edge that led to it, and the note shows that node's `data-tip`. After the last step it starts over. |
| `data-fk-hint="text"` | Spec. figkit builds a muted hint line. |
| `data-fk="threshold-rank"` | Component, described below. |

### threshold-rank

Items ranked by value, a cost threshold, and a capacity N. Zone 0 is above `cost / share` and inside the top N. Zone 1 is above it but past N. Zone 2 is below it.

- **On the component root.** `data-fk-share`, `data-fk-format="usd"`, `data-fk-scale="x0 pxPerUnit"`, and `data-fk-layout="top rowHeight gapAfterZone0 barOffset loneLabelClear"`. Also `data-fk-zone-class` (three classes) and `data-fk-zone-name` (three names, separated by `|`). Also `data-fk-unit` and `data-fk-item-unit` (singular and plural).
- **Items.** `<g data-fk-item="A" data-fk-value="172800" data-fk-y="49" data-fk-tip="... {benefit} ... {zone}">` contains one `[data-fk-bar]`. Geometry hooks are `[data-fk-band="i"]`, `[data-fk-band-label="i"]`, `[data-fk-threshold]`, `[data-fk-threshold-label]`, `[data-fk-cutoff]`, and `[data-fk-cutoff-label]`.
- **Inputs.** Specs in the controls placeholder: `data-fk-input="cost|budget"` with `data-fk-label`, `data-fk-range="min max step default"`, and optional `data-fk-format="usd"`. Also `data-fk-reset="label"` and `data-fk-summary="template"`.
- **Templates.** `data-fk-tpl` on any element, with optional `data-fk-tpl-under` (used when the budget is not filled) and `data-fk-tpl-none` plus `data-fk-zone="i"` (used when that zone is empty). `data-fk-num` renders money tokens as `<span class="num">`. `data-fk-aria` is a template for `aria-label`. `data-fk-zone-group="i"` sets `data-group` to that zone's item keys.
- **Tokens.** `{cost} {threshold} {n} {nword} {unit} {list0..2} {count0..2} {count0items} {low} {lowbenefit}`.

## Theming

figkit colors come only from `--fk-fg`, `--fk-bg`, `--fk-muted`, `--fk-rule`, `--fk-accent`, `--fk-traveler`, `--fk-font`, `--fk-mono`, `--fk-dim`, `--fk-gap`, `--fk-plate` (the figure background, which paints the scroll edge shadows) and `--fk-label-w` (the slider label column). memo sets them from its own theme, light and dark, so a figure matches the page. Use these variables in figure CSS rather than fixed colors.

## UML diagrams

Every UML diagram is a `graph`, and its pieces map as follows:

- **Sequence:** lifelines are nodes; messages are edges with `data-animate="sequence"` in message order; alt, opt, and loop fragments are groups with their guard as the tip.
- **State:** states are nodes, transitions are edges with their label as the tip, and a `data-fk-walk` steps through the main path.
- **Activity:** actions and decisions are nodes, flows are edges that `draw` along the main path, swimlanes are groups, and decisions carry their guards as tips.
- **Use case, component, and class:** actors, use cases, components, and classes are nodes; associations, include and extend links, dependencies, and generalizations are edges, with multiplicities in the tip.

## Accessibility

- **Keyboard.** A figure is one tab stop: the arrow keys move between `data-tip` marks, and Home and End jump to the ends. Every mark can carry a tip.
- **Tips.** A tip states a fact already in the text. It adds no new numbers.
- **Contrast and focus.** Focus rings use `--fk-accent`. Text keeps 4.5:1 contrast in light and dark.
- **Touch.** A tap is a touch `pointerup` that moved under 10px, so it works on iOS WebKit, and a swipe that scrolls never counts. Buttons and slider rows are at least 44px tall. Only a `.fk-scroll` box scrolls sideways, and a vertical swipe still scrolls the page.
- **Zoom viewer.** It is a modal dialog labelled by the figure title, with focus kept inside it and 44px buttons. The stage is one tab stop that carries the figure's description.
- **Reduced motion.** `prefers-reduced-motion: reduce` turns off all motion and the Play button. The figure shows the same finished state as on load. Tips, highlights, controls and the zoom viewer still work; the viewer jumps instead of easing.
