SuperAPI AI Directory

cairn-stack

Office & productivity

View repository

Updated 10-16 06:58

About this skill

Cairn stacks

A Cairn stack is one .cairn.html file. Inside it: the reader software, the optional visual editor, a card-based document stored as plain JSON, every picture and sound it needs, and the reader's place in it. Open it from a folder and it works — no server, no install, no network. Press Edit and the reader becomes an author. Press Save As and the browser writes a new file that carries all of that with it.

That constraint is the whole point, and it shapes authoring:

  • Content is data, never code. No author CSS, no author JavaScript. Colour comes from eight theme tokens; behaviour comes from a small closed set of declarative actions. What you cannot express is deliberately out of reach.
  • Everything is inside the file. Prefer sealed, which cannot reach the network at all. A stack that needs a CDN is a stack that dies with the CDN.
  • Layout is absolute. A card is a fixed canvas — 1280 × 800 unless you choose otherwise — and every element is a positioned box. There is no flexbox to save you, which is why the grid below matters more here than in ordinary web work.

How to build one

You write a *.stack.json spec — the document plus a list of asset sources — and the bundled script produces the .cairn.html. It handles everything mechanical: < escaping, marker splicing, the canonical CSP, sorted asset blocks, exact decoded byte counts. Hand-editing the HTML is possible and unnecessary; spend the effort on content and composition instead.

S=<skill-dir>/scripts/cairn.mjs          # no dependencies, no npm install

node $S new     field-guide.stack.json --title "A field guide to lichen"
node $S build   field-guide.stack.json -o field-guide.cairn.html
node $S check   field-guide.cairn.html
node $S preview field-guide.cairn.html --out shots   # render and measure
node $S extract someone-elses.cairn.html -o edit.stack.json   # to revise one

new copies a working six-card starter and mints a fresh UUID v4 docId. Read that starter (assets/starter.stack.json) before writing your own — it is the grid, the type scale, and the interaction patterns in usable form. Keep its geometry; replace its words.

build refuses to write when the document has errors, and prints design warnings without blocking. check re-runs everything against a built file and also proves that every byte outside the four managed regions still matches the shell it came from.

Then look at it — this step is not optional, and it is the one most easily skipped. preview renders every flow card in headless Chromium and reports what static validation cannot know:

  • Measured text overflow, from the real DOM. A text element that is too small for its copy does not reflow or grow — it becomes a scroll box. The words are still reachable by dragging inside them, but they are invisible at a glance, the arrow keys that page the deck will not reveal them, and nobody scrolls a paragraph on a projector or a phone in a field. It raises no error anywhere, which is why this is the most common way a valid stack is a bad one.
  • Console and page errors raised while rendering.
  • A PNG per card in --out.

Then read the PNGs with your own eyes. preview proves nothing overflows and nothing threw; it cannot tell you a card is ugly, that a figure is optically off-centre, or that two paragraphs say the same thing.

preview needs Playwright — resolved from the project or a global install. It never installs anything. If it reports skipped, either install Playwright (npm i -D playwright && npx playwright install chromium) or open the file yourself and page through with the arrow keys (Esc for the chrome, O for the overview). If you genuinely cannot see it rendered, say so in your report instead of implying you checked — an unseen stack is an unverified one.

Editing an existing stack

extract recovers an editable spec and its asset files. Keep the docId — it is the document's identity, and reader progress in copies out in the world is keyed to it. Mint a new one only when you intend a genuine fork. Keep card, element, and asset ids stable too: handlers, saved state, and navigation all reference ids, never labels.

The grid

Every good Cairn card in this system shares one geometry. Using it consistently is most of what makes a stack look designed rather than assembled.

canvas    1280 × 800
margin    x = 76, content width = 1128      ← left edge of everything
header    kicker y 140 · heading y 174 · rule at y 116
body      content lives between y 268 and y 620
footer    nav buttons y 636 · progress y 700
columns   full 1128 · split 600 + 464 (gap 64) · sidebar 520 + 548 (gap 60)

Right edges land on 1204 (= 1280 − 76). A two-column card is x: 76, w: 600 beside x: 740, w: 464. Keep at least 32px of air on every side; the linter says so, and crowded edges are the most common way a card reads as unfinished.

Put the furniture — rules, page marks, footer navigation, the progress readout — on a background, not on each card. Backgrounds exist so that changing the footer once changes it everywhere, and so a cover can look different by using a different background rather than by special-casing a card.

Choose the canvas for the device

The reader scales the whole card uniformly to fit the window — min(w/W, h/H), letterboxed, never cropped. So the canvas aspect ratio decides how big the type actually is on the reader's screen, and this is the one early decision that is expensive to reverse.

  • 1280 × 800 (the default) suits laptops, desktops, and projectors. On a portrait phone it scales to about 30%, so 19px body copy renders near 6px. That is unreadable, not merely small.
  • 420 × 860 suits a stack meant to be read on a phone in the field. It renders roughly 1:1 on a handset; on a desktop it appears as a narrow centred column, which is an honest trade rather than a bug.

If the user says "on their phones", "in the field", or "on my commute", ask yourself which device wins and pick the canvas deliberately — then tell the user which you chose and why. Do not split the difference: a canvas that fits neither is worse than one that fits one. references/layouts.md carries the full grid for both.

Type

There is one gotcha worth internalising: the runtime renders h1 at 2.3× the element's own font size. So you author a display line by the size you want it to look, divided by 2.3.

Role Looks like Author as Line height
Cover title 62px <h1> at fontSize: 27 1.08
Card heading 42px <h1> at fontSize: 18 1.08
Standfirst 21px <p> at fontSize: 21 1.5
Body copy 19px <p> at fontSize: 19 1.55
Small body 17–18px <p> 1.5
Caption, footnote 14–15px, color: "muted" <p> 1.45
Kicker / eyebrow 13px, fontWeight: 700, color: "accent", UPPER CASE <p> 1.3

A heading frame needs about size × 1.08 × lines + size × 0.45 of height. Give body text a frame with room to spare: the linter estimates whether copy fits, but an estimate is not a measurement, and text that overflows its box turns into a scroll box rather than reflowing — present but invisible.

Tables get no cell padding from the runtime, so key/value lists read better as paired lines (<p><strong>Term</strong><br>Definition</p>) than as <table>.

Colour

Element styles take token names, never hex: color: "accent", not color: "#1C554C". Hex values appear once, in doc.theme.colors. That is what makes a stack restylable — change the theme and every element follows.

The eight tokens are bg, surface, text, muted, accent, accentText, success, danger. Define all eight and a radius; a partial palette silently mixes with defaults and looks accidental.

Contrast is a real constraint, not a nicety — text on bg and accentText on accent both need 4.5:1. references/layouts.md has five palettes with verified ratios; the linter checks whatever you write.

Two habits carry most of the visual weight:

  • One accent, used sparingly. Kickers, the primary button, a rule, a figure stroke. Everything else is text and muted. Stacks look cheap when every element wants attention.
  • Shapes are furniture, not decoration. A surface rect behind a block of text makes a panel. A 2px muted rect at 0.3 opacity is a rule. Three small accent rects stacked are a mark. Give them negative z so content sits above.

Composing a card

Each card should be one idea, held in a shape the reader recognises by the third card. The reliable rhythm is: kicker → heading → one column of prose → one thing to look at or do.

Resist filling the canvas. A card carrying one 300px paragraph and one figure reads as confident; the same card with four boxes and a stat strip reads as a dashboard nobody asked for.

references/layouts.md has the archetypes with real frame numbers — cover, statement, prose-and-figure, two-up comparison, gate, reveal, gallery, closing — plus the palettes and safe-SVG patterns. Read it when you start composing.

Behaviour

State is declared and typed. Four types (string, number, boolean, stringList), each with a matching initial, and no coercion anywhere.

Action values and guards are expression strings, not JSON values. This is the single most common mistake:

{ "act": "set", "var": "done", "to": "true" }        ✓ the expression true
{ "act": "set", "var": "done", "to": true }          ✗ a raw JSON boolean
{ "act": "set", "var": "who", "to": "\"Ada\"" }      ✓ a quoted string literal
{ "act": "set", "var": "who", "to": "Ada" }          ✗ reads Ada as a variable
{ "act": "set", "var": "label", "to": "concat(a, b)" }  ✓ join with concat, not +

Handlers live on elements, cards, backgrounds, or the stack, and resolve in that order. Use the placements deliberately: element handlers for buttons, card handlers for enter/leave bookkeeping, background handlers for shared furniture, stack handlers for keyboard navigation.

Interpolate live values into display text with {{ expression }}: "{{ card }} of {{ cards }}", "Ready? {{ round(confidence * 100) }}%".

references/recipes.md has working JSON for the patterns worth reusing — guarded continue with a hint, reveal-on-demand, quiz scoring, keyboard navigation, a menu card, checklists, and an external link. Copy from there rather than deriving handler wiring from scratch.

What "finished" means

Before you hand a stack over, check and preview should both be quiet, you should have looked at the rendered cards, and these should be true:

  • Every card says something. A card with a heading and eleven words is a slide, and the linter will tell you its character count.
  • The reader always knows where they are and how to move — progress readout, visible navigation, and ideally a key handler.
  • Interactive state is honest: a guard that blocks Continue also explains why, rather than making the button silently inert.
  • Images have alt text, or "decorative": true when they genuinely carry nothing.
  • It is sealed, unless it truly needs the network.

If the brief asks for something comprehensive or fully featured, there is a measurable bar the shell itself defines — 20 cards, two backgrounds, all four variable types, ten element kinds, eight action kinds, handlers at all four placements, a guard, a key handler, interpolation, an embedded SVG, and full reachability. Check it with node $S check file.cairn.html --featured. Do not chase that bar for an ordinary stack; padding a six-card explainer to hit feature counts makes it worse.

Reference files

  • references/format.md — every field of every element kind, variable, handler, action, expression built-in, and asset rule, plus the spec format the build script accepts. Read it when you need a field you have not used.
  • references/layouts.md — card archetypes with frames, five verified palettes, figure and SVG patterns. Read it when composing.
  • references/recipes.md — working handler JSON for common interactions.
  • assets/starter.stack.json — the six-card starter, and the best single example of all of the above at once.
  • assets/cairn-shell.cairn.html — the shell every stack is built into. It also carries its own complete field reference in the first 12 KB, inside #cairn-agent-reference, if you ever need to author without this skill.