v0.9.0 — Editorial theme · watercolor sprinkle · generated-imagery skill rules
Three workstreams shipped together: A · Editorial theme (#23) — open-design-inspired premium - Warm cream paper · coral accent · Inter Tight 800 + Playfair italic emphasis - Mixed serif/sans inside display headings (em → Playfair italic) - Roman-numeral marginalia, hairline rules, asymmetric grids - Banner dot, COPY entry, ARCHETYPES + GENRES wired - Joins editorial cluster · catalog 22 → 23 B · Marketing-site watercolor sprinkle (18 plates · ~$2.50 generation cost) - One seeded reference (_seed-reference.png · seed=7727 · google/gemini-3-pro-image) passed via reference_images[] on every subsequent call so every plate shares one hand - Each subject is a CONCEPT VISUALIZATION for its section (Contra Labs lesson): hero = the rulebook · transitions = eight folded papers / draft+final cards · foundations panels = serif G with grid · 4-wash colour study · plan · brushstroke · marginalia · install harnesses = sealed letter · drafting compass · medieval codex · footer = wax-seal mark · plates = bakery / podcast / architecture / manifesto - Theme-aware blend modes: warm-paper themes get full presence (multiply, 0.95); dark themes get screen-blend ghost trace (invert + hue-rotate, 0.18); high-contrast themes get desaturated multiply - Less-text refactor on Examples + Study + Install copy C · Skill image-generation system - skill/references/generated-imagery.md (NEW) — the seed-and-reference rule, 23-theme style-prompt table, post-processing checklist (grain · asymmetric crop · accent-shift · optional frame), provenance stamp, three user paths (packs · own-key recipe · planned free-trial endpoint) - hero-enrichment.md Tier C rewritten — promoted from "last resort" to peer of Tier B when seed-and-reference + post-processing discipline followed - assets.md Tier C catalog updated — Gemini 3 Pro Image / Nano Banana Pro listed as the 2026 default, hosted via Together AI ($0.134/2K) - anti-patterns.md "Raw generated stills" added to Critical bucket - skill/_assets/scripts/generate.mjs (NEW) — drop-in 200-line Node script users copy into their project; reads TOGETHER_API_KEY from .env.local, looks up locked theme prompt, generates seed reference on first run + caches URL, writes ./img/generated/ D · Open-design steal utilities (any theme can use) - Roman-numeral section-head variant (.num--roman) - Corner-bracket image annotation (.fig-corners + 4 corner spans) - Dark-slab section reversal (.section--invert) - Pulse animation (.pulse, 2.4s opacity breathe, reduced-motion fallback) - .tnum tabular-numerals utility (applied to banner version + dates) - Smart-hide-on-scroll banner stub (.banner--smart) E · Catalog count refresh - 22 themes → 23 across SKILL.md, custom-theme.md, structure.md fingerprint table, README.md, site/index.html, site/js/main.js (HERO_TITLE comment, COPY) - Version 0.8.0 → 0.9.0 in SKILL.md frontmatter, banner version badge, dense colophon stat line, README Generation pipeline pieces: - scripts/generate-marketing.mjs — one-shot script with seed-locking + retry on 5xx + skip-if-exists. Generated all 18 plates including 1 reference + 17 sprinkles. - .env.local + .gitignore set up. Key never echoed. Deferred to follow-up before launch: - Per-theme pre-generated packs (4 imgs × 23 themes = ~$13 · ~20 min) - /api/generate Vercel Edge Function with IP rate-limit + $5/day budget cap - "Try one" button on the marketing site Total cost so far: ~$2.50 in Together AI image generation.
@@ -23,3 +23,11 @@ build/
|
||||
.cache/
|
||||
*.log
|
||||
.vercel
|
||||
|
||||
# Environment + secrets
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Generation pipeline artifacts (raw outputs before commit)
|
||||
.image-cache/
|
||||
|
||||
@@ -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.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
|
||||
@@ -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/<slot-id>.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);
|
||||
});
|
||||
@@ -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 —————————————————————
|
||||
`<span class="num num--roman">XII</span>` 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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Hallmark · watercolor sprinkle · contact sheet</title>
|
||||
<style>
|
||||
:root { --paper: oklch(95% 0.018 75); --ink: oklch(20% 0.020 60); --rule: oklch(78% 0.020 60); --accent: oklch(48% 0.140 40); }
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: "Fraunces", Georgia, serif; background: var(--paper); color: var(--ink); margin: 0; padding: 3rem 2rem; line-height: 1.55; }
|
||||
h1 { font-style: italic; font-size: 2rem; margin: 0 0 0.5rem; }
|
||||
.meta { color: oklch(45% 0.020 55); font-size: 0.875rem; margin: 0 0 2rem; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 2rem; max-width: 96rem; margin: 0 auto; }
|
||||
figure { margin: 0; display: grid; gap: 0.5rem; }
|
||||
figure img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; background: oklch(92% 0.015 75); border: 0.5px solid var(--rule); }
|
||||
figure[data-square] img { aspect-ratio: 1 / 1; }
|
||||
figcaption { font-size: 0.875rem; }
|
||||
.id { font-family: "JetBrains Mono", monospace; color: var(--accent); font-size: 0.75rem; letter-spacing: 0.04em; }
|
||||
.role { color: oklch(45% 0.020 55); margin-top: 0.2em; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Hallmark · watercolor sprinkle · 18 plates</h1>
|
||||
<p class="meta">Seed 7727 · model google/gemini-3-pro-image · all share the locked reference (_seed-reference.png) · cost ≈ $2.41</p>
|
||||
|
||||
<div class="grid">
|
||||
|
||||
<figure>
|
||||
<img src="_seed-reference.png" alt="">
|
||||
<figcaption><span class="id">_seed-reference</span><br>locked style anchor (passed as reference_images[0] on every other call)</figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="hero-bg.png" alt="">
|
||||
<figcaption><span class="id">hero-bg</span><br><span class="role">§01 Hero · ambient backdrop · the rulebook itself</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="transition-02-03.png" alt="">
|
||||
<figcaption><span class="id">transition-02-03</span><br><span class="role">between Examples → Study · eight folded papers</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="study-bg.png" alt="">
|
||||
<figcaption><span class="id">study-bg</span><br><span class="role">§03 Study · diagnostic overlay on a printed page</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="transition-04-05.png" alt="">
|
||||
<figcaption><span class="id">transition-04-05</span><br><span class="role">between Without/With → Foundations · draft vs final</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="found-type.png" alt="">
|
||||
<figcaption><span class="id">found-type</span><br><span class="role">§05 Type · serif G with construction grid</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="found-colour.png" alt="">
|
||||
<figcaption><span class="id">found-colour</span><br><span class="role">§05 Colour · single coral wash at 3% footprint</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="found-space.png" alt="">
|
||||
<figcaption><span class="id">found-space</span><br><span class="role">§05 Space · architect's freehand floor plan</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="found-motion.png" alt="">
|
||||
<figcaption><span class="id">found-motion</span><br><span class="role">§05 Motion · Muybridge brushstroke strip</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="found-voice.png" alt="">
|
||||
<figcaption><span class="id">found-voice</span><br><span class="role">§05 Voice · marginalia, three honest words</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="harness-claude.png" alt="">
|
||||
<figcaption><span class="id">harness-claude</span><br><span class="role">§07 Install · Claude Code = sealed letter</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="harness-cursor.png" alt="">
|
||||
<figcaption><span class="id">harness-cursor</span><br><span class="role">§07 Install · Cursor = brass drafting compass</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="harness-codex.png" alt="">
|
||||
<figcaption><span class="id">harness-codex</span><br><span class="role">§07 Install · Codex = a literal medieval codex</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure data-square>
|
||||
<img src="footer-mark.png" alt="">
|
||||
<figcaption><span class="id">footer-mark</span><br><span class="role">footer · wax-seal colophon mark</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="plate-podcast.png" alt="">
|
||||
<figcaption><span class="id">plate-podcast</span><br><span class="role">examples plate · indie podcast (Tide)</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="plate-bakery.png" alt="">
|
||||
<figcaption><span class="id">plate-bakery</span><br><span class="role">examples plate · bakery (Maple)</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="plate-architecture.png" alt="">
|
||||
<figcaption><span class="id">plate-architecture</span><br><span class="role">examples plate · architecture studio</span></figcaption>
|
||||
</figure>
|
||||
|
||||
<figure>
|
||||
<img src="plate-manifesto.png" alt="">
|
||||
<figcaption><span class="id">plate-manifesto</span><br><span class="role">examples plate · manifesto</span></figcaption>
|
||||
</figure>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 146 KiB |
@@ -0,0 +1 @@
|
||||
https://api.together.ai/shrt/stxmUP34xyTEyg7d
|
||||
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 130 KiB |
@@ -18,11 +18,11 @@
|
||||
<link rel="preload" href="fonts/the-future-medium.woff2" as="font" type="font/woff2" crossorigin />
|
||||
<link rel="preload" href="fonts/the-future-mono-regular.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<!-- Google Fonts — Fraunces (Specimen + Studio italic display), Geist (Pastel), Bricolage + Newsreader (Riso). -->
|
||||
<!-- Google Fonts — Fraunces, Geist, Bricolage, Newsreader, plus Inter Tight + Playfair Display for Editorial. -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link rel="stylesheet"
|
||||
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=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&display=swap" />
|
||||
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" />
|
||||
|
||||
<link rel="stylesheet" href="css/tokens.css" />
|
||||
<link rel="stylesheet" href="css/base.css" />
|
||||
@@ -46,7 +46,7 @@
|
||||
<header class="banner" role="banner">
|
||||
<a class="banner__mark" href="#top" aria-label="Hallmark — top of page">
|
||||
<span class="banner__name">Hallmark</span>
|
||||
<span class="banner__ver">v0.8</span>
|
||||
<span class="banner__ver tnum">v0.9</span>
|
||||
</a>
|
||||
|
||||
<a class="banner__install" href="#install" aria-label="Jump to install section">Install</a>
|
||||
@@ -108,6 +108,8 @@
|
||||
style="--dot: oklch(13% 0.005 60); --dot-edge: oklch(74% 0.16 70);"></button>
|
||||
<button class="banner__dot" type="button" data-theme-btn="plume" aria-label="Plume"
|
||||
style="--dot: oklch(96% 0.020 70); --dot-edge: oklch(68% 0.130 18);"></button>
|
||||
<button class="banner__dot" type="button" data-theme-btn="editorial" aria-label="Editorial"
|
||||
style="--dot: oklch(94% 0.020 75); --dot-edge: oklch(60% 0.160 35);"></button>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -148,7 +150,7 @@
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<p class="examples__intro">Each page is hand-built HTML and CSS — no shared theme, no shared layout. <span class="examples__hint">Click any card to open the full page →</span></p>
|
||||
<p class="examples__intro">No shared theme. No shared layout. <span class="examples__hint">Open any card →</span></p>
|
||||
|
||||
<div class="examples-rail" role="list" aria-label="Eight example landing pages">
|
||||
|
||||
@@ -307,6 +309,13 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- transition · examples → study -->
|
||||
<figure class="transition reveal" style="--i:1.5" aria-hidden="true">
|
||||
<div class="wc">
|
||||
<img src="img/generated/transition-02-03.png" alt="" loading="lazy" decoding="async" width="1264" height="848" />
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<!-- 03 · STUDY — what `study` extracts from a design -->
|
||||
<section class="section reveal" style="--i:2" id="study" aria-labelledby="s-study">
|
||||
<header class="section__head">
|
||||
@@ -319,7 +328,7 @@
|
||||
</header>
|
||||
|
||||
<p class="study__intro">
|
||||
<code>hallmark study</code> 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.
|
||||
<code>hallmark study</code> reads a design's structure, not its pixels. Screenshot left, structured DNA right.
|
||||
</p>
|
||||
|
||||
<div class="study-grid">
|
||||
@@ -539,6 +548,13 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- transition · without/with → foundations -->
|
||||
<figure class="transition reveal" style="--i:3.5" aria-hidden="true">
|
||||
<div class="wc">
|
||||
<img src="img/generated/transition-04-05.png" alt="" loading="lazy" decoding="async" width="1264" height="848" />
|
||||
</div>
|
||||
</figure>
|
||||
|
||||
<!-- 05 · FOUNDATIONS — vertical list + active panel (CSS-only tabs) -->
|
||||
<section class="section reveal" style="--i:4" aria-labelledby="s5-foundations">
|
||||
<header class="section__head">
|
||||
@@ -589,7 +605,10 @@
|
||||
<div class="found-panels">
|
||||
|
||||
<article class="found-panel" data-panel="type">
|
||||
<p class="found-panel__rule">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.</p>
|
||||
<div class="wc found-panel__plate" aria-hidden="true">
|
||||
<img src="img/generated/found-type.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
<p class="found-panel__rule">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.</p>
|
||||
<div class="found-panel__viz spec__viz spec__viz--type">
|
||||
<div class="spec__viz-row spec__viz-row--display"><span>Hallmark</span><em class="spec__viz-tag">Fraunces · 900 italic</em></div>
|
||||
<div class="spec__viz-row spec__viz-row--body"><span>Hallmark</span><em class="spec__viz-tag">Geist · 300</em></div>
|
||||
@@ -598,7 +617,10 @@
|
||||
</article>
|
||||
|
||||
<article class="found-panel" data-panel="colour">
|
||||
<p class="found-panel__rule">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.</p>
|
||||
<div class="wc found-panel__plate" aria-hidden="true">
|
||||
<img src="img/generated/found-colour.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
<p class="found-panel__rule">One accent, used so sparingly you almost miss it — under 3% of the screen. Skip pure black and pure white. Avoid purple gradients.</p>
|
||||
<div class="found-panel__viz spec__viz spec__viz--colour" aria-hidden="true">
|
||||
<span class="spec__viz-dot"></span>
|
||||
<p class="spec__viz-caption">One accent. The whole field is paper. The dot is the accent.</p>
|
||||
@@ -613,7 +635,10 @@
|
||||
</article>
|
||||
|
||||
<article class="found-panel" data-panel="space">
|
||||
<p class="found-panel__rule">Generous space, used unevenly. Pick one consistent scale so things feel related. Asymmetry on purpose, never by accident — centred-everything is a tell.</p>
|
||||
<div class="wc found-panel__plate" aria-hidden="true">
|
||||
<img src="img/generated/found-space.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
<p class="found-panel__rule">Generous space, used unevenly. One consistent scale. Asymmetry on purpose — centred-everything is a tell.</p>
|
||||
<div class="found-panel__viz spec__viz spec__viz--space" aria-hidden="true">
|
||||
<div class="spec__viz-grid spec__viz-grid--bad">
|
||||
<span class="vg-bar vg-bar--xl"></span>
|
||||
@@ -630,6 +655,9 @@
|
||||
</article>
|
||||
|
||||
<article class="found-panel" data-panel="motion">
|
||||
<div class="wc found-panel__plate" aria-hidden="true">
|
||||
<img src="img/generated/found-motion.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
<p class="found-panel__rule">Keep motion quiet. One smooth entrance feels designed; ten little bounces feel busy. Animate fade and slide — never size, colour, or layout.</p>
|
||||
<div class="found-panel__viz spec__viz spec__viz--motion">
|
||||
<button class="spec__viz-play" type="button" data-motion-demo aria-label="Play motion demo">
|
||||
@@ -643,7 +671,10 @@
|
||||
</article>
|
||||
|
||||
<article class="found-panel" data-panel="voice">
|
||||
<p class="found-panel__rule">Write like a person. Specific verbs over vague ones. Link text that makes sense out of context. Real apostrophes, not straight quotes. No "seamless," no "unleash," no "built for the modern team."</p>
|
||||
<div class="wc found-panel__plate" aria-hidden="true">
|
||||
<img src="img/generated/found-voice.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
<p class="found-panel__rule">Write like a person. Specific verbs. Link text that makes sense out of context. Real apostrophes. No "seamless," no "unleash," no "built for the modern team."</p>
|
||||
<div class="found-panel__viz spec__viz spec__viz--copy">
|
||||
<div class="spec__viz-copy-col" data-variant="bad">
|
||||
<p class="spec__viz-copy-label">Vague</p>
|
||||
@@ -742,16 +773,25 @@
|
||||
<p class="install-pane__label"><span class="install-pane__step">II</span> In</p>
|
||||
<ul class="install-pane__harnesses">
|
||||
<li class="harness">
|
||||
<div class="wc harness__plate" aria-hidden="true">
|
||||
<img src="img/generated/harness-claude.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
<span class="harness__name">Claude Code</span>
|
||||
<code class="harness__path">~/.claude/skills/hallmark/</code>
|
||||
<span class="harness__status harness__status--ok">auto-detected</span>
|
||||
</li>
|
||||
<li class="harness">
|
||||
<div class="wc harness__plate" aria-hidden="true">
|
||||
<img src="img/generated/harness-cursor.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
<span class="harness__name">Cursor</span>
|
||||
<code class="harness__path">.cursor/rules/hallmark.mdc</code>
|
||||
<span class="harness__status harness__status--ok">auto-detected</span>
|
||||
</li>
|
||||
<li class="harness">
|
||||
<div class="wc harness__plate" aria-hidden="true">
|
||||
<img src="img/generated/harness-codex.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
<span class="harness__name">Codex & others</span>
|
||||
<code class="harness__path">git clone · copy skill/</code>
|
||||
<span class="harness__status">manual</span>
|
||||
@@ -761,12 +801,17 @@
|
||||
|
||||
<div class="install-pane__row">
|
||||
<p class="install-pane__label"><span class="install-pane__step">III</span> Then</p>
|
||||
<p class="install-pane__next">Ask your agent for a UI. <code>hallmark</code> attaches itself when the prompt mentions design, audit, refine, redesign, or study — so the rules apply without you naming them.</p>
|
||||
<p class="install-pane__next">Ask your agent for a UI. <code>hallmark</code> attaches itself.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- footer mark · sits above the per-theme footer archetype -->
|
||||
<div class="wc foot-mark reveal" style="--i:6.5" aria-hidden="true">
|
||||
<img src="img/generated/footer-mark.png" alt="" loading="lazy" decoding="async" width="1024" height="1024" />
|
||||
</div>
|
||||
|
||||
<!-- FOOTER SLOT — archetype swaps per theme -->
|
||||
<footer class="slot slot--foot reveal" data-slot="footer" style="--i:7" role="contentinfo" aria-label="Colophon">
|
||||
</footer>
|
||||
@@ -968,7 +1013,7 @@
|
||||
<div class="foot__col">
|
||||
<p class="foot__col-head">Catalog</p>
|
||||
<ul>
|
||||
<li>22 themes</li>
|
||||
<li>23 themes</li>
|
||||
<li>21 macrostructures</li>
|
||||
<li>40 archetypes</li>
|
||||
<li>9 navs · 8 footers</li>
|
||||
@@ -981,7 +1026,7 @@
|
||||
<ul>
|
||||
<li><span class="foot__powered-mark">Together AI</span></li>
|
||||
<li>MIT licensed</li>
|
||||
<li>v0.8.0</li>
|
||||
<li>v0.9.0</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,7 @@ const THEMES = {
|
||||
aurora: "Aurora",
|
||||
halo: "Halo",
|
||||
plume: "Plume",
|
||||
editorial: "Editorial",
|
||||
};
|
||||
const STORAGE_KEY = "hallmark-theme";
|
||||
|
||||
@@ -77,6 +78,7 @@ const ARCHETYPES = {
|
||||
aurora: { hero: "marquee", footer: "masthead" },
|
||||
halo: { hero: "clipped", footer: "index" },
|
||||
plume: { hero: "clipped", footer: "masthead" },
|
||||
editorial: { hero: "split", footer: "masthead" },
|
||||
};
|
||||
|
||||
/* — Theme → genre map ——————————————————————————————————
|
||||
@@ -110,12 +112,14 @@ const THEME_GENRES = {
|
||||
// playful — post-Linear soft school (2 themes)
|
||||
pastel: "playful",
|
||||
plume: "playful",
|
||||
// editorial — open-design-inspired premium (added v0.9.0)
|
||||
editorial: "editorial",
|
||||
};
|
||||
|
||||
/* — Locked hero title —————————————————————————————————
|
||||
The H1 string is the same across every theme. Only the visual
|
||||
treatment swaps — italic vs roman, serif vs sans, all-caps vs not.
|
||||
The page is the demo: one sentence, twenty-two distinct designs. */
|
||||
The page is the demo: one sentence, twenty-three distinct designs. */
|
||||
const HERO_TITLE = "A design skill that refuses to look AI-generated.";
|
||||
|
||||
/* — Per-theme copy fixtures —————————————————————————————
|
||||
@@ -146,7 +150,7 @@ const COPY = {
|
||||
newsprint: {
|
||||
eyebrow: "Volume I · Issue 02 · 28 April 2026",
|
||||
title: HERO_TITLE,
|
||||
lede: "Twenty-two themes. Twenty-one named page shapes. Forty component archetypes — nine navs, eight footers, four hero polish patterns. A 55-gate slop test that gates every output before it ships. Hallmark is the rulebook the LLM never read.",
|
||||
lede: "Twenty-three themes. Twenty-one named page shapes. Forty component archetypes — nine navs, eight footers, four hero polish patterns. A 55-gate slop test that gates every output before it ships. Hallmark is the rulebook the LLM never read.",
|
||||
ctaLabel: "Distribution",
|
||||
proofLabel: "From the rule sheet",
|
||||
proofA: "Multi-column body, justified",
|
||||
@@ -258,7 +262,7 @@ const COPY = {
|
||||
quote: "An almanac is a book that knows where to look.",
|
||||
attrib: "Almanac, frontispiece",
|
||||
salutation: "Reference note,",
|
||||
letterBody: "This page is a reference, not an argument. The numbers are the point: 21 macrostructures, 40 archetypes, 22 themes, 55 slop-test gates. Cross-referenced so the next page Hallmark builds is genuinely different from the last.",
|
||||
letterBody: "This page is a reference, not an argument. The numbers are the point: 21 macrostructures, 40 archetypes, 23 themes, 55 slop-test gates. Cross-referenced so the next page Hallmark builds is genuinely different from the last.",
|
||||
signoff: "— editor",
|
||||
captionA: "Vol. III",
|
||||
captionB: "Plate 12",
|
||||
@@ -443,7 +447,7 @@ const COPY = {
|
||||
letterBody: "A theme for the modern enterprise page — the Stripe / Linear / ElevenLabs school of restraint. Clean white, confident typography, pill CTAs. Minimalism with conviction, not absence.",
|
||||
signoff: "Yours,",
|
||||
captionA: "Quiet",
|
||||
captionB: "v0.8",
|
||||
captionB: "v0.9",
|
||||
},
|
||||
bloom: {
|
||||
eyebrow: "Atmospheric · 2026",
|
||||
@@ -571,6 +575,27 @@ const COPY = {
|
||||
captionA: "Plume",
|
||||
captionB: "Late spring",
|
||||
},
|
||||
editorial: {
|
||||
eyebrow: "No XXIII · Editorial",
|
||||
title: HERO_TITLE,
|
||||
lede: "An editorial-premium voice — warm cream paper, coral accent, mixed sans + serif italic. Magazine-shaped, hairline rules, asymmetric grids. Inspired by open-design.",
|
||||
ctaLabel: "I · Install",
|
||||
proofLabel: "From the rule sheet",
|
||||
proofA: "Inter Tight 800 + Playfair italic, mixed in display",
|
||||
proofB: "Hairlines, generous whitespace, Roman-numeral marginalia",
|
||||
proofC: "Coral accent · ≤ 5% of viewport",
|
||||
cta: "Read the issue",
|
||||
stat: "23",
|
||||
qualifier: "the twenty-third theme. open-design-inspired.",
|
||||
mockStat: "23",
|
||||
quote: "A magazine page knows how much to leave out.",
|
||||
attrib: "Editorial, frontispiece",
|
||||
salutation: "Dear reader,",
|
||||
letterBody: "An editorial premium that takes its cue from the print magazines that still feel right — warm cream paper, hairline rules, Roman-numeral marginalia, an italic display word slipped inside a sans-serif headline. Coral the only colour besides ink. Asymmetric without being clever about it.",
|
||||
signoff: "Yours,",
|
||||
captionA: "Issue 23",
|
||||
captionB: "Spring 2026",
|
||||
},
|
||||
};
|
||||
|
||||
/* — Slot population ———————————————————————————————————— */
|
||||
@@ -609,10 +634,10 @@ function buildDenseColophon(themeName) {
|
||||
// template-string'd so it stays monospace-aligned.
|
||||
const today = "2026-05-01";
|
||||
return [
|
||||
`# hallmark · v0.8.0 · ${themeName.toLowerCase()}`,
|
||||
`# hallmark · v0.9.0 · ${themeName.toLowerCase()}`,
|
||||
`# build: ${today} · MIT · powered by together ai`,
|
||||
`#`,
|
||||
`# stats: 21 macros · 40 archetypes · 9 navs · 8 footers · 22 themes · 55 gates`,
|
||||
`# stats: 21 macros · 40 archetypes · 9 navs · 8 footers · 23 themes · 55 gates`,
|
||||
`# catalog (22 named) + custom (per-brand, opt-in)`,
|
||||
`#`,
|
||||
`# repo: github.com/Luffixos/hallmark`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: hallmark
|
||||
description: Use this skill when the user asks to design, build, redesign, audit, refine, or study a UI, web page, landing page, dashboard, component, or interface — or when they ask to make something "feel less AI-generated." Hallmark forces intentional design decisions (typography, color, layout, motion, interaction, structure) and refuses to default to the generic AI-UI template. Trigger phrases include "design a", "build a landing page", "make a dashboard", "redesign this site", "redesign the page", "refine this UI", "audit this design", "this looks AI-generated", "fix the design", "polish this", "give this a different look", and any request that will produce HTML / CSS / JSX / Tailwind output. **Also trigger when the user attaches a screenshot of a design they admire** — that is the `hallmark study` verb (extracts design DNA, never pixel-clones).
|
||||
version: 0.8.0
|
||||
version: 0.9.0
|
||||
---
|
||||
|
||||
# Hallmark
|
||||
@@ -30,7 +30,7 @@ Hallmark has one default behaviour and four explicit verbs.
|
||||
|
||||
If the user types anything that does not clearly map to `audit`, `refine`, `redesign`, or `study`, treat it as default. If the user attaches an image without a verb prefix, ask: *"Should I `study` this (extract the DNA), or should I treat it as a reference for a fresh build?"*
|
||||
|
||||
The default Design flow always picks a theme. By default it picks one of the **22 named themes** — the *catalog* — and rotates among them per the diversification rule. There is also a quiet *custom* branch that constructs a one-off OKLCH palette + free-font pairing for the brief; the custom route fires **only when the brief carries a creative-intent signal** (the user names a brand colour, names a multi-attribute vibe the catalog can't carry, or explicitly asks for a custom theme). For vanilla briefs, the user never sees the words "catalog" or "custom" — the catalog runs silently. See Step 1 (signal detection) and Step 2.6 (dispatch); the protocol lives in [`references/custom-theme.md`](references/custom-theme.md).
|
||||
The default Design flow always picks a theme. By default it picks one of the **23 named themes** — the *catalog* — and rotates among them per the diversification rule. There is also a quiet *custom* branch that constructs a one-off OKLCH palette + free-font pairing for the brief; the custom route fires **only when the brief carries a creative-intent signal** (the user names a brand colour, names a multi-attribute vibe the catalog can't carry, or explicitly asks for a custom theme). For vanilla briefs, the user never sees the words "catalog" or "custom" — the catalog runs silently. See Step 1 (signal detection) and Step 2.6 (dispatch); the protocol lives in [`references/custom-theme.md`](references/custom-theme.md).
|
||||
|
||||
---
|
||||
|
||||
@@ -196,7 +196,7 @@ If two non-default signals fire (rare), ask one short follow-up: *"This brief fi
|
||||
|
||||
State the genre out loud at Step 2.5 alongside the macrostructure and theme picks: *"Genre: atmospheric. Macrostructure: Marquee Hero. Theme: Bloom (atmospheric cluster)."*
|
||||
|
||||
**Theme route — only surface when the brief signals it.** Hallmark has two theme routes: **catalog** (the 22 named themes — Specimen, Atelier, Pastel, Brutal, Salon, Newsprint, Linen, Studio, Manifesto, Terminal, Midnight, Almanac, Garden, Quiet, Riso, Sport, Bloom, Coral, Violet, Aurora, Halo, Plume) and **custom** (an OKLCH palette + free-font pairing tuned to this one brief). **Catalog is the default.** The catalog rotation is *scoped to the genre's theme cluster* — atmospheric rotates Bloom/Midnight/Terminal, modern-minimal stays on Quiet, playful stays on Pastel, editorial walks the remaining twelve. Do **not** offer the user a choice on every prompt — that's friction, not discipline. Surface the catalog/custom fork only when the brief carries one of these signals:
|
||||
**Theme route — only surface when the brief signals it.** Hallmark has two theme routes: **catalog** (the 23 named themes — Specimen, Atelier, Pastel, Brutal, Salon, Newsprint, Linen, Studio, Manifesto, Terminal, Midnight, Almanac, Garden, Quiet, Riso, Sport, Bloom, Coral, Violet, Aurora, Halo, Plume, Editorial) and **custom** (an OKLCH palette + free-font pairing tuned to this one brief). **Catalog is the default.** The catalog rotation is *scoped to the genre's theme cluster* — atmospheric rotates Bloom/Midnight/Terminal, modern-minimal stays on Quiet, playful stays on Pastel, editorial walks the remaining twelve. Do **not** offer the user a choice on every prompt — that's friction, not discipline. Surface the catalog/custom fork only when the brief carries one of these signals:
|
||||
|
||||
- The user explicitly says **custom theme** / **tailored to our brand** / **make it ours** / **something unique** / **play with the colors and fonts**.
|
||||
- The user names a **specific brand colour** as the anchor (e.g., "use our terracotta", "the brand red is hex #c0392b", "anchor on sea-blue").
|
||||
@@ -205,7 +205,7 @@ State the genre out loud at Step 2.5 alongside the macrostructure and theme pick
|
||||
|
||||
If any of those fires, ask one short follow-up before picking: *"This brief reads like a custom palette would fit better than the catalog. Want me to construct a custom OKLCH palette + free-font pairing tuned to <one-line summary of the vibe>, or stay on the catalog for variety + speed?"* Wait for the user to say custom (or catalog). Default is still catalog — silence routes to catalog, not custom.
|
||||
|
||||
If none of the signals fires, **proceed with catalog silently. Do not mention the fork.** Most briefs don't need a custom theme — the catalog's 22 themes plus the rotation rule already deliver structural variety. See Step 2.6 for the dispatch.
|
||||
If none of the signals fires, **proceed with catalog silently. Do not mention the fork.** Most briefs don't need a custom theme — the catalog's 23 themes plus the rotation rule already deliver structural variety. See Step 2.6 for the dispatch.
|
||||
|
||||
**If the user opts out** (says "go ahead", "you pick", "skip", "just build it", "don't ask", or simply doesn't engage with the question after one prompt):
|
||||
|
||||
@@ -292,10 +292,10 @@ The rotation block keeps the user inside the discipline without making them read
|
||||
By the time you reach this step, one of three things is true:
|
||||
|
||||
1. **The user named custom** (because they said so, or because Step 1's signal detection fired and they confirmed) → load [`references/custom-theme.md`](references/custom-theme.md), ask the **one** follow-up (vibe in 4–8 words + optional anchor colour), construct the OKLCH palette + free-font pairing, compute the three axis values (paper-band / display-style / accent-hue), then continue to Step 3.
|
||||
2. **The user named catalog** (or implicitly accepted it by not naming custom) → pick one of the 22 named themes per the diversification rule above. Existing flow — continue to Step 3.
|
||||
2. **The user named catalog** (or implicitly accepted it by not naming custom) → pick one of the 23 named themes per the diversification rule above. Existing flow — continue to Step 3.
|
||||
3. **Neither was discussed** (Step 1's signals didn't fire — vanilla brief) → default to **catalog**. Do not pause. Do not ask. Continue to Step 3.
|
||||
|
||||
**Custom is a quiet branch, not a default question.** Most briefs route to catalog and the user never sees the words "catalog" or "custom." The 22 named themes plus the rotation rule already deliver structural variety; the fork is reserved for when the brief specifically asks for a tuned look the catalog can't carry.
|
||||
**Custom is a quiet branch, not a default question.** Most briefs route to catalog and the user never sees the words "catalog" or "custom." The 23 named themes plus the rotation rule already deliver structural variety; the fork is reserved for when the brief specifically asks for a tuned look the catalog can't carry.
|
||||
|
||||
A custom theme is a **complete** OKLCH palette + font pairing tuned to the brief — not a one-off colour swap, not an excuse to bypass the rules. Every constraint in [`color.md`](references/color.md), [`typography.md`](references/typography.md), and [`anti-patterns.md`](references/anti-patterns.md) still applies. The 55 slop-test gates fire unchanged. The Step 5 preview block surfaces the palette + pairing in plain text **before** any code is emitted, so the user can redirect.
|
||||
|
||||
@@ -358,7 +358,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
|
||||
**Format** (Markdown bullets, not ASCII boxes — they render reliably across every chat client and terminal):
|
||||
|
||||
```markdown
|
||||
**Hallmark · v0.8.0**
|
||||
**Hallmark · v0.9.0**
|
||||
|
||||
- **Macrostructure** · Stat-Led
|
||||
- **Theme** · Plain (#fff paper · cool greys · ink-blue accent)
|
||||
@@ -382,7 +382,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
|
||||
**Three more sample preview blocks** for the model to imitate, varied across macrostructure types:
|
||||
|
||||
*Long Document (editorial, motion-cut):*
|
||||
> **Hallmark · v0.8.0**
|
||||
> **Hallmark · v0.9.0**
|
||||
>
|
||||
> - **Macrostructure** · Long Document
|
||||
> - **Theme** · Linen (cool slate paper · steel-blue accent · geometric sans)
|
||||
@@ -393,7 +393,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
|
||||
> - **Diversification** · first run for this project
|
||||
|
||||
*Bento Grid (SaaS, motion-on):*
|
||||
> **Hallmark · v0.8.0**
|
||||
> **Hallmark · v0.9.0**
|
||||
>
|
||||
> - **Macrostructure** · Bento Grid
|
||||
> - **Theme** · Pastel (light cool paper · indigo accent · geometric Geist)
|
||||
@@ -404,7 +404,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
|
||||
> - **Diversification** · differs from Plain on paper hue (light-cool vs pure-white) + accent (indigo vs ink-blue)
|
||||
|
||||
*Manifesto (declarative, no enrichment):*
|
||||
> **Hallmark · v0.8.0**
|
||||
> **Hallmark · v0.9.0**
|
||||
>
|
||||
> - **Macrostructure** · Manifesto
|
||||
> - **Theme** · Manifesto (dark · Inter Tight 900 · single red bleed)
|
||||
@@ -415,7 +415,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
|
||||
> - **Diversification** · differs from Linen on paper band (dark vs light) + display style (display-heavy vs geometric-sans)
|
||||
|
||||
*Custom (Coffeebox archival café):*
|
||||
> **Hallmark · v0.8.0**
|
||||
> **Hallmark · v0.9.0**
|
||||
>
|
||||
> - **Macrostructure** · Long Document
|
||||
> - **Theme** · custom (vibe: "archival warmth, hand-set, no varnish" · paper oklch(94% 0.020 65) · accent oklch(58% 0.16 35) terracotta · Fraunces italic display + Source Serif 4 body)
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* generate.mjs · Hallmark image-generation recipe (Tier C)
|
||||
*
|
||||
* Drop this script into a Hallmark project. It generates brand-coherent
|
||||
* watercolour / ink-wash / risograph / charcoal / etc. illustrations
|
||||
* that all look like the same artist made them — by locking a seed
|
||||
* and a reference image across every call.
|
||||
*
|
||||
* Setup:
|
||||
* 1. Add `TOGETHER_API_KEY=…` to .env.local at your project root.
|
||||
* 2. Add `.env.local` to .gitignore (don't commit your key).
|
||||
* 3. Pick the theme from skill/references/generated-imagery.md.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/generate.mjs --theme editorial --slot hero --subject "a hand-bound notebook of recipes"
|
||||
* node scripts/generate.mjs --theme bloom --slot section-anchor --subject "a single pomegranate split open"
|
||||
* node scripts/generate.mjs --list-themes
|
||||
*
|
||||
* Output:
|
||||
* ./img/generated/_seed-reference.png (locked anchor, generated on first run)
|
||||
* ./img/generated/_seed-reference.png.url (cached URL for subsequent calls)
|
||||
* ./img/generated/<slot>.png (one per call)
|
||||
*
|
||||
* What it does:
|
||||
* - Reads TOGETHER_API_KEY from .env.local
|
||||
* - Looks up the locked theme prompt prefix
|
||||
* - On first call: generates the reference image, caches its hosted URL
|
||||
* - On every subsequent call: passes the reference URL via reference_images[]
|
||||
* and locks seed=7727 so all images share one visual hand
|
||||
* - Writes ./img/generated/<slot>.png
|
||||
*
|
||||
* Cost: $0.134 per 2K image, $0.034 per 1K. Set --size 1k for cheaper.
|
||||
*/
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const OUT_DIR = path.resolve(process.cwd(), "img/generated");
|
||||
const ENV_PATH = path.resolve(process.cwd(), ".env.local");
|
||||
const API_URL = "https://api.together.xyz/v1/images/generations";
|
||||
const MODEL_ID = "google/gemini-3-pro-image";
|
||||
const SEED = 7727;
|
||||
|
||||
/* — Locked style-prompt prefix per theme ————————————————————
|
||||
Mirror of the table in skill/references/generated-imagery.md.
|
||||
Lock this once per project. Don't edit it between runs. */
|
||||
const THEME_PROMPTS = {
|
||||
specimen: "Pen-and-ink plate on warm cream paper. Crosshatched shading, single-line silhouettes, faint sepia wash. Hand-drawn from a 1930s book. No colour beyond ink and warm cream. Subject:",
|
||||
newsprint: "Halftone newspaper print on warm pulp paper. Visible dot screen, slightly off-register, restrained sepia + black. Editorial illustration in the manner of a daily broadsheet. Subject:",
|
||||
atelier: "Graphite pencil sketch on soft buff paper. Loose construction lines visible, light cross-hatching, a single coral accent in one detail. Studio sketchbook. Subject:",
|
||||
garden: "Watercolour plate with botanical-illustration discipline. Translucent washes of warm green and faded coral on cream paper. Visible paper grain. Hand-drawn in a 19th-century herbal. Subject:",
|
||||
salon: "Ink-wash illustration on warm cream paper, in the manner of a literary journal frontispiece. Restrained colour, fine ink line, generous breathing room. Subject:",
|
||||
linen: "Sepia-tone charcoal sketch on linen-textured paper. Smudged shading, gestural marks, a single warm copper accent. Subject:",
|
||||
almanac: "Lithograph print on warm cream paper, in the manner of a 19th-century reference plate. Crosshatching, careful captions, restrained palette. Subject:",
|
||||
midnight: "Monochrome neon-line illustration on near-black paper. Single phosphor cyan stroke, soft glow, crisp dark surroundings. Subject:",
|
||||
terminal: "Monochrome ASCII-style print on near-black paper. Single phosphor green tone, no painterly gradients, the look of a printed terminal screen. Subject:",
|
||||
brutal: "High-contrast woodcut print, black ink on warm cream paper. Bold blocks, gouge marks, no halftones. Subject:",
|
||||
manifesto: "Cut-paper collage on warm cream paper. Bold geometric shapes torn at the edges, single red ink wash, hand-set rough texture. Subject:",
|
||||
sport: "Bold action-line illustration in a 1970s sports-poster register. Two-colour palette: black and a single warm orange. Crisp diagonals. Subject:",
|
||||
studio: "Clean geometric line illustration on cool grey paper, in the manner of a Swiss design-school plate. Restrained palette: ink + a single sage green accent. Subject:",
|
||||
pastel: "Soft gouache plate on cool cream paper. Translucent pastel washes — pale indigo, mint, faded peach — with crisp pencil lines. Subject:",
|
||||
riso: "Risograph two-colour print on warm cream paper. Visible mis-register between coral red and faded blue, halftone dot texture, flat geometric shapes. Subject:",
|
||||
quiet: "Monochrome ink line on warm white paper. A single gestural stroke, vast negative space, no colour. The minimum that still reads as a drawing. Subject:",
|
||||
bloom: "Deep ink wash on dark paper. Translucent layers of charcoal and faded amber bleeding into each other, a single warm bloom of light. Subject:",
|
||||
coral: "Soft watercolour plate on warm cream paper. Translucent washes of coral, peach, and ivory; visible paper grain; loose ink line. Subject:",
|
||||
violet: "Watercolour plate on cool white paper. Translucent washes of violet, lavender, and dusty rose; ink line in soft charcoal. Subject:",
|
||||
aurora: "Soft gradient line illustration on near-black paper. Translucent washes of cyan, soft green, and faint purple — northern-lights inspired but grounded in line. Subject:",
|
||||
halo: "Hand-drawn glyph plate on warm cream paper. Restrained gold leaf accent, faint ink hatching, a single ornament per plate. The discipline of a medieval illuminator. Subject:",
|
||||
plume: "Calligraphy and brush-stroke illustration on warm cream paper. Translucent rose wash, single ink flourish, soft pencil-line construction. Subject:",
|
||||
editorial: "Watercolor and ink wash on warm cream paper. Soft translucent washes of coral, terracotta, and faded olive. Visible paper grain and bleed edges. Loose asymmetric composition. Hand-made plate from a quiet studio. Subject:",
|
||||
};
|
||||
|
||||
/* — Allowed dimensions ———————————————————————————————————— */
|
||||
const SIZES = {
|
||||
"1k-square": [1024, 1024],
|
||||
"1k-wide": [1264, 848],
|
||||
"1k-tall": [848, 1264],
|
||||
"2k-square": [2048, 2048],
|
||||
"2k-wide": [2528, 1696],
|
||||
"2k-tall": [1696, 2528],
|
||||
};
|
||||
|
||||
async function loadEnv() {
|
||||
let raw;
|
||||
try { raw = await fs.readFile(ENV_PATH, "utf8"); }
|
||||
catch { throw new Error(`.env.local not found at ${ENV_PATH}. Add TOGETHER_API_KEY=… to it.`); }
|
||||
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 arg(name, fallback) {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
return i >= 0 ? process.argv[i + 1] : fallback;
|
||||
}
|
||||
|
||||
async function callTogether({ prompt, width, height, referenceUrl, apiKey }) {
|
||||
const body = {
|
||||
model: MODEL_ID, prompt, width, height, seed: SEED,
|
||||
};
|
||||
if (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),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`Together AI ${res.status}: ${err.slice(0, 400)}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async function downloadOrDecode(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)}`);
|
||||
if (url) {
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`Image fetch ${r.status}`);
|
||||
return { bytes: Buffer.from(await r.arrayBuffer()), hostedUrl: url };
|
||||
}
|
||||
return { bytes: Buffer.from(b64, "base64"), hostedUrl: null };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.argv.includes("--list-themes")) {
|
||||
console.log("Available themes:");
|
||||
for (const t of Object.keys(THEME_PROMPTS)) console.log(` ${t}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const theme = arg("theme");
|
||||
const slot = arg("slot", "untitled");
|
||||
const subject = arg("subject");
|
||||
const sizeKey = arg("size", "1k-wide");
|
||||
|
||||
if (!theme || !subject) {
|
||||
console.error("Usage: node generate.mjs --theme <theme> --slot <slot> --subject \"…\" [--size 1k-wide|1k-square|2k-wide|2k-tall]");
|
||||
console.error(" node generate.mjs --list-themes");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const prefix = THEME_PROMPTS[theme];
|
||||
if (!prefix) {
|
||||
console.error(`Unknown theme: ${theme}. Run --list-themes for the catalog.`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const [width, height] = SIZES[sizeKey] || SIZES["1k-wide"];
|
||||
|
||||
await loadEnv();
|
||||
const apiKey = (process.env.TOGETHER_API_KEY || "").trim();
|
||||
if (!apiKey) {
|
||||
console.error("TOGETHER_API_KEY missing in .env.local at the project root.");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
await fs.mkdir(OUT_DIR, { recursive: true });
|
||||
const refPath = path.join(OUT_DIR, "_seed-reference.png");
|
||||
const refUrlPath = refPath + ".url";
|
||||
|
||||
// Get or create the locked reference image
|
||||
let referenceUrl = null;
|
||||
try {
|
||||
referenceUrl = (await fs.readFile(refUrlPath, "utf8")).trim();
|
||||
} catch {
|
||||
console.log(`→ no reference yet · generating one for theme=${theme}…`);
|
||||
const refPrompt = `${prefix} a small abstract still life that captures the spirit of ${theme}. Loose composition. Hand-made.`;
|
||||
const json = await callTogether({ prompt: refPrompt, width, height, apiKey });
|
||||
const { bytes, hostedUrl } = await downloadOrDecode(json);
|
||||
await fs.writeFile(refPath, bytes);
|
||||
if (hostedUrl) {
|
||||
await fs.writeFile(refUrlPath, hostedUrl);
|
||||
referenceUrl = hostedUrl;
|
||||
}
|
||||
console.log(` ✓ reference → ${refPath} (${Math.round(bytes.length / 1024)} KB)`);
|
||||
}
|
||||
|
||||
// Generate the requested slot
|
||||
const fullPrompt = `${prefix} ${subject}.`;
|
||||
console.log(`→ generating ${slot} (theme=${theme}, ${width}×${height}, seed=${SEED})…`);
|
||||
const json = await callTogether({ prompt: fullPrompt, width, height, referenceUrl, apiKey });
|
||||
const { bytes } = await downloadOrDecode(json);
|
||||
const outPath = path.join(OUT_DIR, `${slot}.png`);
|
||||
await fs.writeFile(outPath, bytes);
|
||||
console.log(` ✓ ${slot} → ${outPath} (${Math.round(bytes.length / 1024)} KB)`);
|
||||
|
||||
console.log("\nNext: post-process. See skill/references/generated-imagery.md § post-processing checklist:");
|
||||
console.log(" 1. Grain overlay 2. Asymmetric crop 3. Colour-shift toward accent 4. Optional frame");
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("FAIL:", e.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -88,6 +88,14 @@ Wordmark hard-left, 4–5 inline text links (`Features · Pricing · Docs · Blo
|
||||
|
||||
**Fix.** Pick from the routing table in [`component-cookbook.md`](component-cookbook.md) § Footers. Default to Ft1 Mast-headed, Ft2 Inline single line, Ft4 Dense colophon, Ft5 Statement, Ft6 Letter close, Ft7 Newsletter-first, or Ft8 Marquee scroll. Use Ft3 Index columns *only* on a genuine hub or docs root with a real sitemap — and even then, never with the social-icon row + tiny copyright tail.
|
||||
|
||||
### Raw generated stills
|
||||
|
||||
Shipping a Nanobanana / Gemini-image / Midjourney output without post-processing — no grain overlay, no asymmetric crop, no colour-grade toward the brand accent, no seed-locking across the project's image set.
|
||||
|
||||
**Why it fails.** Reads as AI in milliseconds. Smooth surface, default lighting, perfect symmetry. The 2026 audience pattern-matches this faster than any other tell.
|
||||
|
||||
**Fix.** Use the seed-and-reference rule + the post-processing checklist in [`generated-imagery.md`](generated-imagery.md). One reference, one seed, four post-processing moves (grain · asymmetric crop · accent-shift · optional hand-drawn frame). Skip any of these and the image reads as raw AI.
|
||||
|
||||
### Aurora-blob background
|
||||
|
||||
Flowing organic mesh blobs in purple-to-pink-to-cyan, layered behind hero text. Looks "premium" until you've seen it on every Dribbble shot since 2022.
|
||||
|
||||
@@ -99,16 +99,16 @@ import { siGithub } from 'simple-icons/icons';
|
||||
|
||||
## Generated illustration (Tier C in the enrichment hierarchy)
|
||||
|
||||
When characters or specific scenes can't be hand-built economically. **Always post-process.** See [`custom-craft.md`](custom-craft.md) Tier E for full discipline.
|
||||
**As of v0.9.0, Tier C is a peer of Tier B (hand-built SVG)** when used with the seed-and-reference discipline plus post-processing. See [`generated-imagery.md`](generated-imagery.md) for the full recipe — that file is the canonical place; this section is the model catalog.
|
||||
|
||||
### Canon
|
||||
|
||||
| Model | URL | Cost | Best for | Output |
|
||||
| Model | Hosted via | Cost | Best for | Output |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **[Nanobanana 2 / Gemini 2.5 Flash Image](https://ai.google.dev/gemini-api/docs/image-generation)** | Google AI | $0.039 / image | Character consistency across panels, fast iteration, brand-style adherence via reference images, infographics with text | PNG (transparent supported) |
|
||||
| **[Gemini 3 Pro Image / Nano Banana Pro](https://ai.google.dev/gemini-api/docs/image-generation)** | **Together AI** (`google/gemini-3-pro-image`) — $0.134/image at 2K | $0.134 / 2K · $0.034 / 1K | Brand-coherent illustration sets via reference-image character consistency, fast iteration, the 2026 Tier C default | PNG |
|
||||
| **[Recraft V4](https://www.recraft.ai/)** | recraft.ai | ~$0.04 / image | **The only model with production-grade SVG output.** Logos, icons, illustrations that need to scale. | SVG + PNG |
|
||||
| **[Midjourney v8](https://www.midjourney.com)** | midjourney.com | ~$0.14 / image | Aesthetic beauty, atmospheric stills, artistic direction | PNG |
|
||||
| **[Flux 2](https://blackforestlabs.ai/)** | blackforestlabs.ai | ~$0.03 / image | Photorealism — skin, fabric, product detail, hands | PNG |
|
||||
| **[Flux 2](https://blackforestlabs.ai/)** | Together AI / Black Forest Labs | ~$0.03 / image | Photorealism — skin, fabric, product detail, hands | PNG |
|
||||
|
||||
### The rules
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Loaded only when the user has opted into the **custom** theme route in Step 1 of
|
||||
|
||||
## Two words, plain English
|
||||
|
||||
- **catalog** — the named-theme catalogue. Hallmark's 22 themes (Specimen, Atelier, Pastel, Brutal, Salon, Newsprint, Linen, Studio, Manifesto, Terminal, Midnight, Almanac, Garden, Quiet, Riso, Sport, Bloom, Coral, Violet, Aurora, Halo, Plume). Each one is a fixed combination of paper-band, display-style, and accent-hue. The rotation rule cycles through them so two consecutive runs don't read alike. **This is the default.** Most briefs use it.
|
||||
- **catalog** — the named-theme catalogue. Hallmark's 23 themes (Specimen, Atelier, Pastel, Brutal, Salon, Newsprint, Linen, Studio, Manifesto, Terminal, Midnight, Almanac, Garden, Quiet, Riso, Sport, Bloom, Coral, Violet, Aurora, Halo, Plume, Editorial). Each one is a fixed combination of paper-band, display-style, and accent-hue. The rotation rule cycles through them so two consecutive runs don't read alike. **This is the default.** Most briefs use it.
|
||||
- **custom** — made-to-measure. A one-off palette + font pairing constructed specifically for one brief. The skill builds the OKLCH values, picks fonts, and stamps the page — but does **not** add a 23rd theme to the catalog. The rules behind the catalog (paper L bands, accent chroma caps, font ban list, slop-test gates) all still apply; only the *combination* is per-brief.
|
||||
|
||||
## When to surface this fork — Step 1 trigger signals
|
||||
@@ -15,12 +15,12 @@ Hallmark must **not** offer catalog-vs-custom on every prompt. That's friction,
|
||||
|
||||
1. **Explicit ask** — the user types `custom`, "custom theme", "tailored to our brand", "make it ours", "something unique", "play around with the colors and fonts", "I want my own palette".
|
||||
2. **Named brand colour** — the user gives a specific anchor colour as a hex / OKLCH / brand name. Example: "use our terracotta", "the brand red is hex #c0392b", "anchor on sea-blue".
|
||||
3. **Multi-attribute aesthetic the catalog can't carry** — three or more vibe words pointing at a specific, off-catalog feel. Examples: "moss, lichen, soft pink, herbal" / "sun-drenched, market-day, carbon-black" / "late-night, neon, brutalist deli". Compare against the 22 catalog themes; if no single catalog theme is within one axis-step of the vibe, fire the fork. **One adjective ("warm", "technical", "playful") is not a signal — that's a tone, the catalog already carries it.**
|
||||
3. **Multi-attribute aesthetic the catalog can't carry** — three or more vibe words pointing at a specific, off-catalog feel. Examples: "moss, lichen, soft pink, herbal" / "sun-drenched, market-day, carbon-black" / "late-night, neon, brutalist deli". Compare against the 23 catalog themes; if no single catalog theme is within one axis-step of the vibe, fire the fork. **One adjective ("warm", "technical", "playful") is not a signal — that's a tone, the catalog already carries it.**
|
||||
4. **Brand-mood reference attached** — the user attaches a colour swatch, a moodboard, a Pantone chip. (If they attach a *page* screenshot, route to `study` instead; custom is for brand colour / mood, study is for design DNA.)
|
||||
|
||||
If any signal fires, ask one short follow-up before picking a theme:
|
||||
|
||||
> *"This brief reads like a custom palette would fit better than the 22 named themes. Want me to construct a custom OKLCH palette + free-font pairing tuned to <one-line summary of the vibe>, or stay on the catalog for variety + speed?"*
|
||||
> *"This brief reads like a custom palette would fit better than the 23 named themes. Want me to construct a custom OKLCH palette + free-font pairing tuned to <one-line summary of the vibe>, or stay on the catalog for variety + speed?"*
|
||||
|
||||
Wait for the user to answer. If they say custom (or yes / go) → continue this protocol from § A. If they say catalog (or no / stay catalog) → drop the fork and proceed with the catalog route. **Default to catalog** — silence routes to catalog, not custom.
|
||||
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
# Generated imagery
|
||||
|
||||
When a Hallmark project ships images, the rule is: **every image looks like the same artist made it**. Within one project — one brand — every plate, glyph, backdrop, and ornament shares the same hand. Across themes, the hand changes — Specimen looks like a pen-and-ink plate; Bloom looks like a deep ink wash on dark paper; Terminal looks like a monochrome ASCII print. The discipline that keeps this together is **a locked seed plus a locked reference image**.
|
||||
|
||||
This file ships the recipe.
|
||||
|
||||
---
|
||||
|
||||
## When generated stills earn their place
|
||||
|
||||
Generated illustration was Tier C in [`hero-enrichment.md`](hero-enrichment.md) — historically a "last resort, only when characters demand it" tier. **As of Hallmark v0.9.0, that's revised.** With Gemini 3 Pro Image (Nano Banana Pro) hosted by Together AI, character-consistency via reference-images, and rigorous post-processing, Tier C is now a **peer of Tier B (hand-built SVG)** when the discipline below is followed.
|
||||
|
||||
What changed: in 2024 you needed Midjourney + heavy retouching. In 2026 a single seed-locked + reference-image-locked Together AI call gives you a coherent set of images for one brand, in one style, that look hand-made.
|
||||
|
||||
The discipline did not change. Raw output still reads as AI in milliseconds. Post-processing is what makes it ours.
|
||||
|
||||
**When to reach for Tier C:**
|
||||
- The brand has a story or set of subjects that benefit from being illustrated (a bakery, a podcast, an architecture studio, a journal, an atelier).
|
||||
- Hand-built SVG (Tier B) can't economically reach the depth — characters, scenes, atmosphere.
|
||||
- The brief carries a specific visual voice the catalog can name (watercolor, ink wash, woodcut, risograph, charcoal, gouache, illustration, photography).
|
||||
|
||||
**When NOT to reach for Tier C:**
|
||||
- The page is a CLI / dev-tool / docs site where typography carries everything (Tier 0).
|
||||
- The brand is "modern professional team" generic — illustrating that is the new template.
|
||||
- A pure-CSS shape (Tier A) would do the job in fewer bytes.
|
||||
- The image would need post-processing the team can't or won't do — better no image than a raw Nanobanana output.
|
||||
|
||||
---
|
||||
|
||||
## The seed-and-reference rule
|
||||
|
||||
This is the rule that makes "all images on this site look like one artist made them."
|
||||
|
||||
**Per project, you pick once and never change:**
|
||||
|
||||
1. **A seed.** A 4–5 digit integer. Hallmark default is `7727`. Use it on every call.
|
||||
2. **A style reference image.** ONE PNG that anchors the visual language for the whole project. Generate this first with the locked theme prompt below + a subject of your choice. Save it at `<project>/img/generated/_seed-reference.png` and commit it.
|
||||
3. **A locked prompt prefix.** From the per-theme table below — the prefix that names the style (medium, palette, paper, mood). Don't tweak it between calls.
|
||||
|
||||
**Per generation call, you only vary the subject.** Always:
|
||||
- Pass `seed` set to your project's seed
|
||||
- Pass the locked reference image as `reference_images[0]`
|
||||
- Use the locked prompt prefix
|
||||
- Append a one-sentence subject specific to the slot
|
||||
|
||||
That's the whole rule. Get those right and every image lands in the same hand.
|
||||
|
||||
```ts
|
||||
// What every call looks like, after step 1 (reference + seed locked):
|
||||
{
|
||||
model: "google/gemini-3-pro-image",
|
||||
prompt: `${THEME_PROMPT_PREFIX} ${slot_subject}.`,
|
||||
width: 1264, height: 848, // or 1024×1024 for square; see allowed sizes below
|
||||
seed: 7727,
|
||||
reference_images: [REFERENCE_URL],
|
||||
}
|
||||
```
|
||||
|
||||
The reference image's hosted URL (returned by Together AI on the first call) is the one you pass on subsequent calls. Cache it next to the file: `_seed-reference.png.url`.
|
||||
|
||||
---
|
||||
|
||||
## Per-theme style prompts
|
||||
|
||||
Each Hallmark theme has a locked style-prompt prefix. Pick the one that matches the active theme and use it as the prefix for every image in the project. Don't blend prefixes; don't "tweak" them.
|
||||
|
||||
| Theme | Style-prompt prefix |
|
||||
| --- | --- |
|
||||
| **Specimen** | Pen-and-ink plate on warm cream paper. Crosshatched shading, single-line silhouettes, faint sepia wash. Hand-drawn from a 1930s book. No colour beyond ink and warm cream. Subject: |
|
||||
| **Newsprint** | Halftone newspaper print on warm pulp paper. Visible dot screen, slightly off-register, restrained sepia + black. Editorial illustration in the manner of a daily broadsheet. Subject: |
|
||||
| **Atelier** | Graphite pencil sketch on soft buff paper. Loose construction lines visible, light cross-hatching, a single coral accent in one detail. Studio sketchbook. Subject: |
|
||||
| **Garden** | Watercolour plate with botanical-illustration discipline. Translucent washes of warm green and faded coral on cream paper. Visible paper grain. Hand-drawn in a 19th-century herbal. Subject: |
|
||||
| **Salon** | Ink-wash illustration on warm cream paper, in the manner of a literary journal frontispiece. Restrained colour, fine ink line, generous breathing room. Subject: |
|
||||
| **Linen** | Sepia-tone charcoal sketch on linen-textured paper. Smudged shading, gestural marks, a single warm copper accent. Subject: |
|
||||
| **Almanac** | Lithograph print on warm cream paper, in the manner of a 19th-century reference plate. Crosshatching, careful captions, restrained palette. Subject: |
|
||||
| **Midnight** | Monochrome neon-line illustration on near-black paper. Single phosphor cyan stroke, soft glow, crisp dark surroundings. Subject: |
|
||||
| **Terminal** | Monochrome ASCII-style print on near-black paper. Single phosphor green tone, no painterly gradients, the look of a printed terminal screen. Subject: |
|
||||
| **Brutal** | High-contrast woodcut print, black ink on warm cream paper. Bold blocks, gouge marks, no halftones. Subject: |
|
||||
| **Manifesto** | Cut-paper collage on warm cream paper. Bold geometric shapes torn at the edges, single red ink wash, hand-set rough texture. Subject: |
|
||||
| **Sport** | Bold action-line illustration in a 1970s sports-poster register. Two-colour palette: black and a single warm orange. Crisp diagonals. Subject: |
|
||||
| **Studio** | Clean geometric line illustration on cool grey paper, in the manner of a Swiss design-school plate. Restrained palette: ink + a single sage green accent. Subject: |
|
||||
| **Pastel** | Soft gouache plate on cool cream paper. Translucent pastel washes — pale indigo, mint, faded peach — with crisp pencil lines. Subject: |
|
||||
| **Riso** | Risograph two-colour print on warm cream paper. Visible mis-register between coral red and faded blue, halftone dot texture, flat geometric shapes. Subject: |
|
||||
| **Quiet** | Monochrome ink line on warm white paper. A single gestural stroke, vast negative space, no colour. The minimum that still reads as a drawing. Subject: |
|
||||
| **Bloom** | Deep ink wash on dark paper. Translucent layers of charcoal and faded amber bleeding into each other, a single warm bloom of light. Subject: |
|
||||
| **Coral** | Soft watercolour plate on warm cream paper. Translucent washes of coral, peach, and ivory; visible paper grain; loose ink line. Subject: |
|
||||
| **Violet** | Watercolour plate on cool white paper. Translucent washes of violet, lavender, and dusty rose; ink line in soft charcoal. Subject: |
|
||||
| **Aurora** | Soft gradient line illustration on near-black paper. Translucent washes of cyan, soft green, and faint purple — northern-lights inspired but grounded in line. Subject: |
|
||||
| **Halo** | Hand-drawn glyph plate on warm cream paper. Restrained gold leaf accent, faint ink hatching, a single ornament per plate. The discipline of a medieval illuminator. Subject: |
|
||||
| **Plume** | Calligraphy and brush-stroke illustration on warm cream paper. Translucent rose wash, single ink flourish, soft pencil-line construction. Subject: |
|
||||
| **Editorial** | Watercolor and ink wash on warm cream paper. Soft translucent washes of coral, terracotta, and faded olive. Visible paper grain and bleed edges. Loose asymmetric composition. Hand-made plate from a quiet studio. Subject: |
|
||||
|
||||
Pick one row. Lock it for the project. Don't blend.
|
||||
|
||||
---
|
||||
|
||||
## Supported dimensions
|
||||
|
||||
Gemini 3 Pro Image accepts only these source sizes. The display crop in CSS can be anything (`object-fit: cover`); these are the source dimensions you request:
|
||||
|
||||
| Aspect | Sizes |
|
||||
| --- | --- |
|
||||
| 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` |
|
||||
|
||||
For most slots, `1264×848` (wide) or `1024×1024` (square) is enough. Reach for 2K only on hero backdrops that go full-bleed.
|
||||
|
||||
`steps`, `width`/`height` outside this list, and other FLUX-style parameters are not supported. Don't send them.
|
||||
|
||||
---
|
||||
|
||||
## Post-processing checklist
|
||||
|
||||
Raw output reads as AI even when the prompt is good. The four post-processing moves are non-negotiable:
|
||||
|
||||
1. **Grain.** Apply an SVG `<feTurbulence>` overlay at 0.04–0.08 opacity, `mix-blend-mode: multiply`, scoped to the image. Or a faint paper-texture PNG at 0.06 opacity. The grain is what breaks the "smooth" AI surface.
|
||||
2. **Asymmetric crop.** Crop 8–15% off one edge (usually right or bottom). Centred crops read as AI; an asymmetric crop reads as a human framing.
|
||||
3. **Colour grade toward the brand accent.** Mix the image 8–12% toward `--color-accent` using `mix-blend-mode: multiply` over an accent-colour layer, or in the post-processor of your choice. The image gets pulled into the project's palette.
|
||||
4. **Optional: hand-drawn frame.** A 0.5px hairline rule in `--color-rule`, or a corner-bracket utility (see `.fig-corners` in component-cookbook), or four small `+` marks at corners. Optional but lifts a generated still toward "made on a desk."
|
||||
|
||||
If a generated image has not been through all four (or three plus a deliberate skip on the fourth), it is not ready to ship.
|
||||
|
||||
---
|
||||
|
||||
## Provenance stamp
|
||||
|
||||
Every project that uses generated imagery records it in the macrostructure stamp:
|
||||
|
||||
```css
|
||||
/* Hallmark · genre: editorial · macrostructure: Long Document
|
||||
* theme: editorial · nav: N6 · footer: Ft1
|
||||
* imagery: gemini-3-pro · seed: 7727 · reference: tokens/_image-reference.png
|
||||
* post-processed: grain + asymmetric-crop + accent-shift
|
||||
* slop: pass (51-55) · contrast: pass (46-50)
|
||||
*/
|
||||
```
|
||||
|
||||
The `imagery:` line tells future Hallmark runs (and any audit) which model + seed + reference produced the images. Reproducible. Honest about provenance.
|
||||
|
||||
---
|
||||
|
||||
## Two paths for users
|
||||
|
||||
### Path A · Pre-generated style packs (no API key needed)
|
||||
|
||||
For users who don't have a Together AI key, Hallmark ships pre-generated style packs at [`skill/_assets/image-packs/<theme>/`](../_assets/image-packs/):
|
||||
|
||||
```
|
||||
_assets/image-packs/editorial/
|
||||
_seed-reference.png (the locked anchor)
|
||||
01-hero-backdrop.png (1920×1080 ambient)
|
||||
02-section-anchor.png (1024×1024 corner plate)
|
||||
03-glyph.png (1024×1024 mark)
|
||||
```
|
||||
|
||||
Users copy the pack into their project's `./img/generated/` directory and reference the files. No API key. Limited to fixed images.
|
||||
|
||||
### Path B · Power-user recipe (own API key)
|
||||
|
||||
For users who want their own brand subjects, ship the recipe at [`skill/_assets/scripts/generate.mjs`](../_assets/scripts/generate.mjs). They copy it into their project, set `TOGETHER_API_KEY` in `.env.local`, and run:
|
||||
|
||||
```bash
|
||||
node scripts/generate.mjs --theme editorial --slot hero --subject "a hand-bound notebook of recipes"
|
||||
```
|
||||
|
||||
The script:
|
||||
- Reads `TOGETHER_API_KEY` from `.env.local`
|
||||
- Looks up the locked theme prompt prefix from this file
|
||||
- Generates the seed reference if none exists, otherwise uses the cached URL
|
||||
- Calls Together AI with seed=7727 + reference_images=[reference_url]
|
||||
- Writes `./img/generated/<slot>.png`
|
||||
|
||||
Power users get unlimited brand-coherent images on their own dime ($0.134 per call).
|
||||
|
||||
### Path C · Free-trial endpoint (planned, post-launch)
|
||||
|
||||
For users who want to sample before paying: a Vercel Edge Function at `hallmark-murex.vercel.app/api/generate` accepts `{ subject, theme }` and returns a generated image. Rate-limited to 5 generations per IP per day, daily budget cap of $5. Users cap their own taste; we cap our exposure. (Not shipped at launch — see roadmap.)
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns to refuse
|
||||
|
||||
- **Raw output.** Shipping any model's first try without grain, crop, colour grade. Slop.
|
||||
- **Mixing styles in one project.** Two themes' image styles on the same site. Pick one, lock it.
|
||||
- **Forgetting the seed or reference.** Each call without them returns a different artist. Don't.
|
||||
- **Aurora-blob backgrounds disguised as Tier C.** A purple-pink mesh blob is still aurora-blob even if Gemini drew it. Refuse.
|
||||
- **Smiling-at-laptop generic.** "Modern professional team" stock-photo poses, smooth-mesh-blob characters, symmetric stage-lighting. Refuse.
|
||||
|
||||
The audit verb checks for these and fails the build.
|
||||
|
||||
---
|
||||
|
||||
## Cost framing
|
||||
|
||||
At $0.134 per 2K image:
|
||||
|
||||
- A small project (~5 images): ~$0.70.
|
||||
- A medium project (~15 images): ~$2.00.
|
||||
- A pre-generated theme pack (4 images × 23 themes): ~$12.30 one-time for Hallmark to ship the packs.
|
||||
- A free-trial endpoint cap of $5/day yields ~37 free generations daily, ~1,100/month.
|
||||
|
||||
These are rounding-error costs vs the time post-processing the same images by hand.
|
||||
@@ -17,7 +17,7 @@ Reach for the highest tier the brief lets you ship in the time you have. Skippin
|
||||
| **0 · Typography only** | No enrichment. Display, lede, optional CTA. | Always acceptable. The strongest fail-state. |
|
||||
| **A · Custom-built CSS art** | Pure-CSS shapes, gradients, clip-paths, no asset, zero dependency. | Geometric shapes, gradient compositions, glyph-style decoration. |
|
||||
| **B · Hand-built SVG** | Designed in Figma, optimised, animated declaratively. | Illustrations more complex than CSS handles cleanly — a loaf, a mascot, a workflow diagram. |
|
||||
| **C · Generated illustration** | Nanobanana / Recraft V4 / Midjourney, with provenance + post-processing. | Characters or specific scenes that hand-build can't economically reach. Always post-processed. |
|
||||
| **C · Generated illustration** | Gemini 3 Pro Image / Nanobanana / Recraft V4, with seed-locking + reference-image + post-processing. **As of v0.9.0, a peer of Tier B** when the discipline is followed (see [`generated-imagery.md`](generated-imagery.md)). | Characters, scenes, atmosphere, brand-coherent illustration sets. The seed-and-reference rule is non-negotiable. Always post-processed. |
|
||||
| **D · Library illustration** | Storyset / Humaaans / unDraw, customised with brand colours. | When budget and timeline force a shortcut — and even then, never unmodified. |
|
||||
| **E · Lottie animation** | LAST RESORT. Only when complex character motion can't be hand-built. | Articulated figures, multi-frame mascot loops. Never for "spinning logo" or "checkmark draw" — those are CSS. |
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ The table below is alphabetical by theme to neutralise any "first row = default"
|
||||
| Newsprint | Bottom-aligned | Multi-column justified | Double rule | Outlined | Inline | None | N6 Masthead | Ft4 Dense colophon |
|
||||
| Pastel | Centered | Single column | Negative space | Outlined | None | Fade-up | N5 Floating pill | Ft2 Inline single line |
|
||||
| Plume | Hanging | Single column | Bleed-colour band | Outlined | Margin-aligned | Fade-up | N9 Edge-min | Ft1 Mast-headed |
|
||||
| Editorial | Hanging | 2-col asym hero / single below | Hairline | Outlined | Tightly cropped or generated (Tier C) | Fade-up | N6 Masthead | Ft1 Mast-headed |
|
||||
| Quiet | Centered | Single column narrow | Negative space | Outlined pill | None | None | N9 Edge-min | Ft2 Inline single line |
|
||||
| Riso | Centered | Single column | Negative space | Outlined | Inline | None | N7 Brutal slab | Ft8 Marquee scroll |
|
||||
| Salon | Centered | Single column narrow | Ornament (fleuron) | Outlined | Tightly cropped | None | N6 Masthead | Ft1 Mast-headed |
|
||||
|
||||