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.
@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 adata:URI. Everywhere elseurl()stays banned, so media can never enter via CSS.expression(),javascript:, and</>anywhere (a literal</stylewould 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.