W/W gap fix · Foundations refactor · 4 theme polishes · component-scope flow · contrast gates (50 total)

Site:
- Section 04 W/W gap fix: .section gets position:relative, .vs-toggle__input goes position:absolute (top:0, left:0). The hidden radio inputs no longer take grid slots — eliminates the ~36rem of phantom gap that was accumulating between section header and toggle, most visible on Specimen.
- Section 05 Foundations refactor: 5 stacked cards → vertical list + active panel. Click a name (01 Type · 02 Colour · 03 Space · 04 Motion · 05 Voice), the right panel updates. Pure CSS via 5 radios + sibling selectors. Section now fits in ~720 px on desktop. Mobile collapses to accordion via grid `order:`.
- Theme polish:
  - Pastel: cool tilt (paper oklch 96% 0.022 285 → 98% 0.008 240); lower-chroma indigo accent. Removes overlap with Plume's warm-cream.
  - Studio: portfolio rhythm — section-label gains "· selected work" suffix; hero photo crops 16/7; example-card names italicised; quote block bumped to 4.75rem display.
  - Atelier: sketchbook signals — italic optical-size 144 + soft 70 on display; old-style figures in body; "· plate" suffix on section-label; tabular nums on numerals only; "see plate" prefix on example-card open links.
  - Sport: scoreboard register — tabular numerals across stat / num / step / dna__v / vs-toggle__num; section-numeral becomes a leaderboard cell with thin top-border + accent tint; sharper sweep easing (cubic-bezier(0.86, 0, 0.07, 1)); hero qualifier set in mono uppercase.

Skill:
- New § "When the brief is a component, not a page" sits before Design flow in SKILL.md. Detects single-component briefs (button / input / card / modal / dropdown / etc.), runs a slimmed flow that keeps pre-flight + genre + theme + 2+1 fonts + state discipline + slop test, but skips macrostructure / enrichment / project-memory append. Outputs the component artifact PLUS an 8-state demo wrapper (.preview.html) so the user sees default · hover · focus · active · disabled · loading · error · success at a glance. Stamps with `component:` prefix to opt out of page diversification rules.
- Contrast & readability gates 46–50 added to slop-test.md (universal, all genres):
  - 46 body text < 4.5:1 contrast
  - 47 large text / icons / focus rings < 3:1 contrast
  - 48 button text colour ≈ background fill (the black-on-black canary)
  - 49 --color-accent used as fill but --color-accent-ink missing
  - 50 dark surface inheriting the page-default ink colour (ink-on-ink in flipped sections)
- New "Contrast discipline" subsection in interaction-and-states.md with APCA / WCAG computation rules + the surface-flip rule (any background override must also state color).
- Slop-test count 45 → 50 across SKILL.md, slop-test.md, custom-theme.md, redesign.md, README, site copy fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Youssef
2026-05-04 13:57:32 +01:00
co-authored by Claude Opus 4.7
parent 28ff81b8e4
commit ac3a94f90e
11 changed files with 632 additions and 90 deletions
+3 -3
View File
@@ -3,7 +3,7 @@
**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 45-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).
→ 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 50-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).
<table>
<tr>
@@ -97,7 +97,7 @@ Each page is its own self-contained HTML + CSS — no shared theme, no shared la
## What's inside
- **[`SKILL.md`](skill/SKILL.md)** — the routing file. Six-step design flow (including `Step 2.5 · Check project memory` reading `.hallmark/log.json`), 45-gate slop test, output contract, always-on `tokens.css` export.
- **[`SKILL.md`](skill/SKILL.md)** — the routing file. Six-step design flow (including `Step 2.5 · Check project memory` reading `.hallmark/log.json`), 50-gate slop test, output contract, always-on `tokens.css` export.
- **[`references/`](skill/references/)** — short, opinionated rule files: typography, colour, layout, motion, microinteractions, interaction-and-states (with the input-state checklist), responsive, copy, anti-patterns, the 21 named macrostructures, the 36 component archetypes with variation knobs, the 6 primitive structure axes, the vision-extraction protocol for `study`, hero enrichment, custom-craft (CSS art over Lottie), assets, the slop-test gates, four genre rule-overlays, per-verb dispatchers, and the export-formats reference (Tailwind / DTCG / shadcn / tokens.css).
- **[`docs/`](docs/)** — human-reading content: **[`recipes.md`](docs/recipes.md)** (8 worked briefs + a canonical try-it prompt) and **[`study-examples.md`](docs/study-examples.md)** (3 worked DNA-extractions). Not auto-loaded by the skill.
- **[`site/`](site/)** — a self-demonstrating landing page. Hand-written HTML + CSS + ES module, no framework, no build step. **Twenty-two themes** clustered into four genres: **editorial** (Specimen, Atelier, Newsprint, Salon, Linen, Almanac, Garden, Studio, Sport, Riso, Brutal, Manifesto), **modern-minimal** (Quiet, Coral, Violet), **atmospheric** (Midnight, Terminal, Bloom, Aurora, Halo), **playful** (Pastel, Plume). Switching themes literally rebuilds the page — different hero archetype, different footer archetype.
@@ -112,7 +112,7 @@ Each page is its own self-contained HTML + CSS — no shared theme, no shared la
- **Macrostructures over axes.** Pick one of 21 named whole-page shapes wholesale; the macrostructure stamp lives in the CSS comment, so the next Hallmark run picks something different.
- **Within-archetype variation.** Two Bento Grids should not be twins; each archetype has 23 picked-per-output knobs.
- **Microinteractions as discipline.** Silent success over celebratory toasts. Optimistic update + Undo over confirm dialogs. Hover delay 800 ms, focus delay 0 ms.
- **A 45-gate slop test** runs before every output. One yes fails the build. Recent additions: typography discipline gates (3940: max three font families per page, outlier face used in ≤ 2 slots) and input-state gates (4145: no border-width layout shift, focus ring via outline not border, input height matches button height, helper-text slot reserves height, disabled state needs three independent signals).
- **A 50-gate slop test** runs before every output. One yes fails the build. Recent additions: typography discipline gates (3940: max three font families per page, outlier face used in ≤ 2 slots) and input-state gates (4145: no border-width layout shift, focus ring via outline not border, input height matches button height, helper-text slot reserves height, disabled state needs three independent signals).
- **Project memory.** A per-project `.hallmark/log.json` records each run's macrostructure + theme + enrichment + brief summary. The skill reads the last 35 entries before picking and writes a new entry after each build, so consecutive Hallmark outputs in the same project don't repeat shapes or themes.
- **Theme-diversification rule.** Two consecutive themes must differ on at least one of three axes: paper band (dark / mid / light), display style (italic-serif / roman-serif / geometric-sans / mono / display-heavy / system-native), accent hue (warm / cool / neutral / chromatic-other).
- **Voice fixtures over LLM defaults.** Each of the 21 macrostructures ships with 23 example opening lines tuned to its tone. "Built for the modern team" is in the banned-phrases list.
+327
View File
@@ -681,6 +681,181 @@
.spec__body { margin-top: var(--space-xs); }
/* — Foundations nav (left column of the spec sheet) ——————————
Each item is a clickable label. The active item shows an accent
dot, slightly bolder name, and a subtle inset background. */
.found-nav__btn {
display: grid;
grid-template-columns: 2.5rem 1fr 0.5rem;
align-items: baseline;
gap: var(--space-sm);
padding: var(--space-md) 0 var(--space-md) var(--space-sm);
border-block-end: var(--rule-hair) solid var(--color-rule);
cursor: pointer;
user-select: none;
transition: background-color var(--dur-micro) var(--ease-out),
padding-inline-start var(--dur-short) var(--ease-out);
}
.found-nav__btn:hover {
background: var(--color-paper-2);
}
.found-nav__num {
font-family: var(--font-label);
font-size: var(--text-xs);
letter-spacing: var(--tracking-label);
color: var(--color-muted);
font-feature-settings: "tnum";
}
.found-nav__name {
font-family: var(--font-display);
font-style: var(--display-style, normal);
font-weight: var(--display-weight, 500);
font-size: var(--text-lg);
letter-spacing: var(--tracking-tight);
color: var(--color-ink-2);
line-height: 1;
}
.found-nav__dot {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: transparent;
align-self: center;
justify-self: end;
transition: background-color var(--dur-short) var(--ease-out);
}
[data-theme="riso"] .found-nav__name { text-transform: lowercase; }
[data-theme="brutal"] .found-nav__name,
[data-theme="manifesto"] .found-nav__name,
[data-theme="sport"] .found-nav__name { text-transform: uppercase; }
/* Active state — driven by which radio is :checked */
#f-type:checked ~ .found-grid .found-nav__btn[for="f-type"],
#f-colour:checked ~ .found-grid .found-nav__btn[for="f-colour"],
#f-space:checked ~ .found-grid .found-nav__btn[for="f-space"],
#f-motion:checked ~ .found-grid .found-nav__btn[for="f-motion"],
#f-voice:checked ~ .found-grid .found-nav__btn[for="f-voice"] {
background: color-mix(in oklch, var(--color-accent) 5%, transparent);
padding-inline-start: var(--space-md);
}
#f-type:checked ~ .found-grid .found-nav__btn[for="f-type"] .found-nav__name,
#f-colour:checked ~ .found-grid .found-nav__btn[for="f-colour"] .found-nav__name,
#f-space:checked ~ .found-grid .found-nav__btn[for="f-space"] .found-nav__name,
#f-motion:checked ~ .found-grid .found-nav__btn[for="f-motion"] .found-nav__name,
#f-voice:checked ~ .found-grid .found-nav__btn[for="f-voice"] .found-nav__name {
color: var(--color-ink);
}
#f-type:checked ~ .found-grid .found-nav__btn[for="f-type"] .found-nav__dot,
#f-colour:checked ~ .found-grid .found-nav__btn[for="f-colour"] .found-nav__dot,
#f-space:checked ~ .found-grid .found-nav__btn[for="f-space"] .found-nav__dot,
#f-motion:checked ~ .found-grid .found-nav__btn[for="f-motion"] .found-nav__dot,
#f-voice:checked ~ .found-grid .found-nav__btn[for="f-voice"] .found-nav__dot {
background: var(--color-accent);
}
/* Focus-visible outline on the *button* when its corresponding radio
is focused (radios are off-screen but keyboard-focusable). */
#f-type:focus-visible ~ .found-grid .found-nav__btn[for="f-type"],
#f-colour:focus-visible ~ .found-grid .found-nav__btn[for="f-colour"],
#f-space:focus-visible ~ .found-grid .found-nav__btn[for="f-space"],
#f-motion:focus-visible ~ .found-grid .found-nav__btn[for="f-motion"],
#f-voice:focus-visible ~ .found-grid .found-nav__btn[for="f-voice"] {
outline: 2px solid var(--color-focus);
outline-offset: -2px;
}
/* — Foundations panels (right column) —————————————————————— */
.found-panels {
position: relative;
min-height: 24rem;
}
.found-panel {
display: none;
padding-inline-start: var(--space-xl);
border-inline-start: var(--rule-hair) solid var(--color-rule);
}
@media (max-width: 60rem) {
.found-panel {
padding-inline-start: 0;
border-inline-start: 0;
padding-block: var(--space-md) var(--space-xl);
}
}
#f-type:checked ~ .found-grid .found-panels .found-panel[data-panel="type"],
#f-colour:checked ~ .found-grid .found-panels .found-panel[data-panel="colour"],
#f-space:checked ~ .found-grid .found-panels .found-panel[data-panel="space"],
#f-motion:checked ~ .found-grid .found-panels .found-panel[data-panel="motion"],
#f-voice:checked ~ .found-grid .found-panels .found-panel[data-panel="voice"] {
display: grid;
gap: var(--space-lg);
animation: found-fade-in 280ms var(--ease-out);
}
@keyframes found-fade-in {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
.found-panel { animation: none !important; }
}
.found-panel__rule {
margin: 0;
font-family: var(--font-serif);
font-style: italic;
font-size: var(--text-md);
line-height: var(--lh-normal);
color: var(--color-ink-2);
max-width: 56ch;
}
[data-theme="brutal"] .found-panel__rule,
[data-theme="manifesto"] .found-panel__rule,
[data-theme="terminal"] .found-panel__rule,
[data-theme="sport"] .found-panel__rule,
[data-theme="almanac"] .found-panel__rule,
[data-theme="quiet"] .found-panel__rule,
[data-theme="bloom"] .found-panel__rule,
[data-theme="aurora"] .found-panel__rule,
[data-theme="halo"] .found-panel__rule,
[data-theme="coral"] .found-panel__rule,
[data-theme="violet"] .found-panel__rule {
font-family: var(--font-body);
font-style: normal;
}
.found-panel__viz {
margin-block-start: 0;
}
/* Mobile accordion — each panel tucks under its corresponding nav row */
@media (max-width: 60rem) {
.found-grid > .found-panels { display: contents; }
.found-panel { order: 0; }
.found-nav__btn[for="f-type"] { order: 1; }
.found-panel[data-panel="type"] { order: 2; }
.found-nav__btn[for="f-colour"] { order: 3; }
.found-panel[data-panel="colour"] { order: 4; }
.found-nav__btn[for="f-space"] { order: 5; }
.found-panel[data-panel="space"] { order: 6; }
.found-nav__btn[for="f-motion"] { order: 7; }
.found-panel[data-panel="motion"] { order: 8; }
.found-nav__btn[for="f-voice"] { order: 9; }
.found-panel[data-panel="voice"] { order: 10; }
}
/* — Swatches ——————————————————————————————————————— */
.swatch-row {
display: grid;
@@ -1820,6 +1995,114 @@
font-feature-settings: "ss01";
}
/* Portfolio / gallery rhythm — distinguishes Studio from Specimen.
Section heads gain a "Selected work · 0X" register; the photographic
hero crops more aggressively (16/7 ratio); pull-quote treatment leans
editorial-magazine instead of agency-deck. */
[data-theme="studio"] .section-label::after {
content: " · selected work";
color: var(--color-muted);
font-style: italic;
text-transform: lowercase;
letter-spacing: 0;
margin-inline-start: 0.4em;
font-feature-settings: "ss01";
}
[data-theme="studio"] .hero__photo {
aspect-ratio: 16 / 7;
border-radius: 0;
border-color: var(--color-rule-2);
}
[data-theme="studio"] .hero__photo-caption {
font-style: italic;
letter-spacing: 0;
text-transform: none;
font-feature-settings: "ss01";
}
[data-theme="studio"] .ex-card {
background: var(--color-paper-2);
border-color: var(--color-rule-2);
}
[data-theme="studio"] .ex-card__name {
font-style: italic;
}
[data-theme="studio"] .hero__quote p {
font-size: clamp(2.5rem, 4vw + 1rem, 4.75rem);
font-variation-settings: "opsz" 144, "SOFT" 50;
}
/* ───────────── ATELIER — sketchbook / working-archive ─────────────
Plate-numeral marginalia beside section heads, old-style figures in
body text, an italic display optical-size axis for hand-set feel.
Earns the workshop name without slipping into the salon-formal
register that Salon owns. */
[data-theme="atelier"] .section-label .num {
font-family: var(--font-display);
font-style: italic;
font-weight: 400;
font-size: var(--text-md);
font-variation-settings: "opsz" 96;
color: var(--color-ink);
}
[data-theme="atelier"] .section-label::after {
content: " · plate";
color: var(--color-muted);
font-style: italic;
text-transform: lowercase;
letter-spacing: 0;
margin-inline-start: 0.4em;
}
[data-theme="atelier"] .section__title {
font-variation-settings: "opsz" 144, "SOFT" 60;
}
[data-theme="atelier"] .hero__display,
[data-theme="atelier"] .hero__quote p,
[data-theme="atelier"] .hero__salutation {
font-variation-settings: "opsz" 144, "SOFT" 70;
}
/* Old-style figures in body text — reads as hand-set, not synthesised */
[data-theme="atelier"] .hero__lede,
[data-theme="atelier"] .hero__letter-body,
[data-theme="atelier"] .spec__rule,
[data-theme="atelier"] .found-panel__rule,
[data-theme="atelier"] .install-pane__next,
[data-theme="atelier"] .ex-card__brief {
font-variant-numeric: oldstyle-nums;
}
/* Tabular numerals in the example tile counter — reads as catalogue */
[data-theme="atelier"] .ex-card__num,
[data-theme="atelier"] .gallery__num,
[data-theme="atelier"] .found-nav__num {
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
}
[data-theme="atelier"] .ex-card {
border-style: solid;
border-color: var(--color-rule);
background: var(--color-paper-2);
}
[data-theme="atelier"] .ex-card__open::before {
content: "see plate ";
color: var(--color-muted);
text-transform: lowercase;
font-style: italic;
letter-spacing: 0;
}
/* ───────────── PASTEL — Geist sans display, soft surfaces ───────────── */
[data-theme="pastel"] .hero__display,
@@ -3993,6 +4276,50 @@ html[data-theme="halo"] aside {
line-height: 1.05;
}
/* Sport — kinetic / scoreboard register.
Tabular numerals everywhere a number appears (counter, stat,
pricing, plate numbers); a top-bordered "leaderboard cell" treatment
on the section numeral; sharper sweep easing on .reveal. */
[data-theme="sport"] .hero__stat,
[data-theme="sport"] .ex-card__num,
[data-theme="sport"] .gallery__num,
[data-theme="sport"] .spec__num,
[data-theme="sport"] .found-nav__num,
[data-theme="sport"] .vs-toggle__num,
[data-theme="sport"] .vs__panel-stat-num,
[data-theme="sport"] .vs__panel-price-num,
[data-theme="sport"] .install-pane__step,
[data-theme="sport"] .dna__v {
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
}
/* Section numeral as a leaderboard-style cell with a thin top rule */
[data-theme="sport"] .section-label .num {
display: inline-block;
padding: 0.15em 0.45em 0.05em;
border-block-start: 2px solid var(--color-accent);
background: color-mix(in oklch, var(--color-accent) 6%, transparent);
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
}
/* Sharper sweep easing on Sport reveals — sportier */
[data-theme="sport"] .reveal.is-in {
animation-timing-function: cubic-bezier(0.86, 0, 0.07, 1);
}
/* Hero stat-row treatment — "MIN · 47 · LAP 12" register, when present */
[data-theme="sport"] .hero__qualifier {
font-family: var(--font-mono);
font-style: normal;
font-size: var(--text-md);
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--color-muted);
}
/* ───────────── SALON — true-centred section heads + fleuron ─────────────
Salon's existing grid override (in sections.css) was fine but the
eyebrow + title weren't perfectly stacked. Tighten the gap between
+59 -1
View File
@@ -20,6 +20,7 @@
.section {
display: grid;
gap: var(--space-2xl);
position: relative; /* anchors absolute children (hidden radios) */
}
.section__head {
@@ -214,6 +215,14 @@
the input at top:0 of the viewport; clicking a label far below
would scroll the page back to the top. */
.vs-toggle__input {
/* Pulled out of the section's grid (otherwise each hidden radio plus each
hidden .vs panel adds a `var(--space-2xl)` phantom gap between the
section header and the first visible child). Anchored absolute inside
the section (which is position: relative) so focus doesn't scroll the
page back to the viewport top. */
position: absolute;
top: 0;
left: 0;
appearance: none;
-webkit-appearance: none;
width: 0;
@@ -223,7 +232,6 @@
border: 0;
outline: 0;
opacity: 0;
flex: 0 0 0;
pointer-events: none;
}
@@ -364,6 +372,56 @@
.spec--feature, .spec--strip { grid-column: auto; }
}
/* — 05 · Foundations as vertical list + active panel ———————
Five clickable names on the left; the active card's rule + visual
demo on the right. Pure CSS via :checked + sibling selectors —
the radios sit absolutely-positioned (out of flow, like the W/W
toggle radios). Fits in ~one viewport on desktop. */
.found-toggle__input {
position: absolute;
top: 0;
left: 0;
appearance: none;
-webkit-appearance: none;
width: 0;
height: 0;
margin: 0;
padding: 0;
border: 0;
outline: 0;
opacity: 0;
pointer-events: none;
}
.found-grid {
display: grid;
grid-template-columns: 14rem 1fr;
gap: var(--space-2xl);
align-items: start;
}
@media (max-width: 60rem) {
.found-grid {
grid-template-columns: 1fr;
gap: 0;
}
}
.found-nav {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0;
border-block-start: var(--rule-hair) solid var(--color-rule);
}
.found-nav li { margin: 0; }
@media (max-width: 60rem) {
.found-nav { display: contents; }
}
/* — 05 · How it works ——————————————————————————————— */
.how { display: grid; gap: var(--space-xl); }
+15 -12
View File
@@ -687,18 +687,21 @@
────────────────────────────────────────────────────────────── */
[data-theme="pastel"] {
--color-paper: oklch(96% 0.022 285);
--color-paper-2: oklch(93.5% 0.026 285);
--color-paper-3: oklch(90% 0.030 285);
--color-rule: oklch(82% 0.024 280);
--color-rule-2: oklch(72% 0.030 275);
--color-muted: oklch(50% 0.030 275);
--color-neutral: oklch(38% 0.038 270);
--color-ink-2: oklch(28% 0.040 270);
--color-ink: oklch(22% 0.040 270);
--color-accent: oklch(56% 0.130 282);
--color-accent-ink: oklch(56% 0.130 282);
--color-focus: oklch(56% 0.130 282);
/* Cool near-white paper — distinguishes from Plume's warm cream.
The "design-tool" register: cool ground, low-chroma indigo, soft
surfaces. No more overlap with the warm-soft register. */
--color-paper: oklch(98% 0.008 240);
--color-paper-2: oklch(96% 0.010 240);
--color-paper-3: oklch(93% 0.012 240);
--color-rule: oklch(88% 0.012 240);
--color-rule-2: oklch(76% 0.014 240);
--color-muted: oklch(52% 0.018 250);
--color-neutral: oklch(40% 0.022 255);
--color-ink-2: oklch(28% 0.025 260);
--color-ink: oklch(20% 0.028 265);
--color-accent: oklch(54% 0.110 282); /* slightly lower chroma indigo */
--color-accent-ink: oklch(98% 0.008 240);
--color-focus: oklch(54% 0.110 282);
--font-display: "Geist", "Söhne", ui-sans-serif, system-ui, sans-serif;
--font-body: "Geist", "Söhne", ui-sans-serif, system-ui, sans-serif;
+63 -55
View File
@@ -91,7 +91,7 @@
<button class="banner__dot" type="button" data-theme-btn="studio" aria-label="Studio"
style="--dot: oklch(96.5% 0.014 95); --dot-edge: oklch(46% 0.140 145);"></button>
<button class="banner__dot" type="button" data-theme-btn="pastel" aria-label="Pastel"
style="--dot: oklch(96% 0.022 285); --dot-edge: oklch(56% 0.130 282);"></button>
style="--dot: oklch(98% 0.008 240); --dot-edge: oklch(54% 0.110 282);"></button>
<button class="banner__dot" type="button" data-theme-btn="riso" aria-label="Riso"
style="--dot: oklch(91% 0.034 30); --dot-edge: oklch(58% 0.170 220);"></button>
<button class="banner__dot" type="button" data-theme-btn="quiet" aria-label="Quiet"
@@ -539,42 +539,67 @@
</div>
</section>
<!-- 05 · FOUNDATIONS — five plain-English rules -->
<!-- 05 · FOUNDATIONS — vertical list + active panel (CSS-only tabs) -->
<section class="section reveal" style="--i:4" aria-labelledby="s5-foundations">
<header class="section__head">
<p class="section-label">
<span class="num">05</span><span class="divider"></span><span>Foundations</span>
</p>
<h2 class="section__title" id="s5-foundations">
Five things Hallmark holds the line on — explained without the jargon.
Five things Hallmark holds the line on.
</h2>
</header>
<div class="foundations">
<input type="radio" name="found-toggle" id="f-type" class="found-toggle__input" checked />
<input type="radio" name="found-toggle" id="f-colour" class="found-toggle__input" />
<input type="radio" name="found-toggle" id="f-space" class="found-toggle__input" />
<input type="radio" name="found-toggle" id="f-motion" class="found-toggle__input" />
<input type="radio" name="found-toggle" id="f-voice" class="found-toggle__input" />
<article class="spec spec--feature">
<header class="spec__head">
<span class="spec__name">Type</span>
<span class="spec__num">F / 01</span>
</header>
<p class="spec__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="spec__body">
<div class="spec__viz spec__viz--type">
<div class="found-grid">
<ol class="found-nav" role="tablist" aria-label="Foundations">
<li><label for="f-type" class="found-nav__btn" data-panel="type" role="tab">
<span class="found-nav__num">01</span>
<span class="found-nav__name">Type</span>
<span class="found-nav__dot" aria-hidden="true"></span>
</label></li>
<li><label for="f-colour" class="found-nav__btn" data-panel="colour" role="tab">
<span class="found-nav__num">02</span>
<span class="found-nav__name">Colour</span>
<span class="found-nav__dot" aria-hidden="true"></span>
</label></li>
<li><label for="f-space" class="found-nav__btn" data-panel="space" role="tab">
<span class="found-nav__num">03</span>
<span class="found-nav__name">Space</span>
<span class="found-nav__dot" aria-hidden="true"></span>
</label></li>
<li><label for="f-motion" class="found-nav__btn" data-panel="motion" role="tab">
<span class="found-nav__num">04</span>
<span class="found-nav__name">Motion</span>
<span class="found-nav__dot" aria-hidden="true"></span>
</label></li>
<li><label for="f-voice" class="found-nav__btn" data-panel="voice" role="tab">
<span class="found-nav__num">05</span>
<span class="found-nav__name">Voice</span>
<span class="found-nav__dot" aria-hidden="true"></span>
</label></li>
</ol>
<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="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>
<div class="spec__viz-row spec__viz-row--mono"><span>Hallmark</span><em class="spec__viz-tag">Mono · 500</em></div>
</div>
</div>
</article>
</article>
<article class="spec spec--feature">
<header class="spec__head">
<span class="spec__name">Colour</span>
<span class="spec__num">F / 02</span>
</header>
<p class="spec__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="spec__body">
<div class="spec__viz spec__viz--colour" aria-hidden="true">
<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="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>
</div>
@@ -585,17 +610,11 @@
<div class="swatch" style="background: var(--color-ink);">Ink</div>
<div class="swatch" style="background: var(--color-accent);">Accent</div>
</div>
</div>
</article>
</article>
<article class="spec">
<header class="spec__head">
<span class="spec__name">Space</span>
<span class="spec__num">F / 03</span>
</header>
<p class="spec__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="spec__body">
<div class="spec__viz spec__viz--space" aria-hidden="true">
<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="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>
<span class="vg-bar vg-bar--md"></span>
@@ -608,17 +627,11 @@
</div>
</div>
<p class="spec__viz-pair"><span>Centred (a tell)</span><span>Asymmetric (designed)</span></p>
</div>
</article>
</article>
<article class="spec">
<header class="spec__head">
<span class="spec__name">Motion</span>
<span class="spec__num">F / 04</span>
</header>
<p class="spec__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="spec__body">
<div class="spec__viz spec__viz--motion">
<article class="found-panel" data-panel="motion">
<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">
<span class="spec__viz-play-icon" aria-hidden="true"></span>
Play
@@ -627,17 +640,11 @@
<span class="spec__viz-block" data-motion-block></span>
</div>
</div>
</div>
</article>
</article>
<article class="spec spec--strip">
<header class="spec__head">
<span class="spec__name">Voice</span>
<span class="spec__num">F / 05</span>
</header>
<p class="spec__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="spec__body">
<div class="spec__viz spec__viz--copy">
<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="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>
<p class="spec__viz-copy-line">Click here</p>
@@ -653,8 +660,9 @@
<p class="spec__viz-copy-line">Notes that think with you</p>
</div>
</div>
</div>
</article>
</article>
</div>
</div>
</section>
@@ -963,7 +971,7 @@
<li>22 themes</li>
<li>21 macrostructures</li>
<li>32 archetypes</li>
<li>45 slop-test gates</li>
<li>50 slop-test gates</li>
</ul>
</div>
<div class="foot__col">
+3 -3
View File
@@ -131,7 +131,7 @@ const COPY = {
proofLabel: "Proof",
proofA: "21 macrostructures",
proofB: "32 component archetypes",
proofC: "45-gate slop test",
proofC: "50-gate slop test",
cta: "Read the rules",
stat: "21",
qualifier: "macrostructures",
@@ -146,7 +146,7 @@ const COPY = {
newsprint: {
eyebrow: "Volume I · Issue 02 · 28 April 2026",
title: HERO_TITLE,
lede: "Twelve themes. Twenty-one named page shapes. Thirty-two component archetypes. A 45-gate slop test that gates every output before it ships. Hallmark is the rulebook the LLM never read.",
lede: "Twelve themes. Twenty-one named page shapes. Thirty-two component archetypes. A 50-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",
@@ -612,7 +612,7 @@ function buildDenseColophon(themeName) {
`# hallmark · v0.8.0 · ${themeName.toLowerCase()}`,
`# build: ${today} · MIT · powered by together ai`,
`#`,
`# stats: 21 macrostructures · 32 archetypes · 22 themes · 45 gates`,
`# stats: 21 macrostructures · 32 archetypes · 22 themes · 50 gates`,
`# catalog (16 named) + custom (per-brand, opt-in)`,
`#`,
`# repo: github.com/Luffixos/hallmark`,
+96 -13
View File
@@ -30,7 +30,90 @@ 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 **17 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 **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).
---
## When the brief is a component, not a page
Before entering the full Design flow, **check scope**. If any of these fire, run the Component-scope flow instead — most day-to-day dev requests are component-shaped, not page-shaped, and the page-level apparatus (macrostructure, hero enrichment, footer archetype, project memory) is wrong for them.
**Component-scope signals:**
- The brief names a single UI element: *a button · an input · a card · a modal · a dropdown · a tooltip · a select · a checkbox · a switch · a tab strip · a chip · a badge · a banner · a snackbar · a popover · a slider · a date picker · an avatar*.
- The brief is short (≤ 30 words) and refers to one element.
- The target file is a single component (e.g., `./Button.tsx`, `./components/Input.css`, `app/components/Card.vue`).
- The user explicitly says *"just the X"*, *"only the Y"*, *"this one element"*, *"a single ___"*.
If two signals fire, route component. If only the page flow fires (multi-section brief, "build me a landing page"), stay in Design flow.
### What Component-scope keeps from the page flow
- **Step 0 · Pre-flight scan** — same. Read existing tokens, fonts, framework, microinteraction stance. A button on a Geist-bodied Tailwind project must adopt those tokens, not invent new ones.
- **Step 1 · Genre detection** — same. Editorial / modern-minimal / atmospheric / playful. The component inherits its surroundings' genre (silent default to editorial when unknown).
- **Step 2.6 · Theme route** — same. If a `tokens.css` or `design.md` exists, the component uses those tokens. Otherwise it asks "is there a system to follow, or should I pick one?" — defaulting to *catalog* if the user is silent.
- **2+1 font discipline** — same.
- **State discipline — STRICTER.** Every interactive component MUST ship code for **all 8 states**: default · hover · `:focus-visible` · `:active` · disabled · loading · error · success. The 8-state checklist in [`interaction-and-states.md`](references/interaction-and-states.md) is mandatory, not advisory.
- **Slop test — universal-only subset.** Run the visual / microinteraction / contrast (gates 4650) / a11y / typography gates. Skip the diversification gates (no `.hallmark/log.json` entry — components don't rotate) and skip the layout-safety gates that assume a full page.
### What Component-scope skips
- **Step 2 · Macrostructure pick.** Components don't have macrostructures. State this explicitly: *"Component-scope: skipping macrostructure."*
- **Step 4 · Enrichment.** No hero illustration, no demo video, no abstract background. The component IS the artifact.
- **Step 5 · Multi-section preview.** Replaced by the 8-state demo wrapper (below).
- **Project-memory append.** No `.hallmark/log.json` entry for component runs. The diversification rule doesn't apply.
### What Component-scope emits
**Two files, side by side:**
1. **The component artifact** — a single self-contained file matching the project's conventions:
- React / Vue / Svelte: `Button.tsx` / `Button.vue` / `Button.svelte`
- Vanilla web: `button.css` + `button.html`
- Tailwind: a `.tsx` with `className` chains AND a `tokens.css` if missing
- The component consumes Hallmark tokens by name (`var(--color-accent)`), never inlines OKLCH values.
2. **An 8-state demo wrapper**`<ComponentName>.preview.html` (or `.preview.tsx`). A small standalone page that renders the component in **all 8 states** stacked vertically, each labelled. The user opens it once, sees the component working, then deletes it. The wrapper is not part of production code. Format:
```
┌──── Button — 8 states ────────────────────────┐
│ │
│ default [ Click me ] │
│ hover [ Click me ] │ ← .is-hover forces :hover styling
│ focus [ Click me ] │ ← .is-focus forces :focus-visible
│ active [ Click me ] │ ← .is-active forces :active
│ disabled [ Click me ] │ ← disabled attr
│ loading [ ⌛ Working… ] │ ← data-state="loading"
│ error [ ⚠ Try again ] │ ← data-state="error"
│ success [ ✓ Saved ] │ ← data-state="success"
│ │
└────────────────────────────────────────────────┘
```
Each labelled row uses a class (e.g. `.is-hover`) that the component's CSS targets in addition to the real pseudo-class, so all 8 states render at once on the demo page. Example:
```css
.btn:hover, .btn.is-hover { background: var(--color-paper-3); }
.btn:focus-visible, .btn.is-focus { outline: 2px solid var(--color-focus); }
.btn:active, .btn.is-active { transform: translateY(1px); }
```
### Stamp format for component output
Components stamp differently from pages:
```css
/* Hallmark · component: <type> · genre: <genre> · theme: <theme>
* states: default · hover · focus · active · disabled · loading · error · success
* contrast: pass (4650)
*/
```
The `component:` prefix tells future Hallmark runs this artifact is component-scoped and shouldn't trigger page-level diversification rules. The `states:` line is a checklist — every state listed must have actual styling in the file.
### When in doubt — ask once
If the brief is ambiguous between component and page (e.g. *"design a pricing section"* — could be one card, could be a whole page), ask one short question: *"One pricing card, or the whole pricing page?"* Default to **component** if the user doesn't engage — single-artifact output is cheaper to redirect than a multi-section page.
---
@@ -111,7 +194,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 17 named themes — Specimen, Atelier, Pastel, Brutal, Salon, Newsprint, Linen, Studio, Manifesto, Terminal, Midnight, Almanac, Garden, Quiet, Riso, Sport, Bloom) 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 22 named themes — Specimen, Atelier, Pastel, Brutal, Salon, Newsprint, Linen, Studio, Manifesto, Terminal, Midnight, Almanac, Garden, Quiet, Riso, Sport, Bloom) 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").
@@ -120,7 +203,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 17 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 22 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):
@@ -201,12 +284,12 @@ 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 48 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 17 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 22 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 17 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 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.
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 45 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.
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 50 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.
The diversification rule is theme-route-blind: a custom run that follows another custom (or a catalog) must differ on at least one of the three axes from the previous entry, same as catalog-vs-catalog. Custom entries record their three axes explicitly into `.hallmark/log.json` (see [`custom-theme.md`](references/custom-theme.md) § F).
@@ -274,7 +357,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
- **Enrichment** · none (typography only)
- **Sections** · Hero · Logos · Stats · Features · Testimonials · Pricing · FAQ · CTA · Footer
- **Motion** · counter · pricing-lift · pulse-once
- **Slop test** · 45 / 45 ✓ (run after Build)
- **Slop test** · 50 / 50 ✓ (run after Build)
- **Diversification** · differs from Pastel on display style + accent hue
```
@@ -285,7 +368,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
3. **Enrichment** — the chosen archetype + tier, or *none (typography only)*.
4. **Sections** — section names separated by ` · `, in DOM order.
5. **Motion** — microinteraction primitives separated by ` · `, or *none — typography only*. Always under three primitives per the [`microinteractions.md`](references/microinteractions.md) hard rules.
6. **Slop test**`45 / 45 ✓` if all gates pass, or `N / 45 — fails: <gate numbers>` if any are open. Run the slop test BEFORE writing this row; the slop test is Step 7.
6. **Slop test**`50 / 50 ✓` if all gates pass, or `N / 50 — fails: <gate numbers>` if any are open. Run the slop test BEFORE writing this row; the slop test is Step 7.
7. **Diversification** *(optional, only when `.hallmark/log.json` has prior entries)* — what axes differ vs the previous run.
**Three more sample preview blocks** for the model to imitate, varied across macrostructure types:
@@ -298,7 +381,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
> - **Enrichment** · Tier-B hand-built SVG (a 60-line coffee bean with `@property --rise` 6 s breathing-loop)
> - **Sections** · Masthead · Letter · Three Notes · Visit · Colophon
> - **Motion** · breathing-loop on bean only (respects `prefers-reduced-motion`)
> - **Slop test** · 45 / 45 ✓
> - **Slop test** · 50 / 50
> - **Diversification** · first run for this project
*Bento Grid (SaaS, motion-on):*
@@ -309,7 +392,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
> - **Enrichment** · E1 Clipped-Edge Demo Video, Tier-A CSS-art trace waterfall
> - **Sections** · Hero · 6-tile Bento (stat · sparkline · quote · code · integrations · spotlight) · Index Footer
> - **Motion** · counter · pricing-lift · CSS marquee on integrations strip
> - **Slop test** · 45 / 45 ✓
> - **Slop test** · 50 / 50
> - **Diversification** · differs from Plain on paper hue (light-cool vs pure-white) + accent (indigo vs ink-blue)
*Manifesto (declarative, no enrichment):*
@@ -320,7 +403,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
> - **Enrichment** · none (typography only — voice carries the brand)
> - **Sections** · Masthead · Title · Five Declarations · Bleed Band · What We Refuse · Working Rules · Practice · Reading · Colophon
> - **Motion** · none — typography only
> - **Slop test** · 45 / 45 ✓
> - **Slop test** · 50 / 50
> - **Diversification** · differs from Linen on paper band (dark vs light) + display style (display-heavy vs geometric-sans)
*Custom (Coffeebox archival café):*
@@ -331,7 +414,7 @@ Before emitting any code, output a tight summary of what you're about to ship. T
> - **Enrichment** · Tier-A pure-CSS coffee bean (60-line SVG, breathing-loop optional)
> - **Sections** · Masthead · Letter · Three Notes · Visit · Colophon
> - **Motion** · breathing-loop on bean (with reduced-motion fallback)
> - **Slop test** · 45 / 45 ✓
> - **Slop test** · 50 / 50
> - **Diversification** · custom axes: light / italic-serif / chromatic-terracotta — differs from previous catalog Linen on accent hue + display style
If any slop-test gate fails when you reach Step 7, return to the relevant Build step, fix it, and **re-emit the preview block** with the corrected slop-test row. The preview is the durable summary; it's wrong to ship if it lies.
@@ -359,7 +442,7 @@ Always:
### 7. The slop test
Before handing back, run the output through the 45-gate slop test in [`references/slop-test.md`](references/slop-test.md). Every answer must be **no**. Load that file at this step (not earlier — it isn't needed until handoff). The active genre matters: some gates are universal, some are genre-scoped (atmospheric loosens the radial-bloom gate; modern-minimal loosens the zero-chroma neutral gate; etc.). The full per-genre overrides are listed inline in `slop-test.md`.
Before handing back, run the output through the 50-gate slop test in [`references/slop-test.md`](references/slop-test.md). Every answer must be **no**. Load that file at this step (not earlier — it isn't needed until handoff). The active genre matters: some gates are universal, some are genre-scoped (atmospheric loosens the radial-bloom gate; modern-minimal loosens the zero-chroma neutral gate; etc.). The full per-genre overrides are listed inline in `slop-test.md`.
Run the slop test BEFORE writing the Slop test row in the Step 5 preview block — that row reflects the actual outcome of this step.
+1 -1
View File
@@ -2,7 +2,7 @@
Loaded only when the user has opted into the **custom** theme route in Step 1 of the Design flow. Custom produces a **complete OKLCH palette + free-font pairing tuned to this brief** — a one-off theme written inline into the page's `:root`, not a permanent catalog entry.
**The freedom is the combination, not the rules.** Every constraint in [`color.md`](color.md), [`typography.md`](typography.md), and [`anti-patterns.md`](anti-patterns.md) still applies. The 45 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.
**The freedom is the combination, not the rules.** Every constraint in [`color.md`](color.md), [`typography.md`](typography.md), and [`anti-patterns.md`](anti-patterns.md) still applies. The 50 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.
## Two words, plain English
@@ -164,3 +164,44 @@ The outline starts transparent at 2 px so when the focus ring appears, the box g
- Disabled elements with no explanation of why they're disabled.
- Colour-only error states.
- Spinners where a skeleton would show layout.
---
## Contrast discipline
Hallmark output must pass slop-test gates 4650 before shipping. Compute contrast for every `(color, background-color)` pair on the page. The common failures Hallmark output trips on:
1. **Text on a flipped surface.** `.section--ink { background: var(--color-ink); }` flips the surface dark; nested text still inherits `color: var(--color-ink)` → ink-on-ink. Fix: any rule that sets a dark `background` must *also* set `color: var(--color-paper)` in the same rule.
2. **Button text on accent fill.** `background: var(--color-accent); color: white;` — but white is 4.5:1 against this accent only if `--color-accent` is dark enough. Use `var(--color-accent-ink)` instead, which the theme guarantees passes ≥ APCA Lc 60.
3. **Muted text on tinted paper.** `color: var(--color-muted); background: var(--color-paper-3);` — both mid-lightness, often falls below 4.5:1. Use `--color-neutral` (darker) or lift the background to `--color-paper`.
4. **Focus ring on accent-coloured button.** `outline: 2px solid var(--color-focus);` on a button whose fill is `--color-accent` — if `--color-focus = --color-accent`, the ring vanishes. Use the contrast pair: `--color-focus` set to a colour with ≥ 3:1 against both the element and the page.
### Computation
For each `(text-colour, background-colour)` pair the page actually renders:
- Run **APCA Lc** (preferred — perceptual) or **WCAG 2.1 ratio**.
- Pre-check: if both are in OKLCH with `|L_a L_b| < 50 %`, flag for full check.
- Body text passes at **APCA Lc ≥ 60** ≈ WCAG 4.5:1.
- Large text / focus rings / icons pass at **APCA Lc ≥ 45** ≈ WCAG 3:1.
### Token contract
Every theme MUST define `--color-accent-ink` — the text colour to use whenever `--color-accent` fills a surface that carries text. The accent-ink colour is verified ≥ APCA Lc 60 against the accent at the time the theme is built. Hallmark code that uses `background: var(--color-accent)` must also set `color: var(--color-accent-ink)`. Falling back to hardcoded `color: white` is a tell — the theme's accent could be a light colour, and white-on-light is the bug.
### When the surface flips
The rule: **any rule that overrides `background-color` must also state the appropriate `color`.** Don't rely on inheritance for surface-flipping classes. Example:
```css
/* WRONG — text inherits color: var(--color-ink); section is now dark; ink-on-ink */
.section--manifesto { background: var(--color-ink); }
/* RIGHT */
.section--manifesto {
background: var(--color-ink);
color: var(--color-paper);
}
```
Same applies to per-theme overrides like `[data-theme="manifesto"] .vs__col:first-child { background: var(--color-ink); }` — set `color: var(--color-paper)` at the same time, OR declare the rule on a parent and let descendants inherit explicitly.
+23 -1
View File
@@ -1,4 +1,4 @@
# Slop test — 45 gates
# Slop test — 50 gates
Run this list before handing back any output. Every answer must be **no**. Update the Step 5 preview block's `Slop test` row to reflect the actual outcome of this run.
@@ -91,6 +91,28 @@ Some gates are **universal** (apply to every genre); some are **genre-scoped** (
44. Is the helper-text container collapsed when no helper or error is shown? The helper slot must reserve `min-height: 1lh` even when empty so that an error appearing doesn't push the page down. Vertical jump on validation is a tell.
45. Is the disabled input state signalled by *only* `opacity: 0.5`? Disabled needs three independent signals: `opacity: 0.55` AND `cursor: not-allowed` AND `aria-disabled="true"` (or the native `disabled` attribute). One channel is missable; three are not.
## Contrast & readability
Universal — apply to every genre. These gates catch the real-world failures the user flagged: black-text-on-black-button, dark sections with unreadable text, ink-on-ink slop where the LLM forgot to flip the text colour after flipping the surface.
Contrast computation: for every `(color, background-color)` pair on the page, run **APCA Lc** OR **WCAG 2.1 ratio**. OKLCH lightness is a fast pre-check — if `|L_text L_bg| < 50 %`, the pair likely fails 4.5:1 — confirm with a full calculation.
Thresholds:
- Body text (under 24 px regular OR under 18 px bold): **WCAG 4.5:1 / APCA Lc ≥ 60**.
- Large text / icons / focus rings: **WCAG 3:1 / APCA Lc ≥ 45**.
46. Does any **body text** have a contrast ratio below **4.5:1** against its computed background? Pair every `color` declaration with its effective `background-color` and verify. The most-missed cases are: text inside cards that inherit `color` but the card switched to `background: var(--color-paper-2)` and the text is now too close in lightness; muted text (`color: var(--color-muted)`) on `background: var(--color-paper-3)` — both are mid-lightness and fail.
47. Does any **large text** (≥ 24 px regular OR ≥ 18 px bold) or **icon** or **`:focus-visible` ring** have a contrast ratio below **3:1** against its background? Same calculation, looser threshold. Specifically check focus rings — `outline: 2px solid var(--color-focus)` only passes if `--color-focus` has ≥ 3:1 contrast against *both* the element and the page surface.
48. Does any **button** have `color``background-color` on its fill? The canary check: if the computed text colour and the computed background colour are within **5 % lightness AND 0.05 chroma** in OKLCH, fail the gate. This catches the common bug where `color: var(--color-ink)` sits on `background: var(--color-ink)` (black-on-black slop) — the LLM forgot to use `--color-accent-ink` (or `--color-paper`) for text-on-fill.
49. When `--color-accent` is used as a fill anywhere on the page (button, badge, surface), is `--color-accent-ink` **also defined** (in `:root` or theme tokens) AND used as the `color` for text on that fill? If `--color-accent-ink` is missing, Hallmark output is one careless `color: white` away from a low-contrast accident. The token must exist, must verify ≥ APCA Lc 60 / WCAG 4.5:1 against `--color-accent`, and must be applied wherever accent fills a surface that carries text.
50. Does any **dark section** (a section or panel whose `background-color` has OKLCH lightness < 50 %) carry text that uses the page-default `color: var(--color-ink)` — i.e. ink-on-ink in a section that flipped its surface? Sections that swap to a dark surface MUST also swap their text colour (typically to `--color-paper`) and ensure nested children inherit. The fix is explicit: any class that sets `background: <dark>` must also set `color: <light>` in the same rule, OR be wrapped in a parent that does. The most common failure: a `.vs__col:first-child` painted with the accent or ink colour but the inner panels still using default ink-coloured text.
The CSS stamp at Step 6 should record the result: `· contrast: pass (4650)` if all five gates pass, or `· contrast: FAIL gates <list>` if any are open. Fix before shipping.
---
If any answer is **yes**, fix it. Do not ship slop.
+1 -1
View File
@@ -209,7 +209,7 @@ For each target page:
### 4. Diversification rule — INVERTED for multi-page
Across pages of the same app, the diversification rule is *inverted*: consecutive pages MUST share theme, accent, type pairing. They may differ on macrostructure within the family. The 45 slop-test gates that check "differs from previous Hallmark run" are skipped for `designed-as-app` outputs — the system overrides the catalog rotation here.
Across pages of the same app, the diversification rule is *inverted*: consecutive pages MUST share theme, accent, type pairing. They may differ on macrostructure within the family. The 50 slop-test gates that check "differs from previous Hallmark run" are skipped for `designed-as-app` outputs — the system overrides the catalog rotation here.
Pages that drift from `design.md` are slop. The audit verb flags `design.md` drift as a critical structural finding (`stamp-vs-design.md disagreement`).