Zero2Webmaster ✦ Cinematic Starter
One config file. A whole cinematic page.
The Cinematic Starter is the tooling distilled from hand-crafted showcase pages. Everything those builds derived from scratch (shader hero, scroll choreography, particle morph, big-type discipline, SEO baseline) is factored into a reusable engine. Producing a new brand's page is now: edit one JSON config → run one command → get a polished, crawlable, on-brand page.
There are now two layouts, selected by the config's "layout" field, because two page families had each shipped twice by hand:
cinematic(the default) — the motion-led showcase, factored from the SAVE THE FROGS! Technology page and the Zero2Webmaster Web Craft page.broadsheet— the still print-newspaper family, factored from The Nonprofit ICU Dispatch and The SAVE THE FROGS! Bulletin. See the broadsheet layout below.
Both layouts share the config envelope (SEO block, out, JSON-LD, section-array-is-page-order) and the colour helpers. They share nothing else, deliberately: a newspaper has no hero, no shader and no scroll choreography, so running one through the cinematic renderer would mean switching most of it off.
Proof of range: the four samples
All four pages below were rendered by the same engine from different configs. Within each layout the two samples share no palette, no fonts, no section list, and no signature set-piece. If they looked like siblings, the kit would have failed its one job.
All four sample brands are fictional demo brands, and say so on their pages. Their copy, figures and institutions are demonstration content, not real-world claims. The broadsheet samples take their structural axes from the two real papers but none of their content or colour, so nothing here can be mistaken for a real publication.
How to make a new page
- Pick a layout and copy its config: for a motion-led showcase, duplicate
configs/sample-a.config.json(dark) orsample-b(light). For a newspaper, duplicatesample-c(three faces, two accent tiers, three columns) orsample-d(two faces, one tier, two columns). Save asconfigs/yourbrand.config.json. - Fill it in: palette, fonts, copy, section list, set-piece, SEO block. Set
"out"to the output folder name. - Bake it:
node public/cinematic-starter/build.mjs configs/yourbrand.config.json(ornpm run build:cinematicto rebuild all). The output is one fully-staticindex.html— deployable anywhere. Broadsheet configs are additionally gated:npm run verify:broadsheetruns insidenpm run build, so a drifted default or a failing contrast tier fails the build rather than shipping. - Capture
og.jpgfrom the live page at 1200×630 and drop it in the output folder.
The config schema
{
"out": "sample-a", // output folder under cinematic-starter/
"meta": { // SEO baseline — title/description/canonical,
"title", "description", "url", // OG + Twitter cards, favicon, Fathom site id
"siteName", "fathomSiteId"
},
"brand": {
"name", "wordmarkHtml", // nav + footer wordmark (span.wm-accent = colored part)
"mode": "dark" | "light", // flips derived tints, glyph brightness, morph glow
"palette": { // ALL page color comes from these 8 tokens
"bg", "ink", "inkDim",
"primary", "primaryBright", "primaryDeep",
"accent", "accentSoft" // accent = links + primary CTA
},
"fonts": {
"display": { "family", "axis", "stack", "headingWeight" },
"body": { "family", "axis", "stack" } // any Google Fonts pair
}
},
"effects": { "grain": 0.045, "cursor": false, "ripple": false, "progress": false },
"nav": { "homeHref", "guideHref", "guideLabel", "guideShort" },
"jsonLd": { ... }, // structured data, emitted verbatim
"sections": [ ... ], // array order = page order; "enabled": false to skip
"footer": { "lines": [ ... ] }
}
Pointer effects are opt-in, and that is a design rule
Two effects flags control mouse-following flourishes, and both default to false:
ripple— concentric waves radiating from the pointer, drawn in the hero shader.cursor— a 44px ring lagging behind the pointer, with the native cursor hidden (cursor: none) and magnetic pull on links and buttons.
They default off because of what happened when they defaulted on. The ripple was written for /stf-technology/ — a page about a pond, where concentric rings on water are the subject — and because it arrived by default it then appeared on a piano page, a video-migration page and a web-studio page, where a water ripple depicts nothing at all. Kerry named it exactly: "It shouldn't be on pages unless it's relevant. Currently it's distracting and also a giveaway that that page was a 'copy' of another page."
That last clause is the real cost. A signature effect reused where it means nothing stops reading as craft and starts reading as a template, which is the opposite of what a showcase page is for. So the question to answer before setting either flag is not "would this look nice" but "does this depict something this page is actually about".
npm run verify:cinematic enforces it: it scans every built page and fails the build if one carries either flourish without being named in the allowlist in scripts/verify-cinematic.mjs. Turning an effect on is therefore a deliberate, reviewable edit rather than something a new page inherits by accident.
Copy fields are author-trusted HTML: <strong> for emphasis, <span class="accent"> in the headline, <span class="hl-a"> / <span class="hl-b"> for primary/accent highlights anywhere.
Section menu
| Type | What it renders |
|---|---|
hero | Full-viewport opener: eyebrow, one <h1> (accent styled as gradient, italic, or color), sub, optional CTAs + note, backdrop module (aurora WebGL shader, paper ruled-light CSS, or none), scroll cue (droplet/beam/none). |
stats | Count-up numerals strip. Final values live in the HTML (no-JS safe); GSAP animates on first view. |
manifesto | Giant statement with word-by-word scroll-scrub reveal + follow-up paragraph. |
marquee | Looping keyword strip (decorative, stops under reduced motion). |
features | Alternating editorial features: ghost numeral, self-drawing SVG visual (10 built-ins: orbits, constellation, scope, stack, press, path, window, chart, wave, shield — or raw svg), body, pill or link. A pill with "measured": true becomes the honest Navigation-Timing speed pill. |
cards | 3-up card grid with glyph icons (10 built-ins), cursor spotlight + 3D tilt on fine pointers. |
wow | The signature set-piece — see the module menu below. |
close | Closing pitch + CTA row. |
Wow-module menu
particle-morph— ~3,200 canvas particles sample shape A (an emoji, or a glyph filled with a brand gradient) and reassemble as the wordmark under a pinned scroll scrub. Params:shapeA,shapeB.lines,particleColors(keep them bright on dark bgs),rareEvery,pin,caption,fallback. Seen on Sample A.kinetic-type— rows of giant real text (crawlable, visible without JS) drift horizontally at different rates under a pinned scrub, over a drawing hairline. Params:rows(text, style:outline/ink/accent/dim, drift),pin,caption. Seen on Sample B.
New modules drop into engine/wow/ as a file exporting html/css/js and one registry line — candidates from the recipe's technique menu: a data-viz orbit system, a WebAudio-reactive piece, a retro-play tile board.
The broadsheet layout
Set "layout": "broadsheet" and the config drives a print newspaper instead: a nameplate between double rules, ears, a dateline strip, justified multi-column stories with a drop cap, desk-labelled secondaries, classifieds, folio-headed inside pages, and a colophon. It exists because two newspapers had already been built by hand — the gate for turning a one-off into a template is two shipped builds, never one.
What the two hand-built papers actually disagreed about
This is the whole basis of the parameterization. Every row below is an axis in the config; every row not below is fixed in the engine.
| Axis | Config field | #7 Dispatch | #8 Bulletin |
|---|---|---|---|
| Ground / tint | brand.paper.ground, .tint | off-white #F4E9E1 | pale straw #F1EFE2 |
| Ink | brand.paper.ink | navy #2E3660 | forest #1F2A16 |
| Accent tiers | .accentInk/.accentBright + optional .linkInk/.linkBright | one (maroon does kickers and links) | two (green kickers, orange links) |
| Faces | brand.faces.display/body/furniture | Libre Caslon ×2 + Montserrat | DM Serif + Source Serif 4 + Barlow Condensed |
| Sheet width | brand.metrics.sheetMax | 1360px | min(1400px, 94vw) |
| Lead columns | brand.metrics.leadColumns | 2 | 3 |
| Drop cap | brand.metrics.dropcap | firstLetter — a coloured ::first-letter | reversed — a cream glyph on an accent block |
| Furniture tracking | .kickerTracking, .folioTracking, .datelineTracking | 0.24em | 0.3em |
| Front-page ratio | brand.metrics.frontRatio | 2.15fr | 2.35fr |
| Rubric set + copy | each section's title / desk | Vitals · Diagnosis · Prescriptions · Case Notes · Prognosis | Dispatches · Field Notes · Almanac · Bulletin Board |
| Edition banner | masthead.editionBanner | absent | present |
| Set-piece | a wow panel in front.rail | canvas ECG trace | typeset event notice |
dropcap is the only axis that changes the emitted HTML rather than just CSS — a pseudo-element cannot carry a background block on a justified column, so a reversed cap needs a real element (with the letter repeated in a .sr-only span so a screen reader still hears "Since", not "S" then "ince").
The fixed half — what a config cannot touch
These are the things both papers agreed on without discussing, and they are what make a page read "newspaper" at thumbnail size before a single word is legible. An author can change the paper's identity completely and still cannot break its layout; that asymmetry is the point of the template.
- Nameplate between double rules; ears above; dateline strip below; a folio at the head of every inside page.
- Tabular numerals (
"tnum" 1) in all furniture; ligatures off ("liga" 0) on display type. - Justified multi-column measure with hyphenation; a tombstone
■ends a story. - Body copy never below 20px — density comes from column count and sheet width, never from shrinking type.
- The ears are a flex row, never a
1fr auto 1frgrid. That grid was #7's one overflow bug: a long ear could not shrink below its content.
Still by default
A broadsheet has no ambient motion of any kind — no shader, no particles, no GSAP, no scroll-triggered reveal, no ticking clock. Two screenshots taken seconds apart are identical. That stillness is the style, not an omission, and the verifier asserts it: zero @keyframes, zero animation/transition properties, zero timers in the page script.
The one thing that can change is the set-piece, and only in response to the reader. Nothing renders on load, so the live page stays byte-identical to its own static HTML until someone touches an input.
Set-piece: notice-composer
A small panel of inputs (select / text / range), a pure function from those inputs to typeset output, and no motion. Templates interpolate {field}, plus {a:field} for the indefinite article, {lower:field} / {upper:field}, and {lc:field} / {uc:field} for first-letter case. Empty text fields fall back to a declared fallback rather than going blank.
The drift problem is solved structurally. Both hand-built papers had to keep their static default markup in sync with what their JS renders for the default inputs — if it drifts, the page visibly rewrites itself the moment a reader acts, and a no-JS reader sees copy the composer would never produce. On both pages that match was checked by eye. Here the two pure functions are used at build time to render the static defaults and serialized into the page script from their own source, so there is no second implementation to keep in step. scripts/verify-broadsheet.mjs then re-derives the default output from the emitted page's own script and asserts it equals the emitted page's own markup.
The canvas-trace model from #7 is deliberately not a module: it is one build, not two.
Imagery — engravings, photographs, and the screen
A newspaper's pictures are cuts. Two kinds are supported, and a cut can sit inside a story or on its own plate page.
- A code-drawn engraving —
{"svgFile": "art/apple-press.svg"}. This is the broadsheet family's native imagery: both hand-built papers drew everything in SVG (masthead emblems, a life-cycle strip, a clipped species cut, a stippled world dot-map, a woodcut tree) and shipped no photographs at all. Engravings are resolution-independent, weigh a few KB, inherit the paper's ink throughcurrentColor, and have no asset that can go missing. Files live inart/as a shared library and are inlined at build time, so nobody has to paste SVG into JSON. - A raster photograph —
{"src": "…", "alt": "…", "width": …, "height": …}.alt,widthandheightare required, and the build fails without them: a missingaltis invisible to the author but not to a screen-reader user, and a missing intrinsic size reflows the page as the image arrives. Neither shows up in review on a fast local machine.
Rasters get screened. A full-colour photograph dropped onto newsprint reads as a website — real papers put it through a halftone dot screen first, so treatment does the same:
| Treatment | What it does | Use it for |
|---|---|---|
halftone | Grayscale, contrast lifted, then a dot screen multiplied over the image. The dots are a CSS radial-gradient — no asset. | Any photograph in a story. The default choice. |
duotone | The paper's accent ink on the frame, the grayscaled image screened over it. | A masthead cut or section opener, where a dot screen would fight the type. |
none | Nothing. The image as supplied. | An image that is already screened, or a logo. |
span picks the measure: column sets the cut inside the justified column (the caption sets to the column width), while full uses column-span: all to break the cut across every column of a multi-column story. Both take a caption (trusted HTML) and an optional credit.
Two details that were bugs first, in case you extend this: the treatment lives on an inner .cut-frame wrapper, not on the <figure> — on the figure, the dot screen covered the caption text. And mix-blend-mode on an <img> blends against the backdrop, not against the image's own background, so a duotone set directly on the image silently does nothing; the ink has to go on the wrapper.
See Plate I on Sample C for both raster treatments side by side from one source file, the full-width engraving in its lead story, and the column-width cut on Sample D.
The per-surface contrast gate
Every text tier is checked at build time against both the page ground and the tint panels, and a failure stops the build with the measured ratio. This exists because contrast is per-surface, not per-palette, and that fact already cost a shipped page: #8's orange link tier began as #ab5200, which measures 5.4:1 on the straw ground — comfortably passing — but only 4.2:1 on the tint panels it also sat on. It had to be darkened to #964800. A ground-only check would have passed it, and a human caught it with a screenshot audit. Now the build does. (The verifier feeds #ab5200 back through the gate on every run to prove the gate still rejects it.)
Other things that stop the build rather than becoming a page that looks fine and is quietly wrong: a reversed drop cap whose ground fails 3:1 on the accent block; an unknown section type or set-piece module; a notice template naming a field that was never declared; zero mastheads, or more than one.
Broadsheet section menu
| Type | What it renders |
|---|---|
masthead | Required, exactly once. Ears row (incl. a pubmark kind for the publisher's emblem + motto), the one <h1> nameplate, standfirst, optional edition banner, dateline strip, closing rule. Rendered outside <main> — it is the paper's nameplate, not content. |
front | The front page: a lead story (kicker, headline, deck, byline, body blocks of kind p / callout / jumpline, plus a dotted-leader "inside this edition" index) beside a rail of panels — each either a box or a wow set-piece. |
secondaries | Below-fold teasers, one column each, rule-separated: desk label, headline (optionally linked), teaser, "more" link. |
classified | The rubric grid — services, contests, ways in. Boxed on the tint ground with a configurable column count; each ad has a desk, a tag, a title, an optional big figure, body and a link. |
spread | An inside page: folio, rubric kicker, optional feature headline, justified essay body, optional tint-panel sidebar. |
close | The closing page: big pull-quote, attribution, a standing statement between double rules, and a CTA row. |
plate | A full-width engraving band — the "Plate I" pattern. One or several cuts across the sheet, folio'd like any inside page. See imagery. |
rule / ornament | Furniture: a double section rule, or a centred typographic mark. |
What the engine guarantees on every page
- Crawlable: every word is real HTML in the static file — view source on either sample. Meta/OG/Twitter/JSON-LD baked into
<head>. - BIG readable text: body 20–26px, hero up to ~124px — the non-negotiable inherited from the exemplars.
- Reduced motion respected: shader renders one still frame, morph swaps to a static fallback, kinetic rows stand still, marquees stop, all copy immediately visible.
- Performance: GSAP vendored locally, rAF loops pause off-screen, devicePixelRatio capped, everything animating is GPU-friendly.
- Accessibility: one
<h1>, semantic sections witharia-labels, decorative canvasesaria-hidden, skip-link, visible focus styles, custom cursor only on fine pointers. - No hardcoded brand: a grep for hex colors in
engine/finds only an offscreen sampling mask. Every visible color derives from the config's 8 palette tokens.
Provenance
The cinematic layout, Samples A and B, and the first version of this guide were designed, coded, and iteration-tested in a single session by Claude Fable 5 (Anthropic's Mythos-class AI model), working from a one-page written brief — the same method that produced the two exemplar pages it generalizes.
The broadsheet layout, Samples C and D, scripts/verify-broadsheet.mjs, and this section were added in a later session by Claude Opus 5, factored from the two shipped newspapers (#7, #8) once the two-build gate was met. Both layouts are structured to port cleanly to a future Next.js <CinematicSite spec={...}> renderer.