mirror of
https://github.com/Nutlope/hallmark.git
synced 2026-08-14 12:35:33 +02:00
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:
co-authored by
Claude Opus 4.7
parent
28ff81b8e4
commit
ac3a94f90e
@@ -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 2–3 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 (39–40: max three font families per page, outlier face used in ≤ 2 slots) and input-state gates (41–45: 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 (39–40: max three font families per page, outlier face used in ≤ 2 slots) and input-state gates (41–45: 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 3–5 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 2–3 example opening lines tuned to its tone. "Built for the modern team" is in the banned-phrases list.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 46–50) / 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 (46–50)
|
||||
*/
|
||||
```
|
||||
|
||||
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 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 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.
|
||||
|
||||
|
||||
@@ -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 46–50 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.
|
||||
|
||||
@@ -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 (46–50)` 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.
|
||||
|
||||
@@ -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`).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user