The stylesheet

A ScriptPub document keeps all reusable visual choices in one real stylesheet — the document sheet: the <style id="scriptpub-style"> element in the file’s head. A bare file opened in any browser renders styled by it, with no tooling — the zero-tooling property covers appearance, not just structure. The config JSON keeps only what CSS cannot say: block identity and flow, foreign-syntax grammar, indexes, revisions.

You author the sheet the way you style a web page: rules, selectors, real @media queries. The engine parses it back into a structured model (the SPub CSS profile below), so forms, validation, print, and pagination all read the same truth.

One machine layer, then the document’s own rules

<style id="scriptpub-style">
@layer spub-base { /* machine prelude — regenerated on save, skipped on parse */ }

/* the document's own rules — UNLAYERED, plain CSS */
main { font-family: var(--font-serif); max-width: 56ch; --spub-outline: right; }
h1.act { text-transform: uppercase; font-weight: bold; text-align: center; break-before: page; }
p.dialogue { margin-left: 2ch; }
@media (max-width: 40rem) { main { --spub-outline: hidden; } }
@media print { @page { size: a4; margin: 2cm 2cm 2cm 2.5cm; } }
html[data-spub-intent~="edit"] main { --spub-editor-active-block: highlight; }
</style>

The sheet has exactly two parts:

Part Owner Holds
@layer spub-base { … } the serializer (machine) Theme role tokens (--paper…, light-dark() pairs), color-scheme, the user-assignable font slots, and the minimal structural floor (body/main defaults, table collapse, media bounds). Regenerated on every save, skipped on parse, never hand-edited.
everything after it the document The format’s own styling as ordinary unlayered CSS: block/mark rules, root typography, conditions, @page, --spub-* settings.

That is the whole cascade story: per CSS, unlayered author rules beat any layered ones, so the document’s rules override the base natively — no layer statement, no specificity games, no named document layer. There are no other layers in a file or in this spec; the styling layers an application composes around a document at runtime (user themes, appearance tweaks) are app-internal and never appear in a file — see the app’s Themes documentation.

A foreign @layer block in a document round-trips verbatim like any foreign CSS; only spub-base is machine-owned.

Selectors — the file spelling

Because a kind’s selector is also its write spelling, the block selectors ARE the kinds — the same string recognizes, writes, and styles:

Selector Styles
:root / html theme custom properties; condition guards (below)
body, main the page / the document surface — root typography, measure (max-width), padding, --spub-* settings
a block kind’s selector (p.dialogue, h1.act, hr) that block kind — the serializer always writes the selector’s tag + classes, so these rules match in a bare browser
a mark’s selector (em, u.underline) that mark
header, nav, main, footer, section.id a section — the semantic element itself (section.appendix, nav.lot, a custom section.synopsis)
section > p.paragraph:not(p.paragraph ~ p.paragraph) the first semantic paragraph block in a section — first-paragraph indent and similar leads
…::first-letter / …::first-line the pseudo-elements a paragraph lead uses — a drop cap (initial-letter/float) or a small-caps opening line

When an application embeds a document it re-scopes the sheet under the host element (main/body/html collapse onto the scope; guards become attribute tests on it), emitting the host scope through :where(…) so embedding does not increase an authored selector’s specificity. The file spelling stays document-native.

Fragmentation — breaks and keeps

Page breaking is real CSS, stated where any print stylesheet states it:

h1.chapter { break-before: page; }
h3.scene   { break-after: avoid; }        /* keep with what follows */
table.stats { break-inside: avoid; }      /* atomic */

The full CSS Fragmentation vocabulary applies — auto, avoid, avoid-page, page, left, right, recto, verso, column, avoid-column for break-before/break-after, and auto / avoid / avoid-page / avoid-column for break-inside — in the sheet per kind, or on an instance’s style attribute for one occurrence. There is no structured pagination directive; a paginating renderer resolves these declarations through the same cascade the browser uses.

Conditions

Environment conditions are real media features — the browser evaluates them natively, and an engine evaluates the same queries in JS where a live cascade isn’t available (pagination, print materialization): screen / print, min-width / max-width, orientation, monochrome / color, pointer, hover, resolution. App-applied conditions are data-spub-* attribute guards on html, inert in a bare browser — see Conditions.

Settings — --spub-*

Everything visual that standard CSS can’t express rides the same cascade as namespaced custom properties (a browser simply ignores them): flow (--spub-flow: continuous | snap | flip), panes (--spub-outline, --spub-progress), page furniture templates (--spub-numbering-format: "{n}.", --spub-page-header, --spub-show-nonprinting), and editor behavior (--spub-editor-block-labels, --spub-editor-active-block). The machine-readable catalog is SETTINGS in the engine (sheet/settings.ts); Settings documents the vocabulary.

Direct element styles

A block, mark occurrence, inline object, or grid cell MAY carry attrs.style, a safe CSS declaration list serialized verbatim as the HTML style attribute. This is the canonical representation of one-off direct formatting (including a one-off forced break). Reusable class and kind styling remains in the document sheet. Inline style wins through the ordinary CSS cascade.

Executable expressions, javascript: URLs, and markup-breaking < or > are never rendered or serialized.

Print

@page is stored natively — size, margins, :first/:left/:right — usually inside @media print. The furniture templates (--spub-numbering-*, --spub-page-header/-footer) are the source; the @page margin-box content rules are compiled from them at render time ({n}counter(page), {title}/{author} → escaped literals, {division} → a CSS named string). Hand-authored margin boxes pass through and cascade.

The SPub CSS profile (what parses as structure)

The engine’s parser recognizes: the @layer spub-base block (skipped and regenerated — never round-tripped as document CSS); flat style rules over the selector vocabulary above; @media with the enumerated feature set; @page (+ pseudo pages, margin boxes); @font-face.

Foreign CSS is preserved verbatim — an unrecognized rule or at-rule (including a foreign @layer block) round-trips byte-identically and surfaces read-only in an editor. Nothing is ever dropped for being unknown; validation warns. The only errors are the security bans:

  • @import — never.
  • url() — only inside @font-face { src: … }, and only as a relative asset reference or a data: URI. Everywhere else url() stays banned, so media can never enter via CSS.
  • expression(), javascript:, and < / > anywhere (a literal </style would terminate the HTML element regardless of CSS strings). Braces are fine inside quoted strings — templates like "{n}." are legitimate.

The curated property catalog survives as the known vocabulary driving editor forms and validation’s warnings — it is not an allowlist that rejects. The known font vocabulary includes the CSS Fonts variant longhands (font-variant-numeric, -ligatures, -emoji, -east-asian, -caps).

Theming

Each theme role is declared once in spub-base with light-dark() under :root { color-scheme: light dark } — a bare file follows the OS scheme; an app’s light/dark toggle flips color-scheme via html[data-spub-scheme]. The machine-known roles are:

Group Roles
Surfaces paper (the page) · surface (raised chrome) · canvas (the workspace backdrop; defaults to var(--paper))
Ink ink · ink-soft · ink-dim · heading (defaults to var(--ink))
Decoration rule · accent · accent-dim · accent-bright · link (defaults to var(--accent))
Emphasis selection · highlight (the default marker) · hl-yellow · hl-green · hl-blue · hl-pink · hl-orange
Semantic true / false (verity marks) · success / failure (outcomes) · warning
Fonts mono (a font stack riding --font-mono, not a colour)

Extra custom properties are legal foreign CSS.

spub-base also declares one user-assignable font token for every CSS generic family: serif · sans-serif · monospace · cursive · fantasy · system-ui · ui-serif · ui-sans-serif · ui-monospace · ui-rounded · math · fangsong. A format references the corresponding token (for example font-family: var(--font-serif)), and a reader can choose the preferred fallback stack for that family without touching the document.