thomasrohde/cairn · Archived

cairn-stack

>- Build beautiful, self-contained Cairn stacks — single `.cairn.html` files that carry their own reader, editor, pictures, and reader progress, work offline from a folder, and stay readable for years. Use this skill whenever the user mentions Cairn, a `.cairn.html` file, or a `cairn/stack` document, AND whenever they ask for anything shaped like one even without naming it: an interactive card deck, a portable explainer or handbook, a field guide, a one-file course or onboarding walkthrough, an…

First seen Jul 27, 2026

Installation

$ npx skills add thomasrohde/cairn --skill cairn-stack

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

License LICENSE
Default branch main
Open issues 1
Status Archived

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 13,502 B
  • docs SUMMARY.md 835 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 2 installs

SKILL.md

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.