diff --git a/.gitignore b/.gitignore
index 3c186c8..b187953 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,11 @@ build/
.cache/
*.log
.vercel
+
+# Environment + secrets
+.env
+.env.local
+.env.*.local
+
+# Generation pipeline artifacts (raw outputs before commit)
+.image-cache/
diff --git a/README.md b/README.md
index 6dab947..d747436 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
**A design skill that studies what you admire — and rebuilds your content with it.**
-→ Live demo: **[hallmark-murex.vercel.app](https://hallmark-murex.vercel.app)** · twenty-two themes, eight worked tests, press `T` to cycle.
-→ Current version: **v0.8.0** — adds genres (editorial · modern-minimal · atmospheric · playful), `design.md` for whole-project redesigns, lazy reference loading, the 2+1 font rule with 55-gate slop test, component-library exports (tokens.css always emitted; Tailwind / DTCG / shadcn formats inline in design.md), a Study gallery on the marketing site, and seven new themes (Quiet refreshed, plus Bloom, Coral, Violet, Aurora, Halo, Plume).
-→ **Latest:** **9 nav archetypes** (Floating pill · Newspaper masthead · Brutal slab · Terminal command · Edge-aligned minimal — plus the original 4) and **8 footer archetypes** (Statement · Letter close · Newsletter-first · Marquee scroll — plus the original 4), with per-genre routing. **4 hero polish patterns** (Vertical-rail · Marquee-overflow · Cursor-spotlight · Decorative-numeral). Five new slop-test gates (51–55) catch the AI nav fingerprint, the AI footer fingerprint, centred-everything heroes, hero padding asymmetry, and decorative-without-purpose. Terminal cursor constraint codified — no standalone blinks, only inside typed commands.
+→ Live demo: **[hallmark-murex.vercel.app](https://hallmark-murex.vercel.app)** · twenty-three themes, eight worked tests, press `T` to cycle.
+→ Current version: **v0.9.0** — adds the **Editorial** theme (open-design-inspired premium · warm cream + coral + Inter Tight × Playfair italic), a **watercolor + ink-wash sprinkle** across the marketing site (18 seeded plates from `google/gemini-3-pro-image` via Together AI), **generated-imagery.md** in the skill (Tier C promoted to peer of Tier B with seed-and-reference discipline + per-theme style-prompt table for all 23 themes), a power-user [`generate.mjs`](skill/_assets/scripts/generate.mjs) drop-in script, and selected open-design utilities (Roman-numeral section heads, corner-bracket image annotations, dark-slab section reversal, pulse animation, `.tnum` utility).
+→ Previously in **v0.8**: genres (editorial · modern-minimal · atmospheric · playful), `design.md` for whole-project redesigns, the 2+1 font rule with 55-gate slop test, component-library exports, 9 nav archetypes (incl Floating pill), 8 footer archetypes, 4 hero polish patterns, contrast gates 46–50, nav/footer/hero structural gates 51–55.
diff --git a/scripts/generate-marketing.mjs b/scripts/generate-marketing.mjs
new file mode 100644
index 0000000..99be8f7
--- /dev/null
+++ b/scripts/generate-marketing.mjs
@@ -0,0 +1,450 @@
+#!/usr/bin/env node
+/*
+ * generate-marketing.mjs
+ *
+ * Generates the watercolor + ink-wash sprinkle for the Hallmark marketing site.
+ * One seeded reference + ~19 sprinkles, all visibly from the same hand.
+ *
+ * Usage:
+ * node scripts/generate-marketing.mjs --slot reference # generate the locked anchor
+ * node scripts/generate-marketing.mjs --slot all # generate every slot defined below
+ * node scripts/generate-marketing.mjs --slot hero-bg # one named slot
+ * node scripts/generate-marketing.mjs --list # print the slot map
+ *
+ * Reads TOGETHER_API_KEY from .env.local at the project root.
+ * Writes PNGs to site/img/generated/.png.
+ *
+ * Style lock:
+ * - seed=7727 on every call
+ * - reference image (_seed-reference.png) passed via reference_images[0]
+ * on every call after the reference itself is generated
+ * - prompt prefix is a constant — only the SUBJECT sentence varies per slot
+ */
+
+import fs from "node:fs/promises";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.resolve(HERE, "..");
+const OUT_DIR = path.join(ROOT, "site/img/generated");
+const ENV_PATH = path.join(ROOT, ".env.local");
+
+const API_URL = "https://api.together.xyz/v1/images/generations";
+const MODEL_ID = "google/gemini-3-pro-image";
+const SEED = 7727;
+
+const STYLE_PREFIX =
+ "Watercolor and ink wash on warm cream paper. Soft translucent washes of " +
+ "coral, terracotta, and faded olive green. Visible paper grain and bleed " +
+ "edges where pigment pools. Loose asymmetric composition. Hand-made plate " +
+ "from a quiet studio. Soft, muted, never glossy, never digital-looking. " +
+ "No text. No logos. No people. Subject:";
+
+/* — Slot map ——————————————————————————————————————————— */
+
+// Gemini 3 Pro Image only accepts these dimensions:
+// Square: 1024×1024 · 2048×2048 · 4096×4096
+// 3:2 wide: 1264×848 · 2528×1696 · 5096×3392
+// 2:3 tall: 848×1264 · 1696×2528 · 3392×5096
+// 4:3 wide: 1200×896 · 2400×1792 · 4800×3584
+// 3:4 tall: 896×1200
+// The slot's *display* dimensions can still be anything the CSS wants
+// (object-fit: cover handles the crop). These are just the source sizes.
+
+const SLOTS = [
+ {
+ id: "_seed-reference",
+ role: "locked style anchor (reference for all subsequent calls)",
+ subject:
+ "a single coffee bean resting on a piece of folded raw linen, ink line " +
+ "around its silhouette, soft coral wash bleeding from one edge",
+ width: 1264,
+ height: 848,
+ isReference: true,
+ },
+ // — Section 01 · HERO ——————————————————————————————————
+ // Hallmark = the rulebook. The image is the rulebook itself.
+ {
+ id: "hero-bg",
+ role: "hero ambient backdrop · 0.22 opacity",
+ subject:
+ "a worn leather rulebook lying open on warm linen, hand-written margin " +
+ "notes in faded ink, a coral-tipped pen resting in the gutter, soft " +
+ "daylight from the upper left. the kind of book a quiet master keeps",
+ width: 2528,
+ height: 1696,
+ },
+
+ // — Transition 02 → 03 ———————————————————————————————————
+ // Bridge: 02 is "Eight briefs, eight different shapes" → 03 is Study.
+ // Image evokes the eight briefs as eight folded papers.
+ {
+ id: "transition-02-03",
+ role: "between Examples (eight briefs) and Study sections",
+ subject:
+ "eight folded sheets of paper laid out in a loose asymmetric row on " +
+ "warm cream, each a slightly different size and corner-fold. faint " +
+ "ink hatching on a few; a single coral mark on one. lit from one side",
+ width: 1264,
+ height: 848,
+ },
+
+ // — Section 03 · STUDY ————————————————————————————————————
+ // What Hallmark sees when it reads a design — diagnostic overlay.
+ {
+ id: "study-bg",
+ role: "Section 03 (Study) ambient backdrop · 0.18 opacity",
+ subject:
+ "a printed page beneath a sheet of tracing paper, the tracing paper " +
+ "covered in red ink and pencil annotations identifying type, grid, " +
+ "accent placement. an architect's diagnostic overlay. soft daylight",
+ width: 1264,
+ height: 848,
+ },
+
+ // — Transition 04 → 05 ———————————————————————————————————
+ // Bridge: 04 is Without/With (parity comparison) → 05 is Foundations.
+ // Image evokes correction → confidence: a draft and a final side by side.
+ {
+ id: "transition-04-05",
+ role: "between Without/With and Foundations sections",
+ subject:
+ "two index cards laid side by side on warm cream paper. the left " +
+ "card is cluttered with crossed-out ink corrections; the right card " +
+ "is clean with three confident pencil lines and a single coral mark",
+ width: 1264,
+ height: 848,
+ },
+
+ // — Section 05 · FOUNDATIONS ——————————————————————————————
+ // Five things Hallmark holds the line on: Type · Colour · Space · Motion · Voice.
+ // Each image is a literal study of that foundation.
+
+ {
+ id: "found-type",
+ role: "Section 05 panel anchor · Type",
+ subject:
+ "a hand drawing the letter G in serif italic on warm cream paper, " +
+ "pencil construction grid visible underneath, faint coral guide-lines " +
+ "marking the x-height and cap-height. a type-specimen study",
+ width: 1024,
+ height: 1024,
+ },
+ {
+ id: "found-colour",
+ role: "Section 05 panel anchor · Colour",
+ subject:
+ "a folded paper colour-study: one bold coral wash occupying about " +
+ "three percent of the field, set against a generous warm-cream ground. " +
+ "two faint olive accents in opposite corners. a pencil note in the margin",
+ width: 1024,
+ height: 1024,
+ },
+ {
+ id: "found-space",
+ role: "Section 05 panel anchor · Space",
+ subject:
+ "an architect's freehand floor-plan on cream paper showing five " +
+ "rooms of asymmetric size, generous white space between them, pencil " +
+ "measurement annotations along one edge, a single coral arrow",
+ width: 1024,
+ height: 1024,
+ },
+ {
+ id: "found-motion",
+ role: "Section 05 panel anchor · Motion",
+ subject:
+ "a Muybridge-style four-frame strip on a single sheet of cream paper: " +
+ "the same brushstroke arcing across four progressive moments, faint " +
+ "pencil arrows between frames showing easing curve",
+ width: 1024,
+ height: 1024,
+ },
+ {
+ id: "found-voice",
+ role: "Section 05 panel anchor · Voice",
+ subject:
+ "a hand-written marginalia in an open book on cream paper, three " +
+ "honest words pencilled tightly together, one word underlined in coral, " +
+ "the faint thumb-smudge of someone who held the page open",
+ width: 1024,
+ height: 1024,
+ },
+
+ // — Section 07 · INSTALL ——————————————————————————————————
+ // Three harnesses: Claude Code · Cursor · Codex.
+ // Each image is a metaphor that fits the harness's NAME.
+
+ {
+ id: "harness-claude",
+ role: "Section 07 install · Claude Code glyph",
+ subject:
+ "a small folded letter on warm cream paper, sealed with a coral wax " +
+ "seal stamped with the letter C, faint ink hatching around the rim. " +
+ "nothing else on the page. a missive ready to be sent",
+ width: 1024,
+ height: 1024,
+ },
+ {
+ id: "harness-cursor",
+ role: "Section 07 install · Cursor glyph",
+ subject:
+ "an antique brass drafting compass laid on cream paper, one foot " +
+ "tipped in coral graphite, a faint pencil arc traced beneath it. " +
+ "a 19th-century instrument of precision",
+ width: 1024,
+ height: 1024,
+ },
+ {
+ id: "harness-codex",
+ role: "Section 07 install · Codex glyph",
+ subject:
+ "a half-open medieval codex book on warm linen, ink-illuminated edges, " +
+ "a coral ribbon bookmark trailing across one page. simple, hand-bound. " +
+ "a codex in the literal sense",
+ width: 1024,
+ height: 1024,
+ },
+
+ // — FOOTER MARK ————————————————————————————————————————————
+ // A signing-off mark, like a colophon at the end of a book.
+ {
+ id: "footer-mark",
+ role: "footer colophon glyph",
+ subject:
+ "a single hand-stamped wax seal pressed into warm cream paper, the " +
+ "stamp abstract — a vertical ink stroke crossing a horizontal coral " +
+ "wash, faint bleed. an unsigned signature. nothing else",
+ width: 1024,
+ height: 1024,
+ },
+
+ // — EXAMPLES PLATES ————————————————————————————————————————
+ // Each plate evokes ONE of Hallmark's worked example briefs.
+ // (See site/_tests/01-tide-podcast etc. — these images can pair with rail cards.)
+
+ {
+ id: "plate-podcast",
+ role: "examples rail · indie podcast (Tide)",
+ subject:
+ "a vintage condenser microphone on a folded warm-cream linen, a " +
+ "handwritten letter half-open beside it, a single coral pencil resting " +
+ "across the page. the quiet of a recording studio at dawn",
+ width: 1264,
+ height: 848,
+ },
+ {
+ id: "plate-bakery",
+ role: "examples rail · bakery (Maple)",
+ subject:
+ "a baker's hand-bound notebook open to a sourdough recipe page, ink " +
+ "notes and a small flour smudge in the margin, a single coral length " +
+ "of kitchen string coiled beside the spine. early-morning daylight",
+ width: 1264,
+ height: 848,
+ },
+ {
+ id: "plate-architecture",
+ role: "examples rail · architecture / studio",
+ subject:
+ "an architect's sketch of a window detail on cream tracing paper, " +
+ "pencil dimensions and arrows, a coral measuring tape partly coiled " +
+ "alongside, a brass scale-rule below. a studio bench mid-thought",
+ width: 1264,
+ height: 848,
+ },
+ {
+ id: "plate-manifesto",
+ role: "examples rail · manifesto",
+ subject:
+ "a single sheet of warm cream paper pinned to a cork board with a " +
+ "coral pushpin, three short declarative sentences hand-written in " +
+ "ink, one word underlined twice. edges curling. a manifesto",
+ width: 1264,
+ height: 848,
+ },
+];
+
+/* — Helpers ——————————————————————————————————————————— */
+
+async function loadEnv() {
+ let raw;
+ try {
+ raw = await fs.readFile(ENV_PATH, "utf8");
+ } catch (e) {
+ throw new Error(`.env.local not found at ${ENV_PATH}`);
+ }
+ for (const line of raw.split("\n")) {
+ const m = line.match(/^([A-Z_][A-Z0-9_]*)\s*=\s*(.*)$/);
+ if (m) process.env[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
+ }
+}
+
+function buildPrompt(subject) {
+ return `${STYLE_PREFIX} ${subject}.`;
+}
+
+async function callOnce(slot, { referenceUrl, apiKey }) {
+ const body = {
+ model: MODEL_ID,
+ prompt: buildPrompt(slot.subject),
+ width: slot.width,
+ height: slot.height,
+ seed: SEED,
+ };
+ if (!slot.isReference && referenceUrl) {
+ body.reference_images = [referenceUrl];
+ }
+ const res = await fetch(API_URL, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ });
+ return res;
+}
+
+async function generateOne(slot, { referenceUrl } = {}) {
+ const apiKey = (process.env.TOGETHER_API_KEY || "").trim();
+ if (!apiKey) throw new Error("TOGETHER_API_KEY missing in .env.local");
+
+ const t0 = Date.now();
+
+ // Retry up to 3 times on 5xx server errors with backoff
+ let res;
+ let lastErr;
+ for (let attempt = 1; attempt <= 3; attempt++) {
+ res = await callOnce(slot, { referenceUrl, apiKey });
+ if (res.ok) break;
+ if (res.status >= 500 && res.status < 600) {
+ lastErr = await res.text();
+ if (attempt < 3) {
+ const wait = attempt * 8000;
+ console.log(` ! ${slot.id} got ${res.status}; retrying in ${wait / 1000}s (attempt ${attempt + 1}/3)`);
+ await new Promise((r) => setTimeout(r, wait));
+ continue;
+ }
+ } else {
+ break;
+ }
+ }
+
+ if (!res.ok) {
+ const err = lastErr || (await res.text());
+ throw new Error(`Together AI ${res.status}: ${err.slice(0, 400)}`);
+ }
+ const json = await res.json();
+ const url = json?.data?.[0]?.url;
+ const b64 = json?.data?.[0]?.b64_json;
+ if (!url && !b64) {
+ throw new Error(`No image returned: ${JSON.stringify(json).slice(0, 300)}`);
+ }
+
+ let bytes;
+ if (url) {
+ const r = await fetch(url);
+ if (!r.ok) throw new Error(`Image fetch ${r.status} from ${url}`);
+ bytes = Buffer.from(await r.arrayBuffer());
+ } else {
+ bytes = Buffer.from(b64, "base64");
+ }
+
+ await fs.mkdir(OUT_DIR, { recursive: true });
+ const outPath = path.join(OUT_DIR, `${slot.id}.png`);
+ await fs.writeFile(outPath, bytes);
+
+ const ms = Date.now() - t0;
+ return { outPath, hostedUrl: url, sizeKb: Math.round(bytes.length / 1024), ms };
+}
+
+async function main() {
+ const args = process.argv.slice(2);
+ const slotArg = args.find((a) => a.startsWith("--slot"))?.split("=")[1] || args[args.indexOf("--slot") + 1];
+ const list = args.includes("--list");
+
+ if (list) {
+ console.log("Slots:");
+ for (const s of SLOTS) {
+ console.log(` ${s.id.padEnd(22)} ${s.width}×${s.height} ${s.role}`);
+ }
+ return;
+ }
+
+ await loadEnv();
+
+ let targets;
+ if (!slotArg || slotArg === "all") targets = SLOTS;
+ else if (slotArg === "reference") targets = SLOTS.filter((s) => s.isReference);
+ else targets = SLOTS.filter((s) => s.id === slotArg);
+
+ if (targets.length === 0) {
+ console.error(`No slot matches: ${slotArg}`);
+ console.error("Try --list");
+ process.exit(2);
+ }
+
+ // Ensure reference exists before any non-reference generation
+ let referenceUrl = null;
+ const refPath = path.join(OUT_DIR, "_seed-reference.png");
+ const needsRef = targets.some((s) => !s.isReference);
+ if (needsRef) {
+ try {
+ await fs.access(refPath);
+ // We need a *URL* for reference_images[]. Re-upload via Together AI's signed pattern,
+ // or use a publicly hosted version. For now, on first run we generate the reference
+ // via this script and use the hosted URL Together AI returns for it.
+ // If the local file exists but we don't have a URL cached, fall back to regenerating.
+ const cacheUrl = await fs.readFile(refPath + ".url", "utf8").catch(() => null);
+ if (cacheUrl) referenceUrl = cacheUrl.trim();
+ } catch {}
+ if (!referenceUrl) {
+ console.log("→ generating reference (no cached URL)…");
+ const refSlot = SLOTS.find((s) => s.isReference);
+ const result = await generateOne(refSlot);
+ referenceUrl = result.hostedUrl;
+ if (referenceUrl) {
+ await fs.writeFile(refPath + ".url", referenceUrl);
+ }
+ console.log(` ✓ ${refSlot.id} → ${result.outPath} (${result.sizeKb} KB, ${result.ms}ms)`);
+ }
+ }
+
+ const skipExisting = !args.includes("--force");
+
+ for (const slot of targets) {
+ const slotPath = path.join(OUT_DIR, `${slot.id}.png`);
+ if (skipExisting) {
+ try {
+ const stat = await fs.stat(slotPath);
+ if (stat.size > 0) {
+ console.log(` · ${slot.id} → exists (${Math.round(stat.size / 1024)} KB) — skipping`);
+ continue;
+ }
+ } catch {}
+ }
+ if (slot.isReference && needsRef === false) {
+ const result = await generateOne(slot);
+ if (result.hostedUrl) await fs.writeFile(refPath + ".url", result.hostedUrl);
+ console.log(` ✓ ${slot.id} → ${result.outPath} (${result.sizeKb} KB, ${result.ms}ms)`);
+ continue;
+ }
+ if (slot.isReference) continue; // already handled above
+ try {
+ const result = await generateOne(slot, { referenceUrl });
+ console.log(` ✓ ${slot.id} → ${result.outPath} (${result.sizeKb} KB, ${result.ms}ms)`);
+ } catch (e) {
+ console.log(` ✗ ${slot.id} FAILED: ${e.message}`);
+ // continue with the next slot rather than aborting the whole batch
+ }
+ }
+
+ console.log("\nDone.");
+}
+
+main().catch((e) => {
+ console.error("FAIL:", e.message);
+ process.exit(1);
+});
diff --git a/site/css/components.css b/site/css/components.css
index b408217..60a9dd2 100644
--- a/site/css/components.css
+++ b/site/css/components.css
@@ -4386,3 +4386,142 @@ html[data-theme="halo"] aside {
[data-theme="manifesto"] .code {
border-width: 1.5px;
}
+
+/* ──────────────────────────────────────────────────────────────
+ EDITORIAL THEME — open-design-inspired structural overrides (v0.9.0)
+
+ The 23rd theme. Editorial-premium voice: mixed serif italic
+ inside sans display, Roman-numeral marginalia, hairline rules,
+ asymmetric grids. Inspired by github.com/nexu-io/open-design.
+ ────────────────────────────────────────────────────────────── */
+
+/* Mixed display: italic Playfair word inside Inter Tight headline */
+[data-theme="editorial"] .hero__display em,
+[data-theme="editorial"] .section__title em,
+[data-theme="editorial"] .opening__title em {
+ font-family: var(--font-serif);
+ font-style: italic;
+ font-weight: 500;
+ letter-spacing: -0.02em;
+}
+
+/* Roman-numeral section heads — replaces the leading number */
+[data-theme="editorial"] .section-label .num {
+ font-family: var(--font-serif);
+ font-style: italic;
+ font-weight: 500;
+ letter-spacing: 0;
+}
+[data-theme="editorial"] .section-label .divider {
+ opacity: 0.55;
+}
+
+/* Quieter rules and accents */
+[data-theme="editorial"] .section__head {
+ border-bottom-color: var(--color-rule);
+ border-bottom-width: 0.5px;
+}
+
+/* Tighten body in editorial — Inter is dense, less line-height needed */
+[data-theme="editorial"] .examples__intro,
+[data-theme="editorial"] .study__intro,
+[data-theme="editorial"] .install-pane__next {
+ font-family: var(--font-body);
+}
+
+/* ──────────────────────────────────────────────────────────────
+ OPEN-DESIGN STEALS — reusable utilities (v0.9.0)
+
+ Six patterns extracted from open-design. NOT theme-only — these
+ are utilities any theme can use. Skill rules cross-reference them
+ in component-cookbook.md and microinteractions.md.
+ ────────────────────────────────────────────────────────────── */
+
+/* — Tabular numerals utility ———————————————————————————————
+ Apply to anything that mixes numbers with text — versions, dates,
+ stats, code stamps — so digit widths align like a ledger. */
+.tnum {
+ font-variant-numeric: tabular-nums;
+ font-feature-settings: "tnum" 1;
+}
+
+/* — Corner-bracket image annotation —————————————————————————
+ Wrap any image with .fig-corners; four 18px brackets appear at
+ the corners. Lightweight alternative to a full frame. */
+.fig-corners {
+ position: relative;
+ display: block;
+}
+.fig-corners::before,
+.fig-corners::after,
+.fig-corners > .fig-corners__br,
+.fig-corners > .fig-corners__bl {
+ content: "";
+ position: absolute;
+ width: 18px;
+ height: 18px;
+ border: 1px solid var(--color-rule-2);
+ pointer-events: none;
+}
+.fig-corners::before { top: -4px; left: -4px; border-right: 0; border-bottom: 0; }
+.fig-corners::after { top: -4px; right: -4px; border-left: 0; border-bottom: 0; }
+.fig-corners > .fig-corners__bl { bottom: -4px; left: -4px; border-right: 0; border-top: 0; }
+.fig-corners > .fig-corners__br { bottom: -4px; right: -4px; border-left: 0; border-top: 0; }
+
+/* — Pulse animation utility ————————————————————————————————
+ Subtle ambient breathing pulse for status dots / live indicators.
+ 2.4s ease-in-out, opacity-only. Reduced-motion fallback is steady. */
+.pulse {
+ animation: pulse-breathe 2.4s var(--ease-in-out, ease-in-out) infinite;
+}
+@keyframes pulse-breathe {
+ 0%, 100% { opacity: 0.45; }
+ 50% { opacity: 1; }
+}
+@media (prefers-reduced-motion: reduce) {
+ .pulse { animation: none; opacity: 0.85; }
+}
+
+/* — Dark-slab section reversal —————————————————————————————
+ Wrap any .section in `.section--invert` to flip paper/ink colours
+ for that one section. Useful for breaking page rhythm. */
+.section--invert {
+ background: var(--color-ink);
+ color: var(--color-paper);
+ margin-inline: calc(-1 * var(--page-gutter));
+ padding-inline: var(--page-gutter);
+ padding-block: var(--space-2xl);
+}
+.section--invert .section__title,
+.section--invert .section-label,
+.section--invert .section__head { color: var(--color-paper); }
+.section--invert .section__head { border-bottom-color: oklch(50% 0.02 60 / 0.4); }
+
+/* — Asymmetric hero ratio knob ——————————————————————————————
+ Default split-hero ratio is 50/50; this knob swaps it to 0.78/1.22
+ per the open-design move. Apply to `.hero--split[data-ratio="asym"]`. */
+.hero--split[data-ratio="asym"] {
+ grid-template-columns: 0.78fr 1.22fr;
+}
+
+/* — Roman-numeral section-label variant —————————————————————
+ `XII` rendered in italic serif. */
+.section-label .num--roman {
+ font-family: var(--font-serif);
+ font-style: italic;
+ font-weight: 500;
+ letter-spacing: 0;
+}
+
+/* — Smart sticky nav (reveal on scroll-up) ———————————————————
+ Apply to .banner with class `banner--smart`. Hidden when scrolling
+ down, slid back in when scrolling up. JS toggles the class on scroll. */
+.banner--smart {
+ transition: transform 220ms var(--ease-out);
+}
+.banner--smart.is-hidden {
+ transform: translateY(-100%);
+}
+@media (prefers-reduced-motion: reduce) {
+ .banner--smart { transition: none; }
+}
diff --git a/site/css/sections.css b/site/css/sections.css
index 54fcce2..fb1aa98 100644
--- a/site/css/sections.css
+++ b/site/css/sections.css
@@ -1136,3 +1136,186 @@ html[data-theme="almanac"] body {
font-style: italic;
color: var(--color-accent-ink);
}
+
+/* — Watercolor sprinkle (v0.9.0) ——————————————————————————
+ Generated watercolor + ink-wash plates, locked to one seed + reference.
+ Each image is a CONCEPT visualization for its section, never decoration.
+ Theme-aware: warm-paper themes show full presence; dark themes show
+ subtle screen-blend trace; high-contrast themes desaturate. */
+
+.wc {
+ position: relative;
+ overflow: hidden;
+ pointer-events: none;
+ isolation: isolate;
+}
+
+.wc img,
+.wc picture {
+ display: block;
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ mix-blend-mode: var(--wc-blend, multiply);
+ opacity: var(--wc-opacity, 0.92);
+ filter: var(--wc-filter, none);
+}
+
+/* Default per-theme treatments — light/warm-paper themes get full presence */
+[data-theme="specimen"] .wc img,
+[data-theme="atelier"] .wc img,
+[data-theme="newsprint"] .wc img,
+[data-theme="salon"] .wc img,
+[data-theme="linen"] .wc img,
+[data-theme="garden"] .wc img,
+[data-theme="almanac"] .wc img,
+[data-theme="studio"] .wc img,
+[data-theme="riso"] .wc img,
+[data-theme="plume"] .wc img,
+[data-theme="editorial"] .wc img {
+ --wc-blend: multiply;
+ --wc-opacity: 0.95;
+}
+
+/* Dark-paper themes — invert + screen-blend for ghostly trace */
+[data-theme="midnight"] .wc img,
+[data-theme="bloom"] .wc img,
+[data-theme="aurora"] .wc img,
+[data-theme="halo"] .wc img,
+[data-theme="terminal"] .wc img {
+ --wc-blend: screen;
+ --wc-opacity: 0.18;
+ --wc-filter: invert(1) hue-rotate(180deg) saturate(0.6);
+}
+
+/* High-contrast themes — desaturate */
+[data-theme="brutal"] .wc img,
+[data-theme="manifesto"] .wc img,
+[data-theme="sport"] .wc img {
+ --wc-blend: multiply;
+ --wc-opacity: 0.65;
+ --wc-filter: grayscale(0.85) contrast(1.05);
+}
+
+/* Modern-minimal — subtle */
+[data-theme="quiet"] .wc img,
+[data-theme="pastel"] .wc img,
+[data-theme="coral"] .wc img,
+[data-theme="violet"] .wc img {
+ --wc-opacity: 0.75;
+ --wc-filter: saturate(0.65);
+}
+
+/* — Slot 1: Hero ambient backdrop ————————————————————— */
+.slot--hero {
+ position: relative;
+ isolation: isolate;
+}
+.slot--hero::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: url("../img/generated/hero-bg.png") center/cover no-repeat;
+ opacity: var(--hero-bg-opacity, 0.20);
+ mix-blend-mode: var(--hero-bg-blend, multiply);
+ pointer-events: none;
+}
+[data-theme="midnight"] .slot--hero::before,
+[data-theme="bloom"] .slot--hero::before,
+[data-theme="aurora"] .slot--hero::before,
+[data-theme="halo"] .slot--hero::before,
+[data-theme="terminal"] .slot--hero::before {
+ --hero-bg-opacity: 0.10;
+ --hero-bg-blend: screen;
+ filter: invert(1) hue-rotate(180deg) saturate(0.6);
+}
+[data-theme="brutal"] .slot--hero::before,
+[data-theme="manifesto"] .slot--hero::before {
+ --hero-bg-opacity: 0.32;
+ filter: grayscale(0.95) contrast(1.05);
+}
+
+/* — Slot 2: Section transitions (full-bleed banners) ————— */
+.transition {
+ width: 100%;
+ height: clamp(180px, 22vw, 300px);
+ overflow: hidden;
+ margin-block: 0;
+ padding: 0;
+ border: none;
+}
+.transition .wc {
+ width: 100%;
+ height: 100%;
+}
+
+/* — Slot 3: Section 03 (Study) ambient backdrop ————————— */
+#before-after,
+#study {
+ position: relative;
+ isolation: isolate;
+}
+#study::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: url("../img/generated/study-bg.png") center/cover no-repeat;
+ opacity: 0.14;
+ mix-blend-mode: multiply;
+ pointer-events: none;
+}
+[data-theme="midnight"] #study::before,
+[data-theme="bloom"] #study::before,
+[data-theme="aurora"] #study::before,
+[data-theme="halo"] #study::before,
+[data-theme="terminal"] #study::before {
+ opacity: 0.08;
+ mix-blend-mode: screen;
+ filter: invert(1) hue-rotate(180deg) saturate(0.6);
+}
+
+/* — Slot 4: Foundations panel corner plates ———————————— */
+.found-panel { position: relative; }
+.found-panel__plate {
+ position: absolute;
+ top: var(--space-md);
+ right: var(--space-md);
+ width: clamp(80px, 11vw, 132px);
+ height: clamp(80px, 11vw, 132px);
+ pointer-events: none;
+ z-index: 0;
+}
+.found-panel > * { position: relative; z-index: 1; }
+.found-panel__plate { z-index: 0; }
+
+@media (max-width: 60rem) {
+ .found-panel__plate {
+ width: 72px;
+ height: 72px;
+ top: var(--space-2xs);
+ right: var(--space-2xs);
+ }
+}
+
+/* — Slot 5: Install harness icons ————————————————————— */
+.harness__plate {
+ flex: 0 0 auto;
+ width: 56px;
+ height: 56px;
+ margin-right: var(--space-sm);
+}
+
+/* — Slot 6: Footer mark — sits above the footer slot ———— */
+.foot-mark {
+ display: block;
+ width: clamp(64px, 8vw, 96px);
+ height: clamp(64px, 8vw, 96px);
+ margin: var(--space-2xl) auto 0;
+ pointer-events: none;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .wc img { transition: none; }
+}
diff --git a/site/css/tokens.css b/site/css/tokens.css
index b6e9356..131e0d0 100644
--- a/site/css/tokens.css
+++ b/site/css/tokens.css
@@ -1095,4 +1095,45 @@
--shadow-card: 0 8px 22px -12px oklch(13% 0.02 60 / 0.5); }
[data-theme="plume"] { --radius-card: 14px; --radius-pill: 999px; --radius-input: 10px;
--shadow-card: 0 10px 28px -12px oklch(45% 0.10 18 / 0.16); }
+[data-theme="editorial"] { --radius-card: 0; --radius-pill: 0; --radius-input: 0; --rule-card: 0.5px; }
+
+
+/* ──────────────────────────────────────────────────────────────
+ EDITORIAL — open-design-inspired editorial premium · the 23rd theme
+ Warm cream paper + coral accent + Inter Tight display with
+ Playfair Display italic emphasis inside. Magazine-shaped: hairline
+ rules, asymmetric grids, Roman-numeral marginalia, 2-axis serif/sans
+ pairing. Inspired by github.com/nexu-io/open-design.
+ ────────────────────────────────────────────────────────────── */
+
+[data-theme="editorial"] {
+ --color-paper: oklch(94% 0.020 75); /* warm cream, slightly cooler than Specimen */
+ --color-paper-2: oklch(91% 0.022 72); /* card backs */
+ --color-paper-3: oklch(87% 0.024 70); /* dark-slab fallback */
+ --color-rule: oklch(80% 0.018 70); /* hairlines */
+ --color-rule-2: oklch(64% 0.014 68);
+ --color-muted: oklch(48% 0.014 60);
+ --color-neutral: oklch(34% 0.012 55);
+ --color-ink-2: oklch(28% 0.014 55);
+ --color-ink: oklch(15% 0.014 280); /* near-black with cool tilt */
+ --color-accent: oklch(60% 0.160 35); /* coral */
+ --color-accent-ink: oklch(98% 0.005 70);
+ --color-focus: oklch(62% 0.160 35);
+
+ --font-display: "Inter Tight", "Inter", ui-sans-serif, system-ui, sans-serif;
+ --font-body: "Inter", ui-sans-serif, system-ui, sans-serif;
+ --font-serif: "Playfair Display", "Fraunces", ui-serif, Georgia, serif;
+ --font-label: "Geist Mono", "JetBrains Mono", ui-monospace, monospace;
+ --font-mono: "Geist Mono", "JetBrains Mono", ui-monospace, monospace;
+
+ --display-weight: 800;
+ --display-style: normal;
+
+ --text-display: clamp(2.5rem, 5vw + 0.75rem, 4.75rem);
+ --text-display-s: clamp(1.875rem, 3vw + 0.5rem, 2.875rem);
+
+ --tracking-display: -0.04em; /* tight, magazine-shaped */
+ --tracking-tight: -0.018em;
+ --tracking-label: 0.10em;
+}
diff --git a/site/img/generated/_contact-sheet.html b/site/img/generated/_contact-sheet.html
new file mode 100644
index 0000000..a8f2b6d
--- /dev/null
+++ b/site/img/generated/_contact-sheet.html
@@ -0,0 +1,121 @@
+
+
+
+
+Hallmark · watercolor sprinkle · contact sheet
+
+
+
+
+
Hallmark · watercolor sprinkle · 18 plates
+
Seed 7727 · model google/gemini-3-pro-image · all share the locked reference (_seed-reference.png) · cost ≈ $2.41
+
+
+
+
+
+ _seed-reference locked style anchor (passed as reference_images[0] on every other call)
+
+
+
+
+ hero-bg §01 Hero · ambient backdrop · the rulebook itself
+
+
+
+
+ transition-02-03 between Examples → Study · eight folded papers
+
+
+
+
+ study-bg §03 Study · diagnostic overlay on a printed page
+
+
+
+
+ transition-04-05 between Without/With → Foundations · draft vs final
+
+
+
+
+ found-type §05 Type · serif G with construction grid
+
+
+
+
+ found-colour §05 Colour · single coral wash at 3% footprint
+
+
+
+
+ found-space §05 Space · architect's freehand floor plan
+
+
+
+
+ found-motion §05 Motion · Muybridge brushstroke strip
+
+
+
+
+ found-voice §05 Voice · marginalia, three honest words
+
+
+
+
+ harness-claude §07 Install · Claude Code = sealed letter
+
+
+
+
+ harness-cursor §07 Install · Cursor = brass drafting compass
+
+
+
+
+ harness-codex §07 Install · Codex = a literal medieval codex
+
+
+
+
+ footer-mark footer · wax-seal colophon mark
+
+
+
+
+ plate-podcast examples plate · indie podcast (Tide)
+
+
+
+
+ plate-bakery examples plate · bakery (Maple)
+
+
+
+
+ plate-architecture examples plate · architecture studio
+
+
+
+
+ plate-manifesto examples plate · manifesto
+
+
+
+
+
+
diff --git a/site/img/generated/_seed-reference.png b/site/img/generated/_seed-reference.png
new file mode 100644
index 0000000..e917269
Binary files /dev/null and b/site/img/generated/_seed-reference.png differ
diff --git a/site/img/generated/_seed-reference.png.url b/site/img/generated/_seed-reference.png.url
new file mode 100644
index 0000000..fa847b5
--- /dev/null
+++ b/site/img/generated/_seed-reference.png.url
@@ -0,0 +1 @@
+https://api.together.ai/shrt/stxmUP34xyTEyg7d
\ No newline at end of file
diff --git a/site/img/generated/footer-mark.png b/site/img/generated/footer-mark.png
new file mode 100644
index 0000000..06a3151
Binary files /dev/null and b/site/img/generated/footer-mark.png differ
diff --git a/site/img/generated/found-colour.png b/site/img/generated/found-colour.png
new file mode 100644
index 0000000..c44bb83
Binary files /dev/null and b/site/img/generated/found-colour.png differ
diff --git a/site/img/generated/found-motion.png b/site/img/generated/found-motion.png
new file mode 100644
index 0000000..b2b137b
Binary files /dev/null and b/site/img/generated/found-motion.png differ
diff --git a/site/img/generated/found-space.png b/site/img/generated/found-space.png
new file mode 100644
index 0000000..9bf92c7
Binary files /dev/null and b/site/img/generated/found-space.png differ
diff --git a/site/img/generated/found-type.png b/site/img/generated/found-type.png
new file mode 100644
index 0000000..a32f64d
Binary files /dev/null and b/site/img/generated/found-type.png differ
diff --git a/site/img/generated/found-voice.png b/site/img/generated/found-voice.png
new file mode 100644
index 0000000..377e3b6
Binary files /dev/null and b/site/img/generated/found-voice.png differ
diff --git a/site/img/generated/harness-claude.png b/site/img/generated/harness-claude.png
new file mode 100644
index 0000000..023b97e
Binary files /dev/null and b/site/img/generated/harness-claude.png differ
diff --git a/site/img/generated/harness-codex.png b/site/img/generated/harness-codex.png
new file mode 100644
index 0000000..98989f9
Binary files /dev/null and b/site/img/generated/harness-codex.png differ
diff --git a/site/img/generated/harness-cursor.png b/site/img/generated/harness-cursor.png
new file mode 100644
index 0000000..ed22119
Binary files /dev/null and b/site/img/generated/harness-cursor.png differ
diff --git a/site/img/generated/hero-bg.png b/site/img/generated/hero-bg.png
new file mode 100644
index 0000000..74585eb
Binary files /dev/null and b/site/img/generated/hero-bg.png differ
diff --git a/site/img/generated/plate-architecture.png b/site/img/generated/plate-architecture.png
new file mode 100644
index 0000000..5bc7c52
Binary files /dev/null and b/site/img/generated/plate-architecture.png differ
diff --git a/site/img/generated/plate-bakery.png b/site/img/generated/plate-bakery.png
new file mode 100644
index 0000000..2f9ed4d
Binary files /dev/null and b/site/img/generated/plate-bakery.png differ
diff --git a/site/img/generated/plate-manifesto.png b/site/img/generated/plate-manifesto.png
new file mode 100644
index 0000000..4a54f98
Binary files /dev/null and b/site/img/generated/plate-manifesto.png differ
diff --git a/site/img/generated/plate-podcast.png b/site/img/generated/plate-podcast.png
new file mode 100644
index 0000000..96cc3ed
Binary files /dev/null and b/site/img/generated/plate-podcast.png differ
diff --git a/site/img/generated/study-bg.png b/site/img/generated/study-bg.png
new file mode 100644
index 0000000..7ab76e6
Binary files /dev/null and b/site/img/generated/study-bg.png differ
diff --git a/site/img/generated/transition-02-03.png b/site/img/generated/transition-02-03.png
new file mode 100644
index 0000000..ce310a1
Binary files /dev/null and b/site/img/generated/transition-02-03.png differ
diff --git a/site/img/generated/transition-04-05.png b/site/img/generated/transition-04-05.png
new file mode 100644
index 0000000..03e0d68
Binary files /dev/null and b/site/img/generated/transition-04-05.png differ
diff --git a/site/index.html b/site/index.html
index d17125c..8e41e13 100644
--- a/site/index.html
+++ b/site/index.html
@@ -18,11 +18,11 @@
-
+
+ href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,700;12..96,800&family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;0,9..144,600;1,9..144,400;1,9..144,500&family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&family=Inter:wght@400;500;600;700&family=Inter+Tight:wght@500;600;700;800&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&family=Playfair+Display:ital,wght@1,400;1,500;1,600&display=swap" />
@@ -46,7 +46,7 @@
Hallmark
- v0.8
+ v0.9Install
@@ -108,6 +108,8 @@
style="--dot: oklch(13% 0.005 60); --dot-edge: oklch(74% 0.16 70);">
+
@@ -148,7 +150,7 @@
-
Each page is hand-built HTML and CSS — no shared theme, no shared layout. Click any card to open the full page →
+
No shared theme. No shared layout. Open any card →
@@ -307,6 +309,13 @@
+
+
+
+
+
+
+
@@ -319,7 +328,7 @@
- hallmark study reads a design's structure, not its pixels. Below, two Hallmark-built pages put through the same DNA extraction the verb runs on user-supplied references. The screenshot is the page; the panel beside it is the structured DNA.
+ hallmark study reads a design's structure, not its pixels. Screenshot left, structured DNA right.
@@ -539,6 +548,13 @@
+
+
+
+
+
+
+
@@ -589,7 +605,10 @@
-
Pick fonts on purpose. Skip Inter and Geist — the defaults every site uses. Pair a display face that has a personality with a clean body face, and lean into the extreme weights, light or heavy, not the safe middle.
+
+
+
+
Pick fonts on purpose. Skip the defaults. Pair a display with personality and a clean body, and lean into extreme weights — not the safe middle.
HallmarkFraunces · 900 italic
HallmarkGeist · 300
@@ -598,7 +617,10 @@
-
One accent colour, used so sparingly you almost miss it — less than 3% of what's on screen. Skip pure black and pure white (they're harsher than they look). Avoid purple gradients.
+
+
+
+
One accent, used so sparingly you almost miss it — under 3% of the screen. Skip pure black and pure white. Avoid purple gradients.
One accent. The whole field is paper. The dot is the accent.
@@ -613,7 +635,10 @@
-
Generous space, used unevenly. Pick one consistent scale so things feel related. Asymmetry on purpose, never by accident — centred-everything is a tell.
+
+
+
+
Generous space, used unevenly. One consistent scale. Asymmetry on purpose — centred-everything is a tell.
@@ -630,6 +655,9 @@
+
+
+
Keep motion quiet. One smooth entrance feels designed; ten little bounces feel busy. Animate fade and slide — never size, colour, or layout.