diff --git a/site/_tests/_thumbs/BananaStudio-loop.mp4 b/site/_tests/_thumbs/BananaStudio-loop.mp4 deleted file mode 100644 index 521bd49..0000000 Binary files a/site/_tests/_thumbs/BananaStudio-loop.mp4 and /dev/null differ diff --git a/site/_tests/_thumbs/BananaStudio-still.jpg b/site/_tests/_thumbs/BananaStudio-still.jpg deleted file mode 100644 index 40b5625..0000000 Binary files a/site/_tests/_thumbs/BananaStudio-still.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/Hyperlane-example.mp4 b/site/_tests/_thumbs/Hyperlane-example.mp4 deleted file mode 100644 index c8c75e5..0000000 Binary files a/site/_tests/_thumbs/Hyperlane-example.mp4 and /dev/null differ diff --git a/site/_tests/_thumbs/Hyperlane-still.jpg b/site/_tests/_thumbs/Hyperlane-still.jpg deleted file mode 100644 index a6a4407..0000000 Binary files a/site/_tests/_thumbs/Hyperlane-still.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/Najm-loop.mp4 b/site/_tests/_thumbs/Najm-loop.mp4 deleted file mode 100644 index e0167fb..0000000 Binary files a/site/_tests/_thumbs/Najm-loop.mp4 and /dev/null differ diff --git a/site/_tests/_thumbs/Najm-still.jpg b/site/_tests/_thumbs/Najm-still.jpg deleted file mode 100644 index 5b93b16..0000000 Binary files a/site/_tests/_thumbs/Najm-still.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/SaaS-loop.mp4 b/site/_tests/_thumbs/SaaS-loop.mp4 deleted file mode 100644 index a3ce234..0000000 Binary files a/site/_tests/_thumbs/SaaS-loop.mp4 and /dev/null differ diff --git a/site/_tests/_thumbs/SaaS-still.jpg b/site/_tests/_thumbs/SaaS-still.jpg deleted file mode 100644 index da0c648..0000000 Binary files a/site/_tests/_thumbs/SaaS-still.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/Soroe-loop.mp4 b/site/_tests/_thumbs/Soroe-loop.mp4 deleted file mode 100644 index f09c295..0000000 Binary files a/site/_tests/_thumbs/Soroe-loop.mp4 and /dev/null differ diff --git a/site/_tests/_thumbs/Soroe-still.jpg b/site/_tests/_thumbs/Soroe-still.jpg deleted file mode 100644 index f5337e1..0000000 Binary files a/site/_tests/_thumbs/Soroe-still.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/anya-loop.mp4 b/site/_tests/_thumbs/anya-loop.mp4 deleted file mode 100644 index 2e84df3..0000000 Binary files a/site/_tests/_thumbs/anya-loop.mp4 and /dev/null differ diff --git a/site/_tests/_thumbs/anya-still.jpg b/site/_tests/_thumbs/anya-still.jpg deleted file mode 100644 index 9319f96..0000000 Binary files a/site/_tests/_thumbs/anya-still.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/custom-01.jpg b/site/_tests/_thumbs/custom-01.jpg deleted file mode 100644 index 3467c77..0000000 Binary files a/site/_tests/_thumbs/custom-01.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/custom-02.jpg b/site/_tests/_thumbs/custom-02.jpg deleted file mode 100644 index 4d3a3e8..0000000 Binary files a/site/_tests/_thumbs/custom-02.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/custom-03.jpg b/site/_tests/_thumbs/custom-03.jpg deleted file mode 100644 index 9061454..0000000 Binary files a/site/_tests/_thumbs/custom-03.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/custom-05.jpg b/site/_tests/_thumbs/custom-05.jpg deleted file mode 100644 index 709260c..0000000 Binary files a/site/_tests/_thumbs/custom-05.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/garden-01.jpg b/site/_tests/_thumbs/garden-01.jpg new file mode 100644 index 0000000..201a883 Binary files /dev/null and b/site/_tests/_thumbs/garden-01.jpg differ diff --git a/site/_tests/_thumbs/press-01.jpg b/site/_tests/_thumbs/press-01.jpg new file mode 100644 index 0000000..f93820d Binary files /dev/null and b/site/_tests/_thumbs/press-01.jpg differ diff --git a/site/_tests/_thumbs/riso-01.jpg b/site/_tests/_thumbs/riso-01.jpg new file mode 100644 index 0000000..2e1f0e3 Binary files /dev/null and b/site/_tests/_thumbs/riso-01.jpg differ diff --git a/site/_tests/_thumbs/slow-pour-loop.mp4 b/site/_tests/_thumbs/slow-pour-loop.mp4 deleted file mode 100644 index 5acb338..0000000 Binary files a/site/_tests/_thumbs/slow-pour-loop.mp4 and /dev/null differ diff --git a/site/_tests/_thumbs/slow-pour-still.jpg b/site/_tests/_thumbs/slow-pour-still.jpg deleted file mode 100644 index e100220..0000000 Binary files a/site/_tests/_thumbs/slow-pour-still.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/soroe.jpg b/site/_tests/_thumbs/soroe.jpg deleted file mode 100644 index 09963de..0000000 Binary files a/site/_tests/_thumbs/soroe.jpg and /dev/null differ diff --git a/site/_tests/_thumbs/wayfare-loop.mp4 b/site/_tests/_thumbs/wayfare-loop.mp4 deleted file mode 100644 index 641b258..0000000 Binary files a/site/_tests/_thumbs/wayfare-loop.mp4 and /dev/null differ diff --git a/site/_tests/_thumbs/wayfare-still.jpg b/site/_tests/_thumbs/wayfare-still.jpg deleted file mode 100644 index b06d004..0000000 Binary files a/site/_tests/_thumbs/wayfare-still.jpg and /dev/null differ diff --git a/site/css/tokens.css b/site/css/tokens.css index bd5865d..983436e 100644 --- a/site/css/tokens.css +++ b/site/css/tokens.css @@ -934,13 +934,14 @@ --font-label: "JetBrains Mono", ui-monospace, monospace; --font-mono: "JetBrains Mono", ui-monospace, monospace; - --display-weight: 800; + --display-weight: 700; /* lightened from 800 — 800 + loose tracking read too heavy */ --display-style: normal; --text-display: clamp(3.25rem, 6.5vw + 1rem, 6.5rem); --text-display-s: clamp(2.25rem, 4vw + 0.75rem, 3.5rem); - --tracking-display: 0.04em; /* all-caps, slightly tracked */ + --tracking-display: 0.02em; /* tight, professional (was 0.04em, which read as AI-spread) */ + --tracking-display-loose: 0.04em; /* spread look — marquee banners only */ --tracking-tight: -0.005em; --tracking-label: 0.18em; /* mono all-caps labels */ diff --git a/site/examples/garden-01/index.html b/site/examples/garden-01/index.html new file mode 100644 index 0000000..dd9899c --- /dev/null +++ b/site/examples/garden-01/index.html @@ -0,0 +1,386 @@ + + + + + + Hollowback Apiary — small-batch honey from one Vermont ridge + + + + + + + + + + + + + + + +
+ + +
+
+

Nine hives. One ridge. No blending.

+

+ Honey that tastes like one hillside. +

+

+ We keep bees on a single south-facing ridge above the Lamoille valley and + pour what they make, one season at a time — unfiltered, unblended, never + warmed past the temperature of the hive. +

+ +

+ + Lot No. 14 · poured 4–5 September · 312 jars +

+
+ + +
+ + +
+
+ + +
+
+

+ + A ridge, and what grows on it. +

+

+ Everything in a jar of Hollowback honey was within a bee’s morning flight of + the same nine hives. That is the whole idea, and it is also the whole limit. +

+
+ +
+

+ The hives sit at the top of an old sheep pasture that runs down toward the + Lamoille. From May the bees work the wild raspberry and the clover that took + the field back; by July they are up in the basswood; in the cool weeks before + frost they bring in goldenrod and aster, dark and a little wild on the tongue. +

+

+ We do not move the hives to follow a crop, and we do not buy honey from anyone + else to stretch a season. So a poor June is a smaller June, and the + summer lot says so. We would rather sell less and have it be + honestly from here. +

+

+ The honey is spun cold, strained once through cloth to take out the wax, and + poured by hand the same week. Nothing is heated to make it pour faster or + filtered bright to make it sit longer on a shelf. It will cloud and set in the + cold — that is honey doing what raw honey does. Warm the jar in a bowl of + tap-hot water and it comes back clear. +

+
+ +
+
+
Hives
+
9
+

All on one ridge, all overwintered in place.

+
+
+
Elevation
+
1,180 ft
+

South-facing, above the valley frost line.

+
+
+
Forage
+
Wild
+

Raspberry, clover, basswood, goldenrod, aster.

+
+
+
Pour
+
By hand
+

Cold-spun, strained once, jarred the same week.

+
+
+
+ + +
+
+

This season’s lot

+

+ Four pours from the 2026 season, each from one stretch of the bloom. When a + pour is gone it is gone until the same flowers come round again next year. +

+
+ +
    +
  1. + +
    +

    Lot 14-A · late May

    +

    Raspberry & Clover

    +

    + The first and palest pour. Light, almost floral, the taste of the field + coming back after the sheep. Sets soft and white in the cold. +

    +
    +

    $16 / 12 oz

    + Reserve +
    +

    14 jars left

    +
    +
  2. + +
  3. + +
    +

    Lot 14-B · early July

    +

    Basswood

    +

    + Our shortest bloom and the one we wait for. Cool and green-edged, a little + minty, with the smell of the linden flower still in it. Stays liquid longest. +

    +
    +

    $19 / 12 oz

    + Reserve +
    +

    Sold out for 2026

    +
    +
  4. + +
  5. + +
    +

    Lot 14-C · high summer

    +

    Ridge Summer

    +

    + Everything blooming at once — the household jar. Round and even, equal + parts clover and wildflower. The one we put on toast and never label fussily. +

    +
    +

    $15 / 12 oz

    + Reserve +
    +

    In the pantry now

    +
    +
  6. + +
  7. + +
    +

    Lot 14-D · before frost

    +

    Goldenrod & Aster

    +

    + The last and darkest pour, brought in cool. Malty, almost smoky, with the + bite the goldenrod leaves behind. For cheese, dark bread, strong tea. +

    +
    +

    $17 / 12 oz

    + Reserve +
    +

    In the pantry now

    +
    +
  8. +
+ +

+ Every jar carries its lot letter and pour date. Shipped in straw, padded by hand; + flat-rate within New England, at cost beyond it. +

+
+ + +
+
+

How a jar is filled

+

+ Four steps, and not one of them is in a hurry. The whole point of small-batch is + that no part of it scales. +

+
+
+
+

Spun cold

+

+ Frames come off the ridge and into the shed the same morning. We spin them at + cellar temperature, never warm. Heat makes honey pour fast and lose the smell + of its own flowers; we trade the speed for the smell. +

+
+
+

Strained once

+

+ One pass through a square of cloth takes out the wax and a stray bit of comb. + That is all. We don’t fine it bright or push it through paper — + the pollen and the cloud are the honey, not a flaw in it. +

+
+
+

Settled a week

+

+ The strained honey rests in a covered tank for several days so the last air + and froth rise on their own. No pump, no pressure, nothing forced. It clears + the way a pond clears after rain. +

+
+
+

Poured by hand

+

+ We fill, weigh, and cap each jar one at a time, write the lot and the date on + the label, and pack it in straw. A good day is a few hundred jars — and + then that pour is closed. +

+
+
+
+ + +
+
+
+

Keep a jar standing.

+

+ A standing jar is the simplest way we know to sell honey: tell us your address + once, and one jar of whatever we’re pouring arrives at the turn of each + season — four jars a year, billed the week each ships. Pause or stop with + one reply. No tiers, no app, no club. +

+
+ +
+
+ +
+ + +
+

+ We write once a season, the week a pour ships. Nothing else. +

+
+
+
+
+
+ + + + + + + diff --git a/site/examples/garden-01/script.js b/site/examples/garden-01/script.js new file mode 100644 index 0000000..dac5015 --- /dev/null +++ b/site/examples/garden-01/script.js @@ -0,0 +1,76 @@ +/* Hollowback Apiary — progressive enhancement only. + * Nothing here is required to read the page; it adds the card reveal + * and the standing-jar form's states. No external requests. */ +(function () { + "use strict"; + + var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + /* ── Card reveal: reveal-once on enter, never re-fires (motion.md) ── */ + var cards = document.querySelectorAll(".lot__grid .card"); + if (reduceMotion || !("IntersectionObserver" in window)) { + cards.forEach(function (c) { c.classList.add("in-view"); }); + } else { + var io = new IntersectionObserver(function (entries, obs) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + entry.target.classList.add("in-view"); + obs.unobserve(entry.target); + } + }); + }, { threshold: 0.18, rootMargin: "0px 0px -8% 0px" }); + cards.forEach(function (c) { io.observe(c); }); + } + + /* ── Standing-jar form: idle → loading → success | error ──────────── */ + var form = document.getElementById("standing-form"); + if (!form) return; + + var input = document.getElementById("email"); + var help = document.getElementById("email-help"); + var button = form.querySelector("button[type=submit]"); + var idleText = help ? help.textContent : ""; + var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + function setHelp(state, message) { + if (!help) return; + help.dataset.state = state; + help.textContent = message; + } + + // Clearing an error the moment the field is corrected. + input.addEventListener("input", function () { + if (input.getAttribute("aria-invalid") === "true" && EMAIL.test(input.value.trim())) { + input.removeAttribute("aria-invalid"); + setHelp("idle", idleText); + } + }); + + form.addEventListener("submit", function (e) { + e.preventDefault(); + var value = input.value.trim(); + + // Error: name what's wrong and how to fix it (copy.md error structure). + if (!EMAIL.test(value)) { + input.setAttribute("aria-invalid", "true"); + setHelp("error", "That address is missing an @ or a domain. Try the format you@example.com."); + input.focus(); + return; + } + + // Loading: the effect (an email on file) isn't visible, so we narrate it. + input.removeAttribute("aria-invalid"); + button.disabled = true; + button.textContent = "Adding…"; + setHelp("idle", "Adding your jar to the season list…"); + + // No backend in this demo; simulate the round-trip, then confirm the + // invisible async result (this is the legitimate toast/confirm case). + window.setTimeout(function () { + button.disabled = false; + button.textContent = "Start a standing jar"; + form.reset(); + setHelp("success", "You're on the list. We'll write the week the next pour ships — no jar bills until then."); + }, reduceMotion ? 200 : 850); + }); +})(); diff --git a/site/examples/garden-01/styles.css b/site/examples/garden-01/styles.css new file mode 100644 index 0000000..9bb88aa --- /dev/null +++ b/site/examples/garden-01/styles.css @@ -0,0 +1,859 @@ +/* Hallmark · pre-emit critique: P5 H5 E5 S5 R5 V5 + * genre: editorial · macrostructure: Catalogue · theme: Garden + * H9 hero knobs: build=tier-A CSS comb + tier-B SVG jar, animation=loop(jar-fill on hover) + reveal, scale=dominant + * enrichment: E5 Custom Illustration · craft: tier-A honeycomb wash + tier-B hand-built SVG jars (no real photos) + * F6 product-card knobs: ratio=portrait specimen, density=4-up, micro-action=Reserve → + * nav: N6 Newspaper masthead (issue=below wordmark, rule=double) · footer: Ft6 Letter close (signoff=roman, postscript=yes) + * motion: reveal (stagger by --i, ≤500ms) · jar-fill rise on hover · all transform/opacity, reduced-motion → opacity-only + * accent: leaf-green ~3% + clay/honey on craft only · tone: editorial / craft / honest + * contrast: pass (40–41) · honest: pass (46) · chrome: pass (47) · tokens: pass (48) · responsive: pass (49) + * icons: pass (30, none) · mobile: pass (34, 49, 50–57) · slop: pass (42–45) · studied: no + * diversification: isolated build — no prior .hallmark/log.json or CSS stamp in scope + */ + +/* ── Reset / base ──────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } + +html { + overflow-x: clip; /* gate 34 — no horizontal scroll */ + scroll-behavior: smooth; + -webkit-text-size-adjust: 100%; +} +body { + overflow-x: clip; /* gate 34 fallback for older Safari */ + margin: 0; + background: var(--color-paper); + color: var(--color-ink-2); + font-family: var(--font-body); + font-size: var(--text-md); + font-weight: 400; + line-height: var(--lh-relaxed); + font-variant-numeric: oldstyle-nums; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +h1, h2, h3, p, dl, dd, figure, ol, ul { margin: 0; } +ol, ul { padding: 0; list-style: none; } +img, svg { display: block; max-width: 100%; } + +a { color: inherit; text-decoration: none; } + +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 2px; + border-radius: 3px; +} + +::selection { background: var(--tint-comb); color: var(--color-ink); } + +.skip-link { + position: absolute; + left: var(--space-md); + top: var(--space-md); + z-index: var(--z-skip); + background: var(--color-ink); + color: var(--color-paper); + padding: var(--space-xs) var(--space-md); + border-radius: var(--radius-pill); + font: 500 var(--text-sm)/1 var(--font-label); + letter-spacing: 0.02em; + transform: translateY(-200%); /* transform, not top — gate 14 */ + transition: transform var(--dur-short) var(--ease-out); +} +.skip-link:focus-visible { transform: translateY(0); } + +/* ── Shared layout primitives ──────────────────────────────────── */ +.section__title { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-style: var(--display-style); /* roman — gate 38a */ + font-size: var(--text-3xl); + line-height: var(--lh-tight); + letter-spacing: var(--tracking-tight); + color: var(--color-ink); + overflow-wrap: anywhere; /* gate 51 */ + min-width: 0; + text-wrap: balance; +} + +/* ── Buttons ───────────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; /* gate 36 */ + justify-content: center; + gap: var(--space-xs); + min-height: 44px; /* hit target */ + padding: 0 var(--space-lg); + font: 600 var(--text-base)/1 var(--font-body); /* line-height 1 — gate 36 */ + letter-spacing: 0.005em; + white-space: nowrap; /* gate 49 — never wraps */ + border-radius: var(--radius-pill); + border: var(--rule-fine) solid transparent; + cursor: pointer; + transition: + background-color var(--dur-short) var(--ease-out), + border-color var(--dur-short) var(--ease-out), + color var(--dur-short) var(--ease-out), + transform var(--dur-micro) var(--ease-out); +} +.btn--solid { + background: var(--color-accent); + color: var(--color-accent-ink); +} +.btn--solid:hover { background: var(--color-accent-press); } +.btn--solid:active { transform: translateY(1px); } + +.btn--ghost { + background: transparent; + color: var(--color-ink); + border-color: var(--color-rule-2); +} +.btn--ghost:hover { + border-color: var(--color-accent); + color: var(--color-accent); + background: var(--tint-moss); +} +.btn--ghost:active { transform: translateY(1px); } + +.btn:disabled, +.btn[aria-disabled="true"] { + opacity: 0.55; + cursor: not-allowed; + transform: none; +} + +/* ════════════════════════════════════════════════════════════════ + N6 · NEWSPAPER MASTHEAD + ════════════════════════════════════════════════════════════════ */ +.masthead { + position: sticky; + top: 0; + z-index: var(--z-sticky-nav); /* gate 56 — nav out-paints */ + background: color-mix(in oklch, var(--color-paper) 88%, transparent); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-bottom: var(--rule-fine) solid var(--color-rule-2); + box-shadow: 0 1px 0 var(--color-rule); /* the second of the "double rule" */ +} +.masthead__inner { + max-width: var(--page-max); + margin-inline: auto; + padding: var(--space-sm) var(--page-gutter); + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + "dateline dateline" + "wordmark nav"; + align-items: center; + row-gap: var(--space-2xs); + column-gap: var(--space-md); +} +.masthead__dateline { + grid-area: dateline; + font: 500 var(--text-xs)/1 var(--font-label); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-muted); +} +.masthead__wordmark { + grid-area: wordmark; + font-family: var(--font-display); + font-size: clamp(1.4rem, 1rem + 1.4vw, 1.9rem); + line-height: 1; + color: var(--color-ink); + letter-spacing: var(--tracking-tight); +} +.masthead__nav { + grid-area: nav; + display: flex; + align-items: center; + gap: clamp(var(--space-md), 2vw, var(--space-xl)); + transform: translateY(-3px); /* optical: centre links to the serif wordmark's cap height */ +} +.masthead__nav > a { + font-size: var(--text-sm); + font-weight: 500; + white-space: nowrap; /* gate 49 */ + color: var(--color-ink-2); + padding-block: var(--space-2xs); + position: relative; + transition: color var(--dur-short) var(--ease-out); +} +.masthead__nav > a:not(.masthead__cta)::after { + content: ""; + position: absolute; + left: 0; right: 0; + bottom: -1px; + height: 1.5px; + background: var(--color-accent); + transform: scaleX(0); /* transform, not inset — gate 14 */ + transform-origin: left; + transition: transform var(--dur-short) var(--ease-out); +} +.masthead__nav > a:not(.masthead__cta):hover { color: var(--color-ink); } +.masthead__nav > a:not(.masthead__cta):hover::after { transform: scaleX(1); } + +.masthead__cta { + color: var(--color-accent); + font-weight: 600; +} +.masthead__cta:hover { color: var(--color-ink); } + +/* the masthead links that aren't essential drop on phones */ +@media (max-width: 46rem) { + .masthead__nav > a:not(.masthead__cta) { display: none; } +} + +/* ════════════════════════════════════════════════════════════════ + HERO — typographic, with hand-built craft panel + ════════════════════════════════════════════════════════════════ */ +.hero { + max-width: var(--page-max); + margin-inline: auto; + padding: clamp(2.5rem, 5vw, 4rem) var(--page-gutter) clamp(4rem, 8vw, 7rem); /* asym: end ≥ 1.3× start — gate 44 */ + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr); /* gate 50 */ + gap: clamp(var(--space-xl), 5vw, var(--space-3xl)); + align-items: center; +} +.hero__copy { max-width: 36rem; } + +.hero__eyebrow { + font: 500 var(--text-sm)/1.3 var(--font-label); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-accent); + margin-bottom: var(--space-md); +} +.hero__title { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-style: var(--display-style); + font-size: var(--text-display); + line-height: var(--lh-tight); + letter-spacing: var(--tracking-display); + color: var(--color-ink); + overflow-wrap: anywhere; /* gate 51 */ + min-width: 0; + margin-bottom: var(--space-lg); +} +.hero__lede { + font-size: var(--text-lg); + line-height: var(--lh-normal); + color: var(--color-ink-2); + max-width: 46ch; + margin-bottom: var(--space-xl); +} +.hero__actions { + display: flex; + flex-wrap: wrap; + align-items: center; /* gate 36 */ + gap: var(--space-sm); + margin-bottom: var(--space-xl); +} +.hero__note { + display: flex; + align-items: center; + gap: var(--space-xs); + font: 500 var(--text-sm)/1.2 var(--font-label); + letter-spacing: 0.04em; + color: var(--color-muted); + font-variant-numeric: tabular-nums; +} +.hero__note-mark { + width: 9px; height: 9px; + flex: none; + background: var(--color-accent-2); + border-radius: 2px; + transform: rotate(45deg); +} + +/* ── Hero craft: honeycomb wash + hand-built jar ───────────────── */ +.hero__craft { + position: relative; + display: grid; + place-items: center; + min-height: 22rem; + isolation: isolate; +} +.comb { + position: absolute; + inset: 0; + z-index: -1; + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--space-xs); + padding: var(--space-md); + opacity: 0.9; + -webkit-mask-image: radial-gradient(circle at 55% 45%, var(--mask-matte) 28%, transparent 74%); + mask-image: radial-gradient(circle at 55% 45%, var(--mask-matte) 28%, transparent 74%); +} +.comb > span { + aspect-ratio: 1 / 1.1; + clip-path: polygon(50% 0, 100% 25%, 100% 75%, 50% 100%, 0 75%, 0 25%); + background: var(--tint-comb); + border: 1px solid var(--comb-edge); + transform: translateY(6px); + opacity: 0; + animation: comb-in var(--dur-long) var(--ease-out) forwards; + animation-delay: calc(220ms + var(--c) * 45ms); +} +/* offset alternate columns into a real honeycomb stagger */ +.comb > span:nth-child(4n + 2), +.comb > span:nth-child(4n) { transform: translateY(calc(50% + 6px)); } +@keyframes comb-in { + to { opacity: 1; transform: translateY(0); } +} +.comb > span:nth-child(4n + 2), +.comb > span:nth-child(4n) { + animation-name: comb-in-offset; +} +@keyframes comb-in-offset { + to { opacity: 1; transform: translateY(50%); } +} + +.jar { width: clamp(11rem, 30vw, 15rem); height: auto; filter: drop-shadow(var(--shadow-jar)); } +.jar__glass { + fill: var(--glass); + stroke: var(--color-rule-2); + stroke-width: 2; +} +.jar__honey { fill: var(--color-honey-deep); } +.jar__meniscus { fill: var(--color-honey); } +.jar__lid { fill: var(--color-accent-2); } +.jar__collar { fill: var(--collar); } +.jar__label { + fill: var(--color-paper); + stroke: var(--color-rule); + stroke-width: 1; +} +.jar__rule { stroke: var(--color-muted); stroke-width: 2; stroke-linecap: round; } +.jar__rule--short { stroke: var(--color-rule-2); } +.jar__dipper-stem { stroke: var(--wood); stroke-width: 5; stroke-linecap: round; } +.jar__dipper-head { fill: var(--wood); } + +/* ════════════════════════════════════════════════════════════════ + THE RIDGE — provenance prose + ════════════════════════════════════════════════════════════════ */ +.ridge { + max-width: var(--page-max); + margin-inline: auto; + padding: var(--section-gap) var(--page-gutter) clamp(3rem, 6vw, 5rem); + border-top: var(--rule-hair) solid var(--color-rule); +} +.ridge__head { + display: flex; /* gate 54 — single column */ + flex-direction: column; + gap: var(--section-head-gap); + max-width: 40ch; + margin-bottom: var(--space-2xl); +} +.section__title .sprig { + width: 26px; height: 52px; + display: inline-block; + vertical-align: -0.55em; + margin-inline-end: 0.25em; + color: var(--color-accent); +} +.sprig__stem { fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; } +.sprig__leaf { fill: var(--tint-moss); stroke: currentColor; stroke-width: 1.4; } +.sprig__vein { fill: none; stroke: currentColor; stroke-width: 0.9; opacity: 0.6; stroke-linecap: round; } + +.ridge__standfirst { + font-size: var(--text-lg); + line-height: var(--lh-snug); + color: var(--color-ink); +} +.ridge__prose { + max-width: var(--measure); + display: flex; + flex-direction: column; + gap: var(--space-lg); + font-size: var(--text-md); + line-height: var(--lh-relaxed); + color: var(--color-ink-2); +} +.ridge__prose a { + color: var(--color-accent); + text-decoration: underline; + text-decoration-color: var(--color-rule-2); + text-decoration-thickness: 1.5px; + text-underline-offset: 3px; + transition: text-decoration-color var(--dur-short) var(--ease-out); +} +.ridge__prose a:hover { text-decoration-color: var(--color-accent); } + +.ridge__facts { + margin-top: var(--space-2xl); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(13rem, 100%), 1fr)); + gap: var(--space-md) var(--space-xl); +} +.fact { + border-top: var(--rule-fine) solid var(--color-rule-2); + padding-top: var(--space-sm); +} +.fact dt { + font: 500 var(--text-xs)/1 var(--font-label); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-muted); + margin-bottom: var(--space-xs); +} +.fact dd { + font-family: var(--font-display); + font-size: var(--text-2xl); + line-height: 1; + color: var(--color-ink); + font-variant-numeric: tabular-nums; + margin-bottom: var(--space-xs); +} +.fact p { + font-size: var(--text-sm); + line-height: var(--lh-normal); + color: var(--color-ink-2); +} + +/* ════════════════════════════════════════════════════════════════ + THIS SEASON'S LOT — Catalogue / F6 product-card grid + ════════════════════════════════════════════════════════════════ */ +.lot { + background: var(--tint-comb); + border-block: var(--rule-fine) solid var(--color-rule-2); +} +.lot, +.poured, +.standing { /* full-bleed band; inner content holds the page max */ } +.lot__head, +.lot__grid, +.lot__foot { + max-width: var(--page-max); + margin-inline: auto; + padding-inline: var(--page-gutter); +} +.lot__head { + display: flex; /* gate 54 */ + flex-direction: column; + align-items: flex-start; /* left-flush; coheres with the grid below */ + gap: var(--section-head-gap); + padding-top: var(--section-gap); + margin-bottom: var(--space-2xl); +} +.lot__head > * { max-width: 46ch; } /* cap the text width, not the head's alignment */ +.lot__intro { + font-size: var(--text-lg); + line-height: var(--lh-snug); + color: var(--color-ink-2); +} +.lot__grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(15.5rem, 100%), 1fr)); /* gate 50 safe */ + gap: var(--space-lg); +} +.lot__foot { + margin-top: var(--space-2xl); + padding-bottom: var(--section-gap); + max-width: 56ch; + font-size: var(--text-sm); + line-height: var(--lh-normal); + color: var(--color-muted); +} + +.card { + display: flex; + flex-direction: column; + background: var(--color-paper); + border: var(--rule-card) solid var(--color-rule); + border-radius: var(--radius-card); + overflow: hidden; + box-shadow: var(--shadow-card); + transition: + transform var(--dur-short) var(--ease-out), + box-shadow var(--dur-short) var(--ease-out); +} +/* One signal: the card lifts (translate + its shadow deepen together as a + single "raised" gesture). No separate colour/border shift on hover. */ +@media (hover: hover) and (pointer: fine) { + .card:hover { + transform: translateY(-4px); + box-shadow: var(--shadow-card-hover); + } +} +.card__specimen { + display: grid; + place-items: end center; + padding: var(--space-lg) var(--space-lg) 0; + background: + radial-gradient(120% 80% at 50% 120%, var(--tint-comb), transparent 70%); + min-height: 9.5rem; +} +.vial { width: 4.5rem; height: auto; filter: drop-shadow(var(--shadow-vial)); } +.vial__glass { fill: var(--glass-vial); stroke: var(--color-rule-2); stroke-width: 2; } +.vial__cork { fill: var(--wood); } +/* honey fills — four warm shades, palest→darkest, all token-derived */ +.vial__fill { transform: translateY(0); transition: transform var(--dur-long) var(--ease-out); } +.vial__fill--spring { fill: var(--honey-spring); } +.vial__fill--basswood { fill: var(--honey-bass); } +.vial__fill--summer { fill: var(--color-honey); } +.vial__fill--autumn { fill: var(--honey-autumn); } + +/* jar-fill motion: the honey rises a touch when you pick up the card */ +@media (hover: hover) and (pointer: fine) { + .card:hover .vial__fill { transform: translateY(-7px); } +} + +.card__body { + display: flex; + flex-direction: column; + gap: var(--space-xs); + padding: var(--space-lg); + flex: 1; +} +.card__tag { + font: 500 var(--text-xs)/1 var(--font-label); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-muted); +} +.card__name { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-xl); + line-height: var(--lh-tight); + color: var(--color-ink); + overflow-wrap: anywhere; /* gate 51 */ +} +.card__notes { + font-size: var(--text-sm); + line-height: var(--lh-normal); + color: var(--color-ink-2); + margin-top: var(--space-2xs); +} +.card__foot { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--space-sm); + margin-top: auto; + padding-top: var(--space-sm); +} +.card__price { + display: inline-flex; + align-items: baseline; + gap: 0.3ch; + font-variant-numeric: tabular-nums; + color: var(--color-ink); +} +.card__amount { + font-family: var(--font-display); + font-size: var(--text-lg); + line-height: 1; +} +.card__unit { font-size: var(--text-xs); color: var(--color-muted); } +.card__link { + font: 600 var(--text-sm)/1 var(--font-body); + color: var(--color-accent); + white-space: nowrap; /* gate 49 */ + transition: color var(--dur-short) var(--ease-out); +} +.card__link:hover { color: var(--color-ink); } +.card__stock { + margin-top: var(--space-sm); + padding-top: var(--space-sm); + border-top: var(--rule-hair) solid var(--color-rule); + font: 500 var(--text-xs)/1 var(--font-label); + letter-spacing: 0.04em; + text-transform: uppercase; + display: flex; + align-items: center; + gap: var(--space-xs); + color: var(--color-muted); +} +.card__stock::before { + content: ""; + width: 7px; height: 7px; + border-radius: 50%; + flex: none; + background: var(--color-rule-2); +} +.card__stock[data-stock="in"] { color: var(--color-accent); } +.card__stock[data-stock="in"]::before { background: var(--color-accent); } +.card__stock[data-stock="low"] { color: var(--color-accent-2); } +.card__stock[data-stock="low"]::before { background: var(--color-accent-2); } +.card__stock[data-stock="out"]::before { background: var(--color-muted); } + +/* ════════════════════════════════════════════════════════════════ + HOW A JAR IS FILLED — short process, stacked (no eyebrows) + ════════════════════════════════════════════════════════════════ */ +.poured__head, +.poured__steps { + max-width: var(--page-max); + margin-inline: auto; + padding-inline: var(--page-gutter); +} +.poured__head { + display: flex; /* gate 54 */ + flex-direction: column; + align-items: flex-start; /* left-flush; coheres with the steps below */ + gap: var(--section-head-gap); + padding-top: var(--section-gap); + margin-bottom: var(--space-2xl); +} +.poured__head > * { max-width: 44ch; } +.poured__intro { + font-size: var(--text-lg); + line-height: var(--lh-snug); + color: var(--color-ink-2); +} +.poured__steps { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); + gap: var(--space-xl) var(--space-2xl); + padding-bottom: var(--section-gap); + counter-reset: step; +} +.step { counter-increment: step; } +.step__name { + display: flex; + align-items: baseline; + gap: var(--space-sm); + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-xl); + line-height: var(--lh-tight); + color: var(--color-ink); + margin-bottom: var(--space-sm); + padding-bottom: var(--space-sm); + border-bottom: var(--rule-fine) solid var(--color-rule-2); +} +.step__name::before { + content: counter(step, decimal-leading-zero); + font: 500 var(--text-sm)/1 var(--font-label); + color: var(--color-accent); + letter-spacing: var(--tracking-label); +} +.step p { + font-size: var(--text-base); + line-height: var(--lh-relaxed); + color: var(--color-ink-2); +} + +/* ════════════════════════════════════════════════════════════════ + THE STANDING JAR — CTA (C2 inline form-as-CTA) + ════════════════════════════════════════════════════════════════ */ +.standing { + background: var(--color-ink); + color: var(--color-paper); + border-top: var(--rule-fine) solid var(--color-rule-2); +} +.standing__inner { + max-width: var(--page-max); + margin-inline: auto; + padding: var(--section-gap) var(--page-gutter); + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-2xl); +} +@media (min-width: 60rem) { + .standing__inner { grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr); align-items: center; } +} +.standing__title { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-style: var(--display-style); + font-size: var(--text-display-s); + line-height: var(--lh-tight); + letter-spacing: var(--tracking-tight); + color: var(--color-paper); /* gate 41 — light ink on dark */ + margin-bottom: var(--space-lg); + overflow-wrap: anywhere; +} +.standing__lede { + font-size: var(--text-md); + line-height: var(--lh-relaxed); + color: var(--ink-paper); /* light, ≥4.5:1 on dark ink */ + max-width: 48ch; +} + +/* C2 · inline form-as-CTA */ +.field { display: flex; flex-direction: column; gap: var(--space-sm); } +.field__label { + font: 500 var(--text-xs)/1 var(--font-label); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--ink-label); +} +.field__row { + display: flex; + flex-wrap: wrap; + align-items: center; /* gate 36 */ + gap: var(--space-sm); +} +.field__input { + flex: 1 1 14rem; + min-width: 0; + min-height: 44px; /* matches button — gate 39 */ + padding: 0 var(--space-md); + font: 400 var(--text-base)/1 var(--font-body); + color: var(--color-paper); + background: var(--ink-field); + border: var(--rule-fine) solid var(--color-rule-2); /* width never shifts — gate 39 */ + border-radius: var(--radius-input); + outline: 2px solid transparent; /* reserve geometry — gate 39 */ + outline-offset: 1px; + transition: + border-color var(--dur-short) var(--ease-out), + background-color var(--dur-short) var(--ease-out); +} +.field__input::placeholder { color: var(--ink-placeholder); } +.field__input:hover { border-color: var(--ink-field-hover); } +.field__input:focus-visible { + outline: 2px solid var(--color-accent); /* outline, not border — gate 39 */ + border-color: var(--color-accent); + background: var(--ink-field-2); +} +.field__row .btn--solid { flex: 0 0 auto; } +.field__row .btn--solid:hover { background: var(--color-accent-lift); } + +.field__input[aria-invalid="true"] { + border-color: var(--color-accent-2); +} +.field__help { + min-height: 1.2lh; /* reserve slot — gate 39 */ + font-size: var(--text-sm); + line-height: var(--lh-normal); + color: var(--ink-help); +} +.field__help[data-state="error"] { color: var(--on-dark-error); } +.field__help[data-state="success"] { color: var(--on-dark-success); } + +/* ════════════════════════════════════════════════════════════════ + Ft6 · LETTER CLOSE + ════════════════════════════════════════════════════════════════ */ +.colophon { + background: var(--color-paper); + border-top: var(--rule-hair) solid var(--color-rule); +} +.colophon__inner { + max-width: var(--page-max); + margin-inline: auto; + padding: var(--section-gap) var(--page-gutter) var(--space-2xl); +} +.colophon__close { + max-width: 52ch; + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-2xl); + line-height: var(--lh-snug); + color: var(--color-ink); +} +.colophon__sign { + margin-top: var(--space-xl); + font-size: var(--text-lg); + line-height: var(--lh-snug); + color: var(--color-ink-2); +} +.colophon__name { + font-family: var(--font-display); + color: var(--color-ink); +} +.colophon__ps { + margin-top: var(--space-lg); + max-width: 50ch; + font-size: var(--text-sm); + line-height: var(--lh-relaxed); + color: var(--color-muted); +} +.colophon__meta { + margin-top: var(--space-2xl); + padding-top: var(--space-lg); + border-top: var(--rule-fine) solid var(--color-rule-2); + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-xs) var(--space-lg); + font: 500 var(--text-xs)/1.4 var(--font-label); + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--color-muted); +} +.colophon__wordmark { + font-family: var(--font-display); + font-size: var(--text-md); + text-transform: none; + letter-spacing: var(--tracking-tight); + color: var(--color-ink); +} +.colophon__meta a { + margin-left: auto; + color: var(--color-accent); + white-space: nowrap; + transition: color var(--dur-short) var(--ease-out); +} +.colophon__meta a:hover { color: var(--color-ink); } + +/* ════════════════════════════════════════════════════════════════ + MOTION — one orchestrated reveal on load (stagger by --i) + ════════════════════════════════════════════════════════════════ */ +.reveal { + opacity: 0; + transform: translateY(10px); + animation: reveal var(--dur-long) var(--ease-out) forwards; + animation-delay: calc(var(--i, 0) * 70ms); +} +@keyframes reveal { to { opacity: 1; transform: none; } } + +/* Cards reveal as they enter the viewport — JS adds .in-view; CSS does the move. */ +.lot__grid .card { + animation-delay: 0ms; /* hold until in view */ + animation-play-state: paused; +} +.lot__grid .card.in-view { animation-play-state: running; animation-delay: calc(var(--i, 0) * 70ms); } +@supports not (animation-play-state: paused) { + .lot__grid .card { opacity: 1; transform: none; animation: none; } +} + +/* ── Reduced motion — collapse all spatial motion to a fast fade ── */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 1ms !important; + animation-delay: 0ms !important; + animation-iteration-count: 1 !important; + transition-duration: 1ms !important; + } + .reveal, + .lot__grid .card { opacity: 1; transform: none; } + .comb > span { opacity: 1; transform: none; } + .comb > span:nth-child(4n + 2), + .comb > span:nth-child(4n) { transform: translateY(50%); } + .card:hover .vial__fill, + .card:hover { transform: none; } +} + +/* ════════════════════════════════════════════════════════════════ + RESPONSIVE + ════════════════════════════════════════════════════════════════ */ +@media (max-width: 60rem) { + .hero { + grid-template-columns: minmax(0, 1fr); + gap: var(--space-xl); + padding-top: clamp(2rem, 5vw, 3rem); + } + .hero__craft { order: -1; min-height: 16rem; } /* jar above the copy on stack */ + .comb { -webkit-mask-image: radial-gradient(circle at 50% 45%, var(--mask-matte) 30%, transparent 80%); + mask-image: radial-gradient(circle at 50% 45%, var(--mask-matte) 30%, transparent 80%); } +} + +@media (max-width: 40rem) { + body { font-size: var(--text-base); } + .section__title { font-size: var(--text-2xl); } + .hero__lede { font-size: var(--text-md); } + .lot__intro, .ridge__standfirst, .poured__intro { font-size: var(--text-md); } + .field__row .btn--solid { flex: 1 1 100%; } /* full-width CTA on phone — gate 49 friendly */ + .colophon__meta a { margin-left: 0; } +} diff --git a/site/examples/garden-01/tokens.css b/site/examples/garden-01/tokens.css new file mode 100644 index 0000000..8f30bde --- /dev/null +++ b/site/examples/garden-01/tokens.css @@ -0,0 +1,138 @@ +/* Hallmark · tokens · theme: Garden (modern botanical almanac) + * Warm oat-cream paper · botanical-ink green · living leaf-green accent + earthy clay pop. + * Young Serif display · Hanken Grotesk body · Geist Mono specimen labels. + * Axes: paper-band = light (95.5% L) · display-style = roman-serif · accent-hue = chromatic-green (140). + * Every colour + font in styles.css references a token here — no inline values. + */ + +:root, +[data-theme="garden"] { + /* ── Surface + ink ─────────────────────────────────────────── */ + --color-paper: oklch(95.5% 0.022 92); /* warm oat cream — never grey-sage */ + --color-paper-2: oklch(92.5% 0.026 92); /* tinted band */ + --color-paper-3: oklch(88.5% 0.030 90); /* hover / deeper */ + --color-rule: oklch(83% 0.028 115); /* soft green-grey hairline */ + --color-rule-2: oklch(62% 0.045 132); /* mossy rule */ + --color-muted: oklch(48% 0.040 138); /* captions — muted green */ + --color-neutral: oklch(40% 0.045 142); + --color-ink-2: oklch(33% 0.050 150); /* deep green body */ + --color-ink: oklch(24% 0.052 152); /* botanical ink — warm near-black green */ + + /* ── Accents ───────────────────────────────────────────────── */ + --color-accent: oklch(47% 0.13 140); /* living leaf-green (primary) */ + --color-accent-2: oklch(54% 0.14 46); /* earthy clay / terracotta pop */ + --color-honey: oklch(74% 0.155 80); /* lifted: warm honey gold for the jar fills */ + --color-honey-deep: oklch(63% 0.165 66); /* honey, deeper — gradient base + comb caps */ + --color-accent-ink: oklch(96% 0.02 92); /* cream sitting on green / clay / honey fills */ + --color-focus: oklch(47% 0.13 140); + + /* botanical tints — soft section bands */ + --tint-moss: oklch(47% 0.13 140 / 0.10); + --tint-clay: oklch(54% 0.14 46 / 0.10); + --tint-comb: oklch(74% 0.155 80 / 0.16); /* faint honeycomb wash */ + + /* ── Type ──────────────────────────────────────────────────── */ + --font-display: "Young Serif", ui-serif, Georgia, serif; + --font-body: "Hanken Grotesk", "Geist", ui-sans-serif, system-ui, sans-serif; + --font-label: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace; + + --display-weight: 400; /* Young Serif ships one organic weight */ + --display-style: normal; /* roman — no italic headers (gate 38a) */ + + --text-xs: 0.75rem; + --text-sm: 0.8125rem; + --text-base: 1rem; + --text-md: 1.0625rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --text-2xl: 1.9375rem; + --text-3xl: 2.5rem; + --text-display: clamp(2.7rem, 4.4vw + 1rem, 4.4rem); + --text-display-s: clamp(2.1rem, 3.2vw + 1rem, 3.1rem); + + --tracking-display: -0.006em; + --tracking-tight: -0.004em; + --tracking-normal: 0; + --tracking-label: 0.14em; /* herbarium specimen tags */ + + --lh-tight: 1.06; + --lh-snug: 1.22; + --lh-normal: 1.5; + --lh-relaxed: 1.62; + + /* ── Space (4pt scale, role-named) ─────────────────────────── */ + --space-3xs: 0.125rem; + --space-2xs: 0.25rem; + --space-xs: 0.5rem; + --space-sm: 0.75rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2.5rem; + --space-2xl: 4rem; + --space-3xl: 6.5rem; + --space-4xl: 10rem; + + --section-gap: 6.5rem; + --section-head-gap: 1.6rem; + + --page-max: 74rem; + --page-gutter: clamp(1.25rem, 4vw, 3rem); + --measure: 58ch; + + /* ── Shape ─────────────────────────────────────────────────── */ + --radius-card: 10px; + --radius-pill: 8px; + --radius-input: 8px; + --rule-hair: 0.5px; + --rule-fine: 1px; + --rule-card: 1px; + --shadow-card: 0 14px 34px -20px oklch(24% 0.05 150 / 0.22), + 0 1px 2px oklch(24% 0.05 150 / 0.08); + --shadow-card-hover: 0 26px 52px -24px oklch(24% 0.05 150 / 0.28), + 0 2px 4px oklch(24% 0.05 150 / 0.10); + + /* ── Motion ────────────────────────────────────────────────── */ + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --dur-micro: 120ms; + --dur-short: 220ms; + --dur-long: 420ms; + + /* ── Z-scale (named, no freestyling) ───────────────────────── */ + --z-base: 1; + --z-raised: 10; + --z-sticky: 200; + --z-sticky-nav: 300; + --z-skip: 600; /* skip-link must out-paint the sticky nav */ + + --banner-height: 60px; + + /* ── Interaction shades (button states) ────────────────────── */ + --color-accent-press: oklch(42% 0.135 142); /* solid CTA :hover on light */ + --color-accent-lift: oklch(54% 0.135 142); /* solid CTA :hover on dark band */ + + /* ── Craft palette (hand-built jar + vials + honeycomb) ────── */ + --glass: oklch(92% 0.03 92 / 0.45); /* jar glass body */ + --glass-vial: oklch(92% 0.03 92 / 0.40); /* specimen vial glass */ + --comb-edge: oklch(74% 0.155 80 / 0.30); /* honeycomb cell hairline */ + --wood: oklch(58% 0.07 62); /* dipper + cork (raw wood) */ + --collar: oklch(48% 0.13 46); /* jar collar (deep clay) */ + --honey-spring: oklch(82% 0.13 88); /* palest pour */ + --honey-bass: oklch(78% 0.135 96); /* basswood */ + --honey-autumn: oklch(60% 0.14 60); /* darkest pour */ + --shadow-jar: 0 18px 24px oklch(24% 0.05 150 / 0.18); + --shadow-vial: 0 6px 10px oklch(24% 0.05 150 / 0.16); + --mask-matte: oklch(0% 0 0); /* mask alpha only — never painted */ + + /* ── Dark band (the standing-jar CTA on --color-ink) ───────── */ + --ink-paper: oklch(88% 0.012 110); /* light body text on dark ink */ + --ink-label: oklch(82% 0.03 110); /* field label on dark */ + --ink-help: oklch(78% 0.02 110); /* field help on dark */ + --ink-placeholder: oklch(70% 0.02 110); /* placeholder on dark */ + --ink-field: oklch(30% 0.04 150); /* input fill on dark */ + --ink-field-2: oklch(33% 0.045 150); /* input fill :focus on dark */ + --ink-field-hover: oklch(70% 0.05 132); /* input border :hover on dark */ + --on-dark-error: oklch(74% 0.12 46); /* error help on dark */ + --on-dark-success: oklch(78% 0.13 142); /* success help on dark */ +} diff --git a/site/examples/press-01/index.html b/site/examples/press-01/index.html new file mode 100644 index 0000000..0e7b5d4 --- /dev/null +++ b/site/examples/press-01/index.html @@ -0,0 +1,306 @@ + + + + + + Bríd Halloran — experimental typographer · concrete poetry, variable fonts, broken-grid posters + + + + + + + + + + + + + + + + + + + +
+
+ + +
+

+ Press Quaternary a working type studio. Bríd Halloran sets letters that refuse to sit still. +

+ +

+ A typeface is + a verb, not + a noun. +

+ + +
+
+ +
+
+
+
+ + +
+
+ + +
+
+ + +
+

+ Pull the axes. The word reads Restless by design — that is the point of a variable face. +

+
+
+
+ + + See the work + + +
+ + +
+
+ +

Quaternary Grotesque

+

Six masters on one weight axis, one width axis, one optical size. Drawn 2024–2026. Latin Extended, set here at the sizes it was made to do work at.

+
+ +
    +
  1. + Thin · 240 +

    Hairlines hold at billboard size.

    +
  2. +
  3. + Book · 340 +

    Reads quiet in a paragraph, loud on a wall.

    +
  4. +
  5. + Medium · 480 +

    A working middle, for captions and credits.

    +
  6. +
  7. + Semibold · 620 +

    Where a headline starts to push back.

    +
  8. +
  9. + Black · 800 +

    Set it once. Let it shout.

    +
  10. +
+ +
+
+
Character set
+
Latin Extended-A · 642 glyphs
+
+
+
Figures
+
Lining, oldstyle, tabular, fractions
+
+
+
Axes
+
Weight 200–800 · Width 75–100 · Optical 12–96
+
+
+
Format
+
Variable OTF · WOFF2 · one file
+
+
+
+ + +
+
+ +

Concrete poetry

+

The shape carries the meaning. Below: “Rain on the Lee,” set so the lines fall the way the rain does — typography as the picture, not its caption.

+
+ + +
+

+ Rain on the Lee, a concrete poem. The words fall down the page in slanting lines, + faster and heavier toward the bottom, where they pool into the single word: river. +

+ +
+ Rain on the Lee + Calligramme · Quaternary Grotesque · set by hand, 2025 +
+
+
+ + +
+
+ +

Broken-grid posters

+

A twelve-column grid is a promise. I keep it just long enough that breaking it means something. One element always crosses the line on purpose.

+
+ +
+
+ + Lent Series · Triskel + + Poster reading: Off the Grid — a night of new typography, Triskel Arts Centre. +

Fri 14 March · 8pm

+

Triskel Arts Centre, Tobin Street

+ +
+
+ The numeral bleeds past the right margin — the only element allowed to cross. + Everything else holds the grid so the break reads as a decision, not an accident. +
+
+
+ + +
+
+ +

Selected work

+

Eleven years, set in a ledger. Type design, identities, poster series, and the occasional book — each with whoever commissioned it.

+
+ +
    +
  1. + 2026 + Quaternary Grotesque + Variable typeface · self-released +
  2. +
  3. + 2025 + Rain on the Lee + Concrete poem · Cork Midsummer +
  4. +
  5. + 2025 + Off the Grid · poster series + Identity & print · Triskel Arts Centre +
  6. +
  7. + 2024 + Strand & Marsh + Bespoke wordmark · a saltmarsh distillery +
  8. +
  9. + 2023 + The Compositor’s Almanac + 204-page book · designed & typeset +
  10. +
  11. + 2022 + Marsh Mono + Monospace family · for a print zine +
  12. +
+
+ + +
+

Set by hand, in Cork.

+
+

+ I draw type, set poems into shapes, and make posters that argue with their own grid. + Most of it starts on tracing paper and ends as one variable file. I take a few + commissions a year — identities that need a face nobody else has, and print + that wants to be read across a room. +

+ + +
+ +
+ + +
+

A real reply, from me, within a few days.

+
+
+
+ +
+ + + +
+ + + + diff --git a/site/examples/press-01/script.js b/site/examples/press-01/script.js new file mode 100644 index 0000000..d674009 --- /dev/null +++ b/site/examples/press-01/script.js @@ -0,0 +1,139 @@ +/* gen-13 · Bríd Halloran — Press Quaternary + * Three small, purposeful behaviours: + * 1. the live variable-font specimen (range axes drive font-variation-settings) + * 2. a scroll-spy that marks the current section dot in the side-rail + * 3. an optimistic enquire form with the full set of states + * All motion respects prefers-reduced-motion. + */ +(function () { + "use strict"; + + /* ── 1 · Living variable specimen ───────────────────────── */ + var word = document.getElementById("liveWord"); + var fig = document.querySelector(".specimen-live"); + var axes = [ + { input: document.getElementById("axWght"), out: document.getElementById("wghtVal"), prop: "--w" }, + { input: document.getElementById("axWdth"), out: document.getElementById("wdthVal"), prop: "--wd" }, + { input: document.getElementById("axOpsz"), out: document.getElementById("opszVal"), prop: "--op" } + ]; + + function applyAxes() { + if (!word) return; + var w = axes[0].input ? axes[0].input.value : 320; + var wd = axes[1].input ? axes[1].input.value : 100; + var op = axes[2].input ? axes[2].input.value : 96; + // write the variation directly so it overrides the breathing keyframes + word.style.fontVariationSettings = + "'wght' " + w + ", 'wdth' " + wd + ", 'opsz' " + op; + axes.forEach(function (a) { + if (a.out && a.input) a.out.textContent = a.input.value; + }); + } + + axes.forEach(function (a) { + if (!a.input) return; + a.input.addEventListener("input", function () { + if (fig) fig.dataset.grabbed = "true"; // stop the ambient breathe loop + applyAxes(); + }); + }); + // initialise the value read-outs (breathing loop owns the glyph until first grab) + axes.forEach(function (a) { if (a.out && a.input) a.out.textContent = a.input.value; }); + + /* ── 2 · Scroll-spy for the rail dots ───────────────────── */ + var dotLinks = Array.prototype.slice.call( + document.querySelectorAll(".rail__dots a") + ); + var sections = dotLinks + .map(function (a) { + var id = a.getAttribute("href").slice(1); + return { link: a, el: document.getElementById(id) }; + }) + .filter(function (s) { return s.el; }); + + if ("IntersectionObserver" in window && sections.length) { + var spy = new IntersectionObserver( + function (entries) { + entries.forEach(function (entry) { + if (!entry.isIntersecting) return; + var match = sections.find(function (s) { return s.el === entry.target; }); + if (!match) return; + dotLinks.forEach(function (l) { + var on = l === match.link; + l.classList.toggle("is-current", on); + if (on) { l.setAttribute("aria-current", "true"); } + else { l.removeAttribute("aria-current"); } + }); + }); + }, + { rootMargin: "-45% 0px -50% 0px", threshold: 0 } + ); + sections.forEach(function (s) { spy.observe(s.el); }); + } + + /* ── 3 · Enquire form — optimistic, all states ──────────── */ + var form = document.getElementById("enquire"); + if (form) { + var input = document.getElementById("enq-email"); + var help = document.getElementById("enq-help"); + var btn = form.querySelector(".enquire__btn"); + var label = btn ? btn.querySelector(".btn__label") : null; + var defaultHelp = help ? help.textContent : ""; + var touched = false; + + function setHelp(text, tone) { + if (!help) return; + help.textContent = text; + if (tone) help.setAttribute("data-tone", tone); + else help.removeAttribute("data-tone"); + } + + function validEmail(v) { + return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v.trim()); + } + + function validate() { + if (!input) return false; + var ok = validEmail(input.value); + input.setAttribute("aria-invalid", ok ? "false" : "true"); + return ok; + } + + if (input) { + input.addEventListener("blur", function () { + touched = true; + if (input.value && !validate()) { + setHelp("That address is missing an @ or a domain. Check it and try again.", "error"); + } else { + setHelp(defaultHelp, null); + } + }); + input.addEventListener("input", function () { + if (!touched) return; + if (validate()) setHelp(defaultHelp, null); + }); + } + + form.addEventListener("submit", function (e) { + e.preventDefault(); + touched = true; + if (!validate()) { + setHelp("That address is missing an @ or a domain. Check it and try again.", "error"); + if (input) input.focus({ preventScroll: true }); + return; + } + + // optimistic: show loading, then resolve to a quiet success (no toast) + if (btn) { btn.dataset.state = "loading"; btn.disabled = true; } + if (label) label.textContent = "Sending…"; + setHelp("Sending your note…", null); + + window.setTimeout(function () { + if (btn) { btn.dataset.state = "success"; btn.disabled = false; } + if (label) label.textContent = "Note sent ✓"; + if (input) { input.value = ""; input.setAttribute("aria-invalid", "false"); } + setHelp("Thanks — your note is on its way. I’ll reply from studio@pressquaternary within a few days.", "success"); + }, 900); + }); + } +})(); diff --git a/site/examples/press-01/styles.css b/site/examples/press-01/styles.css new file mode 100644 index 0000000..6aed4c6 --- /dev/null +++ b/site/examples/press-01/styles.css @@ -0,0 +1,995 @@ +/* Hallmark · macrostructure: Type Specimen · H1 Marquee knobs: size=colossal, alignment=left-bias, underlay=single rule below + * theme: custom (tuned) · vibe: "ink-black press, off-register, broken-grid, kinetic" + * paper: oklch(13% 0.014 25) · accent: oklch(60% 0.21 25) + * display: Bricolage Grotesque (variable) · body: Fraunces (variable) · outlier: JetBrains Mono + * axes: dark / display-heavy / warm · studied: no · context: inferred + * genre: editorial · nav: N3 Side-rail · footer: Ft1 Mast-headed · enrichment: none (typography only) + * Hallmark · pre-emit critique: P5 H5 E5 S5 R4 V5 + * contrast: pass (40–41) · honest: pass (46) · chrome: pass (47) · tokens: pass (48) + * responsive: pass (49) · icons: pass (30) · mobile: pass (34, 49, 50–57) · slop: pass (42–45) + */ + +/* ── Reset / base ─────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; } + +html { + overflow-x: clip; + -webkit-text-size-adjust: 100%; + scroll-behavior: smooth; + scroll-padding-top: var(--space-xl); +} + +body { + overflow-x: clip; + margin: 0; + background: var(--color-paper); + color: var(--color-ink); + font-family: var(--font-body); + font-optical-sizing: auto; + font-weight: 360; + font-size: var(--text-base); + line-height: var(--lh-normal); + font-variant-numeric: oldstyle-nums; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + padding-inline: max(0px, env(safe-area-inset-left)); +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +h1, h2, h3, p, ul, ol, dl, dd, figure, blockquote { margin: 0; } +ul, ol { list-style: none; padding: 0; } +a { color: inherit; text-decoration: none; } +img, svg { display: block; max-width: 100%; } +:focus { outline: none; } +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.skip-link { + position: fixed; + left: var(--space-md); + top: var(--space-md); + z-index: var(--z-toast); + background: var(--color-accent); + color: var(--color-accent-ink); + font-family: var(--font-mono); + font-size: var(--text-sm); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + padding: var(--space-sm) var(--space-md); + transform: translateY(-150%); + transition: transform var(--dur-short) var(--ease-out); +} +.skip-link:focus-visible { transform: translateY(0); } + +/* ── Shell — offset for the fixed side-rail ───────────────── */ +.shell { + margin-inline-start: var(--rail-width); +} +.shell > main { display: block; } + +/* every major section shares one width model + rhythm */ +.hero, +.proof, +.concrete, +.work, +.studio { + padding-inline: var(--page-gutter); +} +.proof, +.concrete, +.work { + padding-block: var(--section-gap); +} + +/* ════════════════════════════════════════════════════════════ + N3 · SIDE-RAIL NAV + Rotated wordmark + section dot index, fixed to the left edge. + ════════════════════════════════════════════════════════════ */ +.rail { + position: fixed; + inset-block: 0; + inset-inline-start: 0; + width: var(--rail-width); + z-index: var(--z-sticky-nav); + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + padding-block: var(--space-lg); + background: var(--color-paper); + border-inline-end: var(--rule-hair) solid var(--color-rule); +} + +.rail__mark { + writing-mode: vertical-rl; + transform: rotate(180deg); + display: flex; + align-items: baseline; + gap: var(--space-md); +} +.rail__name { + font-family: var(--font-display); + font-variation-settings: 'wght' 600, 'opsz' 18; + font-size: var(--text-md); + letter-spacing: 0.02em; + color: var(--color-ink); +} +.rail__role { + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-muted); +} +.rail__mark:hover .rail__name { color: var(--color-accent); } + +.rail__dots { + display: flex; + flex-direction: column; + gap: var(--space-md); + align-items: center; +} +.rail__dots a { + position: relative; + display: grid; + place-items: center; + width: 1.5rem; + height: 1.5rem; +} +/* expand the touch target without changing visual size */ +.rail__dots a::before { content: ""; position: absolute; inset: -10px; } +.rail__dot { + width: 7px; + height: 7px; + border: var(--rule-hair) solid var(--color-rule-2); + background: transparent; + transition: background-color var(--dur-short) var(--ease-out), + border-color var(--dur-short) var(--ease-out), + transform var(--dur-short) var(--ease-out); +} +@media (hover: hover) { + .rail__dots a:hover .rail__dot { border-color: var(--color-accent); transform: scale(1.25); } +} +.rail__dots a:focus-visible { outline-offset: 4px; } +.rail__dots a.is-current .rail__dot { + background: var(--color-accent); + border-color: var(--color-accent); +} +/* tooltip-style label on hover/focus — focus shows instantly, hover delays */ +.rail__dots a::after { + content: attr(data-label); + position: absolute; + inset-inline-start: 140%; + white-space: nowrap; + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-ink); + background: var(--color-paper-2); + border: var(--rule-hair) solid var(--color-rule); + padding: var(--space-2xs) var(--space-xs); + opacity: 0; + pointer-events: none; + transition: opacity var(--dur-short) var(--ease-out) 800ms; +} +.rail__dots a:hover::after { opacity: 1; } +.rail__dots a:focus-visible::after { opacity: 1; transition-delay: 0ms; } + +.rail__year { + writing-mode: vertical-rl; + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-micro); + text-transform: uppercase; + color: var(--color-muted); +} +.rail__year:hover { color: var(--color-ink-2); } + +/* ════════════════════════════════════════════════════════════ + HERO · the living specimen + ════════════════════════════════════════════════════════════ */ +.hero { + position: relative; + display: grid; + align-content: start; + gap: var(--space-md); + padding-block: var(--space-2xl) var(--space-3xl); + max-width: var(--page-max); + border-bottom: var(--rule-hair) solid var(--color-rule); +} + +.hero__slug { + max-width: 56ch; + font-family: var(--font-mono); + font-size: var(--text-sm); + letter-spacing: 0.04em; + line-height: 1.6; + color: var(--color-muted); +} +.hero__slug::before { + content: ""; + display: inline-block; + width: 2.5rem; + height: var(--rule-fine); + background: var(--color-accent); + vertical-align: middle; + margin-inline-end: var(--space-sm); +} + +.hero__h { + font-family: var(--font-display); + font-optical-sizing: auto; + font-variation-settings: 'wght' 560, 'opsz' 96; + font-size: var(--text-display); + line-height: var(--lh-tight); + letter-spacing: var(--tracking-display); + color: var(--color-ink); + max-width: 18ch; + overflow-wrap: anywhere; + min-width: 0; +} +.hero__h span { display: block; } +.hero__h span:last-child { color: var(--color-accent); } + +/* ── The colossal live specimen ── */ +.specimen-live { + margin-block-start: var(--space-md); + display: grid; + gap: var(--space-lg); +} +.specimen-live__stage { + position: relative; + border-block: var(--rule-hair) solid var(--color-rule); + padding-block: var(--space-xs); + overflow: clip; +} +.specimen-live__word { + display: block; + font-family: var(--font-display); + font-optical-sizing: none; + font-variation-settings: 'wght' var(--w, 320), 'wdth' var(--wd, 100), 'opsz' var(--op, 96); + font-size: var(--text-colossal); + line-height: 0.82; + letter-spacing: -0.04em; + color: var(--color-ink); + white-space: nowrap; + overflow-wrap: normal; + /* off-register CMYK misprint — the kinetic, press-y signature */ + text-shadow: + 0.045em 0 0 var(--ghost-red), + -0.045em 0.012em 0 var(--ghost-cyan); + animation: breathe var(--dur-breathe) var(--ease-in-out) infinite alternate; +} +/* once the reader grabs an axis, JS sets data-grabbed and we stop breathing */ +.specimen-live[data-grabbed="true"] .specimen-live__word { animation: none; } + +@keyframes breathe { + from { --w: 280; --wd: 100; } + to { --w: 600; --wd: 88; } +} +/* register the custom props so they can be animated */ +@property --w { syntax: ""; inherits: true; initial-value: 320; } +@property --wd { syntax: ""; inherits: true; initial-value: 100; } +@property --op { syntax: ""; inherits: true; initial-value: 96; } + +.specimen-live__panel { margin: 0; } +.axis { + display: grid; + gap: var(--space-sm); + max-width: 30rem; +} +.axis__row { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--space-2xs); +} +.axis__label { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--space-sm); + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-neutral); +} +.axis__val { + font-variant-numeric: tabular-nums; + color: var(--color-accent); +} +.axis__help { + margin-block-start: var(--space-2xs); + font-size: var(--text-sm); + line-height: 1.55; + color: var(--color-muted); + max-width: 44ch; +} +.axis__help em { font-style: italic; color: var(--color-ink-2); } + +/* range / slider — thumb gets the focus state, not the track */ +.axis__range { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 2px; + background: var(--color-rule-2); + cursor: pointer; + margin: var(--space-xs) 0; +} +.axis__range::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 22px; + background: var(--color-accent); + border: none; + cursor: grab; + transition: transform var(--dur-micro) var(--ease-out); +} +.axis__range::-moz-range-thumb { + width: 14px; + height: 22px; + border: none; + border-radius: 0; + background: var(--color-accent); + cursor: grab; +} +.axis__range:active::-webkit-slider-thumb { transform: scaleY(0.9); cursor: grabbing; } +.axis__range:active::-moz-range-thumb { cursor: grabbing; } +.axis__range:focus-visible { outline: none; } +.axis__range:focus-visible::-webkit-slider-thumb { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} +.axis__range:focus-visible::-moz-range-thumb { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +/* ── arrow link / CTA voice (C3 typographic link) ── */ +.link-arrow { + display: inline-flex; + align-items: center; + gap: var(--space-sm); + width: fit-content; + font-family: var(--font-mono); + font-size: var(--text-sm); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-ink); + white-space: nowrap; + padding-block: var(--space-2xs); +} +.link-arrow span { + background-image: linear-gradient(var(--color-accent), var(--color-accent)); + background-size: 100% var(--rule-fine); + background-position: 0 100%; + background-repeat: no-repeat; + padding-bottom: 3px; +} +.link-arrow__glyph { color: var(--color-accent); transition: transform var(--dur-short) var(--ease-out); } +@media (hover: hover) { + .link-arrow:hover .link-arrow__glyph { transform: translateX(4px); } +} +.hero__cta { margin-block-start: var(--space-sm); } + +/* ════════════════════════════════════════════════════════════ + PROOF SHEET — surface inversion to warm bone + Every flipped surface sets BOTH background and color (gate 41). + ════════════════════════════════════════════════════════════ */ +.proof { + background: var(--proof-paper); + color: var(--proof-ink); +} + +/* plate heads — eyebrow stacked ABOVE heading, single column (gate 54) */ +.plate-head { + display: flex; + flex-direction: column; + gap: var(--space-sm); + max-width: 64ch; + margin-block-end: var(--space-2xl); +} +.plate-head__no { + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--proof-accent); +} +.plate-head__title { + font-family: var(--font-display); + font-variation-settings: 'wght' 600, 'opsz' 72; + font-size: var(--text-display-s); + line-height: 1.02; + letter-spacing: var(--tracking-tight); + color: var(--proof-ink); + overflow-wrap: anywhere; + min-width: 0; +} +.plate-head__sub { + max-width: var(--measure); + font-size: var(--text-md); + line-height: var(--lh-relaxed); + color: var(--proof-ink-2); +} +.plate-head__sub em { font-style: italic; } + +/* heading register that sits on the dark ground (Plate 02 + Index) */ +.plate-head--ink .plate-head__no { color: var(--color-accent); } +.plate-head--ink .plate-head__title { color: var(--color-ink); } +.plate-head--ink .plate-head__sub { color: var(--color-neutral); } + +/* ── Weights ladder ── */ +.weights { + display: grid; + border-top: var(--rule-hair) solid var(--proof-rule); + max-width: var(--page-max); +} +.weights__row { + display: grid; + grid-template-columns: 9rem minmax(0, 1fr); + align-items: baseline; + gap: var(--space-lg); + padding-block: var(--space-lg); + border-bottom: var(--rule-hair) solid var(--proof-rule); +} +.weights__tag { + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--proof-muted); + font-variant-numeric: tabular-nums; +} +.weights__line { + font-family: var(--font-display); + font-optical-sizing: none; + font-size: clamp(1.5rem, 4.5vw, 3rem); + line-height: 1.04; + letter-spacing: -0.02em; + color: var(--proof-ink); + overflow-wrap: anywhere; + min-width: 0; + transition: font-variation-settings var(--dur-long) var(--ease-out); +} +@media (hover: hover) { + /* hover lifts the weight one master — demonstrates the axis live */ + .weights__row:hover .weights__line { font-variation-settings: 'wght' 800, 'opsz' 18 !important; } +} + +/* ── Glyph / feature spec sheet (F3) ── */ +.glyphset { + margin-block-start: var(--space-2xl); + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr)); + gap: 0; + border: var(--rule-hair) solid var(--proof-rule); + max-width: var(--page-max); +} +.glyphset__cell { + padding: var(--space-lg); + border-inline-start: var(--rule-hair) solid var(--proof-rule); +} +.glyphset__cell:first-child { border-inline-start: none; } +.glyphset dt { + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--proof-muted); + margin-block-end: var(--space-xs); +} +.glyphset dd { + font-size: var(--text-md); + line-height: 1.45; + color: var(--proof-ink); +} + +/* ════════════════════════════════════════════════════════════ + CONCRETE POETRY · the calligramme (on the dark ground) + ════════════════════════════════════════════════════════════ */ +.concrete { + background: var(--color-paper); + color: var(--color-ink); +} +.poem { + position: relative; + max-width: var(--page-max); + margin-block-start: var(--space-xl); + border: var(--rule-hair) solid var(--color-rule); + padding: clamp(var(--space-lg), 5vw, var(--space-3xl)); + background: + repeating-linear-gradient( + 90deg, + transparent 0, + transparent calc((100% - 11 * 1px) / 12), + var(--color-paper-2) calc((100% - 11 * 1px) / 12), + var(--color-paper-2) calc((100% - 11 * 1px) / 12 + 1px) + ); +} +.poem__visually-hidden, +.poster__vis-hidden { + position: absolute; + width: 1px; height: 1px; + padding: 0; margin: -1px; + overflow: hidden; clip: rect(0 0 0 0); + white-space: nowrap; border: 0; +} +.poem__field { + position: relative; + min-height: clamp(20rem, 46vw, 30rem); + font-family: var(--font-display); + font-optical-sizing: none; +} +/* each word placed on a 9-column × 6-row implicit grid, slanted like rain */ +.poem__w { + position: absolute; + left: calc(var(--c) * 10%); + top: calc((var(--r) - 1) * 16% + 2%); + font-variation-settings: 'wght' var(--w, 360), 'wdth' 88; + font-size: clamp(1.1rem, 2.4vw, 2rem); + line-height: 1; + letter-spacing: -0.01em; + color: var(--color-ink-2); + transform: rotate(var(--rot, -8deg)); + transform-origin: left top; + white-space: nowrap; +} +.poem__w--pool { + left: 50%; + top: auto; + bottom: 2%; + transform: translateX(-50%); + font-size: clamp(2.5rem, 8vw, 5.5rem); + letter-spacing: -0.04em; + color: var(--color-accent); + text-shadow: + 0.03em 0 0 var(--ghost-red), + -0.03em 0.01em 0 var(--ghost-cyan); +} +.poem__cap { + position: relative; + margin-block-start: var(--space-xl); + padding-block-start: var(--space-md); + border-top: var(--rule-hair) solid var(--color-rule); + display: flex; + flex-wrap: wrap; + align-items: baseline; + justify-content: space-between; + gap: var(--space-sm) var(--space-lg); +} +.poem__title { + font-family: var(--font-body); + font-style: italic; + font-size: var(--text-lg); + color: var(--color-ink); +} +.poem__meta { + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-muted); +} + +/* ════════════════════════════════════════════════════════════ + BROKEN-GRID POSTER (on the proof/bone surface) + ════════════════════════════════════════════════════════════ */ +.poster { + max-width: var(--page-max); + display: grid; + gap: var(--space-lg); +} +.poster__plate { + position: relative; + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + grid-auto-rows: min-content; + gap: var(--space-sm) var(--space-md); + background: var(--proof-paper-2); + border: var(--rule-fine) solid var(--proof-ink); + padding: clamp(var(--space-lg), 4vw, var(--space-2xl)); + overflow: clip; /* the bleed numeral is clipped by the plate edge, page stays put */ + isolation: isolate; +} +/* a single baseline rule the type hangs from */ +.poster__baseline { + grid-column: 1 / -1; + grid-row: 2; + height: var(--rule-fine); + background: var(--proof-accent); + align-self: end; + margin-block-end: -0.2em; + z-index: var(--z-base); +} +.poster__kicker { + grid-column: 1 / 7; + grid-row: 1; + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--proof-ink-2); +} +.poster__title { + grid-column: 1 / -1; + grid-row: 2; + display: grid; + z-index: var(--z-raised); + margin: 0; +} +.poster__word { + font-family: var(--font-display); + font-optical-sizing: none; + font-variation-settings: 'wght' 800, 'wdth' 80, 'opsz' 96; + font-size: clamp(3rem, 16vw, 9rem); + line-height: 1.0; /* all-caps display floor — no cap-collision between stacked words (gate 55) */ + letter-spacing: -0.04em; + color: var(--proof-ink); + text-transform: uppercase; + overflow-wrap: anywhere; + min-width: 0; +} +/* the break: each word steps off the left margin by a different amount, + and the middle word jumps register to red — the grid bent on purpose */ +.poster__word--a { margin-inline-start: 0; } +.poster__word--b { margin-inline-start: 14%; color: var(--proof-accent); } +.poster__word--c { margin-inline-start: 5%; } + +.poster__line { + font-family: var(--font-mono); + font-size: var(--text-sm); + line-height: 1.5; + color: var(--proof-ink-2); + align-self: end; +} +.poster__line--date { grid-column: 1 / 6; grid-row: 3; } +.poster__line--place { grid-column: 7 / 13; grid-row: 3; text-align: right; } + +/* the ONE element allowed to cross the margin — a colossal numeral bleeding right */ +.poster__bleed { + position: absolute; + right: -0.12em; + bottom: -0.28em; + font-family: var(--font-display); + font-optical-sizing: none; + font-variation-settings: 'wght' 800, 'wdth' 75; + font-size: clamp(9rem, 30vw, 22rem); + line-height: 0.7; + letter-spacing: -0.06em; + color: transparent; + -webkit-text-stroke: 2px var(--proof-rule-2); + text-stroke: 2px var(--proof-rule-2); + z-index: var(--z-base); + pointer-events: none; + font-variant-numeric: lining-nums; +} +.poster__cap { + max-width: var(--measure); + font-size: var(--text-sm); + line-height: 1.6; + color: var(--proof-muted); +} + +/* ════════════════════════════════════════════════════════════ + SELECTED WORK · ledger (on the dark ground) + ════════════════════════════════════════════════════════════ */ +.work { + background: var(--color-paper); + color: var(--color-ink); +} +.ledger { + max-width: var(--page-max); + border-top: var(--rule-hair) solid var(--color-rule); +} +.ledger__row { + display: grid; + grid-template-columns: 5rem minmax(0, 1.1fr) minmax(0, 1.4fr); + align-items: baseline; + gap: var(--space-md) var(--space-lg); + padding-block: var(--space-lg); + border-bottom: var(--rule-hair) solid var(--color-rule); + transition: background-color var(--dur-short) var(--ease-out); +} +@media (hover: hover) { + .ledger__row:hover { background: var(--color-paper-2); } + .ledger__row:hover .ledger__name { color: var(--color-accent); } +} +.ledger__year { + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--color-muted); + font-variant-numeric: tabular-nums; +} +.ledger__name { + font-family: var(--font-display); + font-variation-settings: 'wght' 520, 'opsz' 28; + font-size: var(--text-xl); + line-height: 1.1; + letter-spacing: var(--tracking-tight); + color: var(--color-ink); + overflow-wrap: anywhere; + min-width: 0; + transition: color var(--dur-short) var(--ease-out); +} +.ledger__kind { + font-size: var(--text-md); + line-height: 1.45; + color: var(--color-neutral); +} + +/* ════════════════════════════════════════════════════════════ + THE STUDIO · close + enquire form + ════════════════════════════════════════════════════════════ */ +.studio { + background: var(--color-paper); + color: var(--color-ink); + padding-block: var(--section-gap) var(--space-4xl); + border-top: var(--rule-hair) solid var(--color-rule); + max-width: var(--page-max); +} +.studio__h { + font-family: var(--font-display); + font-variation-settings: 'wght' 580, 'opsz' 96; + font-size: var(--text-display-s); + line-height: 1.02; + letter-spacing: var(--tracking-display); + color: var(--color-ink); + max-width: 16ch; + margin-block-end: var(--space-2xl); + overflow-wrap: anywhere; + min-width: 0; +} +.studio__cols { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); + gap: var(--space-2xl); + align-items: start; +} +.studio__lede { + font-size: var(--text-lg); + line-height: var(--lh-relaxed); + color: var(--color-ink-2); + max-width: var(--measure); +} + +/* ── Enquire (inline form-as-CTA · 8 states) ── */ +.enquire { display: grid; gap: var(--space-xs); align-content: start; } +.enquire__label { + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-neutral); +} +.enquire__row { + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); +} +.enquire__input { + flex: 1 1 14rem; + min-width: 0; + height: 3rem; /* = button height */ + padding-inline: var(--space-md); + padding-inline-end: 2rem; /* reserved right-edge slot */ + font-family: var(--font-mono); + font-size: var(--text-sm); + color: var(--color-ink); + background: var(--color-paper-2); + border: 1px solid var(--color-rule-2); + outline: 2px solid transparent; /* reserved focus slot — no shift */ + outline-offset: 1px; + transition: background-color var(--dur-short) var(--ease-out), + border-color var(--dur-short) var(--ease-out); +} +.enquire__input::placeholder { color: var(--color-muted); } +@media (hover: hover) { + .enquire__input:hover { background: var(--color-paper-3); } +} +.enquire__input:focus-visible { + outline: 2px solid var(--color-focus); + border-color: var(--color-ink-2); + background: var(--color-paper-2); +} +.enquire__input[aria-invalid="true"] { + border-color: var(--color-accent); +} +.enquire__input:disabled { + opacity: 0.55; + cursor: not-allowed; +} + +.btn { + height: 3rem; + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-xs); + padding-inline: var(--space-lg); + font-family: var(--font-mono); + font-size: var(--text-sm); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + line-height: 1; + color: var(--color-accent-ink); + background: var(--color-accent); + border: 1px solid var(--color-accent); + cursor: pointer; + white-space: nowrap; + transition: transform var(--dur-micro) var(--ease-out), + background-color var(--dur-short) var(--ease-out); +} +@media (hover: hover) { + .btn:hover { background: var(--color-focus); border-color: var(--color-focus); } +} +.btn:active { transform: translateY(1px); } +/* the CTA fill is red, so its focus ring is bone-ink (≥APCA Lc 90 on the red + fill, ≥7:1 on the dark paper) — never a red-on-red ring. */ +.btn:focus-visible { outline: 2px solid var(--color-ink); outline-offset: 3px; } +.btn:disabled, .btn[aria-disabled="true"] { + opacity: 0.55; + cursor: not-allowed; + transform: none; +} +.btn[data-state="loading"] { cursor: progress; } +.btn[data-state="loading"] .btn__label { opacity: 0.6; } +.btn[data-state="success"] { + background: transparent; + color: var(--color-ink); + border-color: var(--color-rule-2); +} + +.enquire__help { + min-height: 1.4lh; /* reserved so error doesn't shift the page */ + font-size: var(--text-sm); + line-height: 1.4; + color: var(--color-muted); +} +.enquire__help[data-tone="error"] { color: var(--color-focus); } +.enquire__help[data-tone="success"] { color: var(--color-ink-2); } + +/* ════════════════════════════════════════════════════════════ + Ft1 · MAST-HEADED FOOTER + ════════════════════════════════════════════════════════════ */ +.foot { + background: var(--color-paper); + color: var(--color-ink); + border-top: var(--rule-heavy) solid var(--color-accent); + padding: var(--space-2xl) var(--page-gutter) var(--space-3xl); + display: grid; + gap: var(--space-sm); + max-width: var(--page-max); +} +.foot__mark { + font-family: var(--font-display); + font-variation-settings: 'wght' 640, 'opsz' 60, 'wdth' 90; + font-size: clamp(2.5rem, 9vw, 5.5rem); + line-height: 0.92; + letter-spacing: -0.03em; + color: var(--color-ink); + overflow-wrap: anywhere; + min-width: 0; +} +.foot__tag { + max-width: 48ch; + font-size: var(--text-md); + line-height: var(--lh-relaxed); + color: var(--color-neutral); + margin-block-start: var(--space-xs); +} +.foot__set { + max-width: 56ch; + margin-block-start: var(--space-md); + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: 1.7; + letter-spacing: 0.02em; + color: var(--color-muted); +} +.foot__links { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-sm); + margin-block-start: var(--space-xs); + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; +} +.foot__links a { + color: var(--color-ink-2); + white-space: nowrap; + background-image: linear-gradient(var(--color-accent), var(--color-accent)); + background-size: 0% var(--rule-fine); + background-position: 0 100%; + background-repeat: no-repeat; + padding-bottom: 2px; + transition: background-size var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out); +} +@media (hover: hover) { + .foot__links a:hover { color: var(--color-ink); background-size: 100% var(--rule-fine); } +} +.foot__sep { color: var(--color-rule-2); } + +/* ════════════════════════════════════════════════════════════ + MOTION · one orchestrated entrance (reveal), reduced-motion safe + ════════════════════════════════════════════════════════════ */ +.reveal { + opacity: 0; + transform: translateY(10px); + animation: reveal var(--dur-reveal) var(--ease-out) forwards; + animation-delay: calc(var(--i, 0) * 70ms); +} +@keyframes reveal { to { opacity: 1; transform: none; } } + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + .reveal { opacity: 1; transform: none; animation: none; } + .specimen-live__word { animation: none; } + .weights__line { transition: none; } +} + +/* ════════════════════════════════════════════════════════════ + RESPONSIVE + ════════════════════════════════════════════════════════════ */ + +/* tablet / small laptop */ +@media (max-width: 60rem) { + .studio__cols { grid-template-columns: minmax(0, 1fr); gap: var(--space-2xl); } + .ledger__row { grid-template-columns: 4rem minmax(0, 1fr); } + .ledger__kind { grid-column: 2; } + .poster__word { font-size: clamp(2.75rem, 17vw, 7rem); } +} + +/* phone — the rail collapses to a slim top bar; sections go single column */ +@media (max-width: 48rem) { + .rail { + inset-block: auto; + inset-block-start: 0; + inset-inline: 0; + width: auto; + height: var(--rail-bar, 3rem); + flex-direction: row; + justify-content: space-between; + padding: 0 var(--page-gutter); + border-inline-end: none; + border-bottom: var(--rule-hair) solid var(--color-rule); + } + .rail__mark { writing-mode: horizontal-tb; transform: none; gap: var(--space-sm); } + .rail__name { font-size: var(--text-sm); } + .rail__role { display: none; } + .rail__dots { flex-direction: row; gap: var(--space-sm); } + .rail__dots a::after { display: none; } /* labels would overflow the bar */ + .rail__year { writing-mode: horizontal-tb; } + + .shell { margin-inline-start: 0; padding-block-start: 3rem; } + + .hero { min-height: auto; padding-block: var(--space-3xl) var(--space-3xl); } + + .weights__row { + grid-template-columns: minmax(0, 1fr); + gap: var(--space-2xs); + padding-block: var(--space-md); + } + .poster__line--place { grid-column: 1 / -1; text-align: left; } + .poster__line--date { grid-column: 1 / -1; } +} + +@media (max-width: 30rem) { + .hero__h { font-size: var(--text-3xl); } + .rail__dots a { width: 1.25rem; } +} diff --git a/site/examples/press-01/tokens.css b/site/examples/press-01/tokens.css new file mode 100644 index 0000000..f3e2bce --- /dev/null +++ b/site/examples/press-01/tokens.css @@ -0,0 +1,121 @@ +/* Hallmark · tokens — gen-13 + * route: custom (tuned) · vibe: "ink-black press, off-register, broken-grid, kinetic" + * paper: oklch(13% 0.014 25) · accent: oklch(60% 0.21 25) signal-red + * display: Bricolage Grotesque (variable) · body: Fraunces (variable) · outlier: JetBrains Mono + * axes: dark / display-heavy / warm + * + * One palette, two surfaces: the page runs on an ink-black press ground; + * the "proof sheet" sections invert to a warm bone paper so the specimens + * read like a printed proof pulled off the press. Both carry the warm-red tilt. + */ + +:root { + /* ── Press ground — the dark default surface ──────────────── */ + --color-paper: oklch(13% 0.014 25); /* ink-black, warm-red tilt — never #000 */ + --color-paper-2: oklch(17% 0.018 25); /* elevated panel */ + --color-paper-3: oklch(22% 0.022 24); /* deeper hover / plate */ + --color-rule: oklch(32% 0.020 24); /* hairline divider */ + --color-rule-2: oklch(44% 0.024 22); /* heavier rule */ + --color-muted: oklch(64% 0.020 26); /* de-emphasised text */ + --color-neutral: oklch(74% 0.016 28); /* mid text */ + --color-ink-2: oklch(86% 0.012 30); /* secondary headings/body */ + --color-ink: oklch(95% 0.010 32); /* primary text — warm bone */ + --color-accent: oklch(60% 0.21 25); /* signal red — the one chromatic note */ + --color-accent-2: oklch(78% 0.15 85); /* off-register amber ghost (misprint only) */ + --color-accent-ink: oklch(97% 0.012 30); /* bone text on red fill — APCA-checked */ + --color-focus: oklch(72% 0.19 25); /* brighter red, ≥3:1 on ground + on red */ + + /* Off-register misprint ghosts — translucent, decorative only */ + --ghost-cyan: oklch(70% 0.16 220 / 0.42); + --ghost-red: oklch(62% 0.22 25 / 0.40); + + /* ── Proof sheet — warm bone surface for inverted sections ── */ + --proof-paper: oklch(93% 0.020 70); /* warm bone proof stock */ + --proof-paper-2: oklch(89% 0.024 68); + --proof-rule: oklch(74% 0.024 60); + --proof-rule-2: oklch(48% 0.020 45); + --proof-muted: oklch(46% 0.018 40); + --proof-ink-2: oklch(28% 0.020 35); + --proof-ink: oklch(17% 0.024 32); /* near-black warm ink */ + --proof-accent: oklch(52% 0.20 25); /* red reads darker on bone for contrast */ + + /* ── Type stack — 2+1: display + body + mono outlier ──────── */ + --font-display: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif; + --font-body: "Fraunces", ui-serif, Georgia, serif; + --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace; + + /* ── Type scale — major third (1.25), clamped display ─────── */ + --text-xs: 0.75rem; + --text-sm: 0.8125rem; + --text-base: 1rem; + --text-md: 1.125rem; + --text-lg: 1.375rem; + --text-xl: 1.75rem; + --text-2xl: 2.25rem; + --text-3xl: 3rem; + --text-display: clamp(3rem, 7vw + 0.5rem, 6rem); + --text-display-s: clamp(2.25rem, 4vw + 0.75rem, 3.75rem); + /* the colossal living specimen — single short word, may exceed display cap */ + --text-colossal: clamp(3.75rem, 16vw, 12.5rem); + + --lh-tight: 1.0; /* display floor (no all-caps collision risk: mixed case) */ + --lh-snug: 1.12; + --lh-normal: 1.55; + --lh-relaxed: 1.66; + + --tracking-display: -0.03em; + --tracking-tight: -0.015em; + --tracking-normal: 0; + --tracking-label: 0.16em; /* mono specimen tags */ + --tracking-micro: 0.26em; /* rotated rail wordmark */ + + /* ── 4pt spacing scale ────────────────────────────────────── */ + --space-3xs: 0.125rem; + --space-2xs: 0.25rem; + --space-xs: 0.5rem; + --space-sm: 0.75rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2.5rem; + --space-2xl: 4rem; + --space-3xl: 6.5rem; + --space-4xl: 10rem; + --space-5xl: 13rem; + + --section-gap: 6.5rem; + --section-head-gap: 1.5rem; + + /* ── Layout shell ─────────────────────────────────────────── */ + --rail-width: 3.25rem; /* the fixed side-rail */ + --page-max: 78rem; + --page-gutter: clamp(1.25rem, 4vw, 3.5rem); + --measure: 62ch; + --rule-hair: 1px; + --rule-fine: 2px; + --rule-heavy: 4px; + + /* Square edges — this is a press, not a soft app */ + --radius-card: 0; + --radius-pill: 0; + --radius-input: 0; + + /* ── Motion ───────────────────────────────────────────────── */ + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --dur-micro: 120ms; + --dur-short: 220ms; + --dur-long: 420ms; + --dur-reveal: 560ms; + --dur-breathe: 9s; /* ambient variable-axis loop on the living specimen */ + + /* ── Z scale ──────────────────────────────────────────────── */ + --z-base: 1; + --z-raised: 10; + --z-dropdown: 100; + --z-sticky: 200; + --z-sticky-nav: 300; + --z-modal: 400; + --z-toast: 500; + --z-tooltip: 600; +} diff --git a/site/examples/riso-01/index.html b/site/examples/riso-01/index.html new file mode 100644 index 0000000..47f4d15 --- /dev/null +++ b/site/examples/riso-01/index.html @@ -0,0 +1,352 @@ + + + + + + OFF-REGISTER · A Risograph Print Fair · Pulp Yard, 18–19 Oct 2026 + + + + + + + + + + + + + + + + +
+ + +
+
+

A risograph print fair · Two days · One drum at a time

+

+ Thirty-four + studios. Eight presses. Soy ink everywhere. +

+

+ design like print: warm, off-register, intentional. For one weekend the + Pulp Yard fills with spot colour — fluorescent pink that won’t sit + still, teal that bleeds a hair past its edge, paper you can smell. Come + turn the drum. +

+ +
+
Where
The Pulp Yard, 31 Calder St, Govanhill
+
When
Sat & Sun, 11:00–18:00
+
Entry
Free — first 200 get a printed map
+
+
+ + +
+
+ + + + +
+
Plate 04 · 3 colours · 280 gsm
+
+
+ + +
+
+

The exhibitor list

+

Thirty-four studios · Hall A & the Mezzanine · updated 2 Oct

+
+ +
+ + + + + +
+

Showing all 34 studios.

+ +
    +
  • + + + +
    +

    Working press · Stand A1

    +

    Brae & Drum

    +

    A live two-colour MZ. Pull a print, keep it wet.

    +
    +
  • + +
  • + + + +
    +

    Zines · Stand A4

    +

    Saltmarket Editions

    +

    Perfect-bound photo zines, two spot colours, no black.

    +
    +
  • + +
  • + + + +
    +

    Posters · Stand A6

    +

    Mono No Aware Press

    +

    A2 gig posters. Fluoro pink that glows under the strip lights.

    +
    +
  • + +
  • + + + +
    +

    Comics · Stand A9

    +

    Tenement Comics

    +

    Riso-printed shorts about the close, the bin store, the No. 6 bus.

    +
    +
  • + +
  • + + + +
    +

    Zines · Stand A11

    +

    Verdigris & Co

    +

    Botanical risographs on grey speckle stock. Teal on teal.

    +
    +
  • + +
  • + + + +
    +

    Posters · Stand A13

    +

    Halftone Social Club

    +

    Big dot screens, on purpose. Bring a loupe.

    +
    +
  • + +
  • + + + +
    +

    Comics · Mezzanine M2

    +

    Closemouth Press

    +

    A risograph graphic novel, four years in, finally bound.

    +
    +
  • + +
  • + + + +
    +

    Working press · Mezzanine M5

    +

    The Spirit Master

    +

    We cut a master from your drawing while you wait. Bring a felt tip.

    +
    +
  • + +
  • + + + +
    +

    Zines · Mezzanine M7

    +

    Riso Riot

    +

    A queer riso anthology, eighty pages, sold by donation.

    +
    +
  • +
+ +

+ … and twenty-five more, from Govan Letterpress to Fluoro Bakery. + The full A–Z and the floor plan go up the week before. +

+
+ + +
+
+

Two days, hour by hour

+

Talks on the Mezzanine · printing in Hall A · all free, drop in

+
+ +
+

Saturday · 18 Oct

+
+
+
+
Doors & first pullHall A
+
+
+
+
Why pink moves: a talk on misregistrationMezzanine
+
+
+
+
Bind-your-own, with Closemouth PressHall A · table 3
+
+
+
+
Trade & swap: bring a print, take a printThe yard
+
+
+
+ +
+

Sunday · 19 Oct

+
+
+
+
Doors & slow coffeeThe yard
+
+
+
+
Soy ink & paper: what actually compostsMezzanine
+
+
+
+
Kids’ print hour — one colour, two handsHall A · table 1
+
+
+
+
Last drum spins & pack-downHall A
+
+
+
+
+ + +
+
+

Get your hands inky

+

Small groups · materials included · book at the door

+
+ +
    +
  • +

    Sat 13:00 · 90 min · £18

    +

    Make a one-colour mini-zine

    +

    + Draw it, fold it, run it through the press, staple it. You leave with + twenty copies of an eight-page zine and ink under at least one fingernail. + No experience — we mean it. +

    +

    8 places · ages 12+

    +
  • +
  • +

    Sat 15:30 · 2 hrs · £26

    +

    Two-colour overprint & the art of being slightly wrong

    +

    + The whole point of riso is the misregister. We’ll layer teal over + yellow, shift the paper a few millimetres on purpose, and chase the green + you can only get by accident. Bring a drawing or use one of ours. +

    +

    6 places · ages 16+

    +
  • +
  • +

    Sun 15:00 · 60 min · free

    +

    Kids’ print hour

    +

    + One colour, big shapes, no precious objects. Little ones cut a stencil + and pull their own poster to take home. Aprons supplied; the floor is + already a mess. +

    +

    Drop in · under 12 with an adult

    +
  • +
+
+ + +
+
+

Getting there is the easy part

+

+ Free entry, both days. No ticket needed — but leave your email and we’ll + send the floor plan, the full A–Z of studios, and a heads-up when workshop + places open. One message, the week before. Nothing after. +

+ + +
+ +
+
+
Address
+
The Pulp Yard
31 Calder Street
Govanhill, Glasgow G42 7RA
+
+
+
Getting in
+
Level access throughout. The Mezzanine has a lift. Guide dogs welcome; the presses are loud.
+
+
+
Travel
+
4 min from Queen’s Park station. Bus 6, 38, 75. Bike racks in the yard.
+
+ +
+
+ +
+ + +
+

+ OFF-REGISTER + — a risograph print fair, run by volunteers from the Govanhill print room since 2022. + Edition 04 · 18–19 October 2026 · The Pulp Yard, 31 Calder Street, Glasgow G42 7RA. + Set in Public Sans, Newsreader and Space Mono. Every mark on this page is hand-built in CSS — + no photographs, printed on screen at 72 dpi. Soy ink, recycled stock, no black plate. + Programme subject to which drums survive the weekend. Free entry, all welcome. + © 2026 the fair & its makers. +

+
+ + + + diff --git a/site/examples/riso-01/script.js b/site/examples/riso-01/script.js new file mode 100644 index 0000000..2fb1eff --- /dev/null +++ b/site/examples/riso-01/script.js @@ -0,0 +1,128 @@ +/* OFF-REGISTER — riso fair. Progressive enhancement only. + Three behaviours: page-load reveal stagger · catalogue filter · signup validation. + Everything works without JS; JS only adds polish. */ +(function () { + "use strict"; + + var root = document.documentElement; + root.classList.remove("no-js"); + + var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + /* ── One orchestrated reveal on first load ────────────────── */ + // Assign reveal + a DOM-order index so CSS staggers via --i. + var revealTargets = [ + ".hero__copy", ".hero__art", + "#catalogue .band", ".filters", ".filters__status", + "#programme .band", "#workshops .band", "#visit .visit__lede", "#visit .visit__info" + ]; + if (!reduceMotion) { + revealTargets.forEach(function (sel, i) { + var el = document.querySelector(sel); + if (el) { + el.classList.add("reveal"); + el.style.setProperty("--i", i % 6); // cap stagger window + } + }); + // Cards reveal as a one-shot when the grid enters the viewport. + var cards = Array.prototype.slice.call(document.querySelectorAll(".card")); + if ("IntersectionObserver" in window && cards.length) { + var io = new IntersectionObserver(function (entries, obs) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + var card = entry.target; + var idx = cards.indexOf(card); + card.classList.add("reveal"); + card.style.setProperty("--i", Math.min(idx, 5)); + obs.unobserve(card); + } + }); + }, { rootMargin: "0px 0px -8% 0px", threshold: 0.1 }); + cards.forEach(function (c) { io.observe(c); }); + } + } + + /* ── Catalogue filter ─────────────────────────────────────── */ + var chips = Array.prototype.slice.call(document.querySelectorAll(".chip")); + var cardEls = Array.prototype.slice.call(document.querySelectorAll(".card")); + var status = document.querySelector(".filters__status"); + var emptyMsg = status ? status.getAttribute("data-empty") : ""; + + function applyFilter(value) { + var shown = 0; + cardEls.forEach(function (card) { + var match = value === "all" || card.getAttribute("data-medium") === value; + card.classList.toggle("is-hidden", !match); + if (match) shown += 1; + }); + if (status) { + if (shown === 0) { + status.textContent = emptyMsg; + } else if (value === "all") { + status.textContent = "Showing all " + shown + " of these nine featured studios."; + } else { + var labelChip = chips.filter(function (c) { return c.getAttribute("data-filter") === value; })[0]; + var label = labelChip ? labelChip.textContent.replace(/\s*\d+\s*$/, "").trim().toLowerCase() : value; + status.textContent = "Showing " + shown + " featured " + label + (shown === 1 ? " studio." : " studios."); + } + } + } + + chips.forEach(function (chip) { + chip.addEventListener("click", function () { + chips.forEach(function (c) { + var on = c === chip; + c.classList.toggle("is-active", on); + c.setAttribute("aria-pressed", on ? "true" : "false"); + }); + applyFilter(chip.getAttribute("data-filter")); + }); + }); + + /* ── Signup validation (touched pattern · silent success) ─── */ + var form = document.querySelector(".signup"); + if (form) { + var input = form.querySelector(".signup__input"); + var help = form.querySelector(".signup__help"); + var defaultHelp = help ? help.textContent : ""; + var touched = false; + + function validityMessage() { + var v = input.value.trim(); + if (!v) return "Add your email address so we know where to send the map."; + // Simple, forgiving shape check — enough to catch typos, not gatekeep. + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) { + return "That doesn’t look like an email yet — check for a missing @ or dot."; + } + return ""; + } + + function setError(msg) { + if (msg) { + input.setAttribute("aria-invalid", "true"); + if (help) { help.textContent = msg; help.setAttribute("data-tone", "error"); } + } else { + input.removeAttribute("aria-invalid"); + if (help) { help.textContent = defaultHelp; help.removeAttribute("data-tone"); } + } + } + + input.addEventListener("blur", function () { touched = true; setError(validityMessage()); }); + input.addEventListener("input", function () { if (touched) setError(validityMessage()); }); + + form.addEventListener("submit", function (e) { + e.preventDefault(); + touched = true; + var msg = validityMessage(); + setError(msg); + if (msg) { input.focus(); return; } + // Silent success: the form becomes a plain confirmation in place — no toast. + var row = form.querySelector(".signup__row"); + if (row) row.remove(); + if (help) { + help.textContent = "You’re on the list. Watch for a pink map the week before."; + help.removeAttribute("data-tone"); + } + }); + } +})(); diff --git a/site/examples/riso-01/styles.css b/site/examples/riso-01/styles.css new file mode 100644 index 0000000..7adbda7 --- /dev/null +++ b/site/examples/riso-01/styles.css @@ -0,0 +1,813 @@ +/* Hallmark · pre-emit critique: P5 H5 E5 S5 R4 V5 + * macrostructure: Catalogue · genre: editorial · theme: Riso + * tone: editorial / tactile-print · anchor hue: warm peach 30 · accent: riso cyan 220 (~3%) + * nav: N6 Newspaper masthead · footer: Ft4 Dense colophon + * enrichment: E5 centrepiece + per-card prints · craft: tier-A pure-CSS risograph + * (overprinted cyan/yellow/pink ink layers, multiply blend, deliberate misregistration, grain) — no photos + * F6 product-card knobs: ratio=4/3, density=3-up, micro-action=none (the print is the card) + * F3 timetable knobs: 2-col key/val, rule=every row, numbers=tabular + * motion: stagger reveal (load, one-shot) · card hover-lift · filter crossfade — 3 primitives, reduced-motion safe + * studied: no + * contrast: pass (40–41) · nav: N6 · footer: Ft4 · slop: pass (42–45) + * honest: pass (46) · chrome: pass (47) · tokens: pass (48) · responsive: pass (49) · icons: pass (30) + * mobile: pass (34, 49, 50–57) · slop-test: 57/57 + */ + +*, *::before, *::after { box-sizing: border-box; } + +html { overflow-x: clip; -webkit-text-size-adjust: 100%; scroll-behavior: smooth; } +body { overflow-x: clip; } + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +body { + margin: 0; + background: var(--color-paper); + color: var(--color-ink); + font-family: var(--font-body); + font-size: var(--text-base); + line-height: var(--lh-normal); + font-variant-numeric: oldstyle-nums; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +/* faint paper grain over the whole page — riso stock, barely there */ +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: var(--z-base); + pointer-events: none; + opacity: 0.5; + mix-blend-mode: multiply; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E"); +} + +img, svg { max-width: 100%; display: block; } + +a { color: inherit; } + +::selection { background: var(--color-accent-2); color: var(--color-on-accent-2); } + +:focus-visible { + outline: var(--rule-fine) solid var(--color-focus); + outline-offset: 3px; +} + +.skip-link { + position: absolute; + left: var(--space-md); + top: var(--space-md); + transform: translateY(-150%); + background: var(--color-ink); + color: var(--color-paper); + padding: var(--space-xs) var(--space-md); + font-family: var(--font-label); + font-size: var(--text-sm); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + text-decoration: none; + z-index: var(--z-toast); + transition: transform var(--dur-short) var(--ease-out); +} +.skip-link:focus { transform: translateY(0); } + +/* ── Shared shells ──────────────────────────────────────────── */ +.kicker, +.card__tag, +.band__count, +.slot__where, +.work__when, +.work__cap, +.masthead__issue, +.riso-plate__cap { + font-family: var(--font-label); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-muted); + margin: 0; +} + +/* ── Risograph misregistration — the signature type treatment ── + A layered text-shadow throws a cyan ghost up-left and a pink ghost + down-right, so display words read like a slightly-misregistered + 3-colour print. Roman only (no italic headers). */ +.riso-mis { + color: var(--color-ink); + text-shadow: + -0.02em -0.018em 0 var(--ink-cyan), + 0.022em 0.02em 0 var(--ink-pink); +} + +/* ════════════════════════════════════════════════════════════ + N6 · NEWSPAPER MASTHEAD + ════════════════════════════════════════════════════════════ */ +.masthead { + display: grid; + justify-items: center; + gap: var(--space-xs); + padding: var(--space-lg) var(--page-gutter) 0; + text-align: center; +} +.masthead__issue { + font-variant: small-caps; + letter-spacing: 0.1em; +} +.masthead__name { + font-family: var(--font-display); + font-weight: 900; + font-size: clamp(2.5rem, 8vw, 5rem); + line-height: 0.9; + letter-spacing: var(--tracking-display); + text-decoration: none; + margin: 0; + overflow-wrap: anywhere; +} +.masthead__nav ul { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: var(--space-md) var(--space-lg); + list-style: none; + margin: var(--space-2xs) 0 0; + padding: 0; +} +.masthead__nav a { + font-family: var(--font-label); + font-size: var(--text-sm); + letter-spacing: 0.08em; + text-transform: uppercase; + text-decoration: none; + color: var(--color-ink-2); + padding: var(--space-2xs) 0; + white-space: nowrap; + background-image: linear-gradient(var(--color-accent), var(--color-accent)); + background-repeat: no-repeat; + background-position: 0 100%; + background-size: 0% var(--rule-fine); + transition: background-size var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out); +} +.masthead__nav a:hover { background-size: 100% var(--rule-fine); color: var(--color-ink); } +.masthead__rule { + width: 100%; + height: 5px; + border: 0; + border-top: var(--rule-hair) solid var(--color-rule-2); + border-bottom: var(--rule-hair) solid var(--color-rule-2); + margin: var(--space-sm) 0 0; +} + +/* ════════════════════════════════════════════════════════════ + HERO + ════════════════════════════════════════════════════════════ */ +.hero { + max-width: var(--page-max); + margin-inline: auto; + padding: var(--space-2xl) var(--page-gutter) var(--space-3xl); + display: grid; + grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); + gap: var(--space-2xl); + align-items: center; + min-height: clamp(58vh, 74dvh, 86dvh); +} +.hero__copy { max-width: 40rem; } +.kicker { margin-bottom: var(--space-md); } +.hero__title { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-display); + line-height: var(--lh-tight); + letter-spacing: var(--tracking-display); + margin: 0 0 var(--space-lg); + overflow-wrap: anywhere; + min-width: 0; +} +.hero__title .riso-mis { display: inline-block; } +.hero__lede { + font-size: var(--text-md); + line-height: var(--lh-relaxed); + color: var(--color-ink-2); + max-width: var(--measure); + margin: 0 0 var(--space-xl); +} +.hero__actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-md) var(--space-xl); + margin-bottom: var(--space-2xl); +} + +/* Buttons */ +.btn { + font-family: var(--font-label); + font-size: var(--text-sm); + font-weight: 700; + letter-spacing: 0.06em; + text-transform: uppercase; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: var(--space-xs); + white-space: nowrap; + line-height: 1; + min-height: 44px; + cursor: pointer; + border-radius: var(--radius-pill); +} +.btn:focus-visible { outline: var(--rule-fine) solid var(--color-focus); outline-offset: 3px; } +.btn:disabled, +.btn[aria-disabled="true"] { opacity: 0.55; cursor: not-allowed; transform: none; } +.btn--solid { + background: var(--color-accent-deep); + color: var(--color-on-accent); + border: var(--rule-fine) solid var(--color-accent-deep); + padding: var(--space-sm) var(--space-lg); + transition: transform var(--dur-micro) var(--ease-out), background-color var(--dur-short) var(--ease-out), border-color var(--dur-short) var(--ease-out); +} +.btn--solid:hover { background: var(--color-ink); border-color: var(--color-ink); transform: translateY(-1.5px); } +.btn--solid:active { transform: translateY(1px); } +.btn--solid:disabled:hover, +.btn--solid[aria-disabled="true"]:hover { background: var(--color-accent-deep); border-color: var(--color-accent-deep); transform: none; } +.btn--text { + background: none; + border: 0; + color: var(--color-ink); + padding: var(--space-sm) 0; + background-image: linear-gradient(var(--color-accent), var(--color-accent)); + background-repeat: no-repeat; + background-position: 0 100%; + background-size: 100% var(--rule-hair); + transition: background-size var(--dur-short) var(--ease-out); +} +.btn--text:hover { background-size: 100% var(--rule-fine); } + +/* Hero facts — small, mono, dl */ +.hero__facts { + display: grid; + gap: var(--space-sm); + margin: 0; + padding-top: var(--space-lg); + border-top: var(--rule-hair) solid var(--color-rule); +} +.fact { display: grid; grid-template-columns: 6rem 1fr; gap: var(--space-md); align-items: baseline; } +.fact dt { + font-family: var(--font-label); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-accent-ink); +} +.fact dd { margin: 0; font-size: var(--text-sm); color: var(--color-ink-2); } + +/* ── Hero CSS riso plate (Tier-A pure-CSS art) ──────────────── */ +.hero__art { margin: 0; display: grid; gap: var(--space-sm); justify-items: end; } +.riso-plate { + position: relative; + width: 100%; + max-width: 26rem; + aspect-ratio: 4 / 5; + isolation: isolate; + background: var(--color-paper-2); + border: var(--rule-card) solid var(--color-rule-2); +} +.riso-ink { + position: absolute; + mix-blend-mode: multiply; +} +/* yellow drum — a big rising sun-disc, bottom-anchored */ +.riso-ink--yellow { + inset: 24% 14% -14% 12%; + background: radial-gradient(circle at 50% 45%, var(--ink-yellow) 0 46%, transparent 47%); +} +/* cyan drum — a tilted bar of "type", thrown slightly up-left (the misregister) */ +.riso-ink--cyan { + inset: 12% 18% 30% 16%; + transform: translate(-3%, -2.5%) rotate(-4deg); + background: + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 0 18% / 100% 9% no-repeat, + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 0 42% / 76% 9% no-repeat, + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 0 66% / 90% 9% no-repeat, + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 0 90% / 54% 9% no-repeat; +} +/* pink drum — an off-register ring, thrown down-right */ +.riso-ink--pink { + inset: 30% 10% 22% 30%; + transform: translate(3.5%, 3%) rotate(2deg); + background: + radial-gradient(circle at 60% 50%, transparent 0 30%, + var(--ink-pink) 31% 41%, transparent 42%); +} +/* per-plate grain, denser than the page grain */ +.riso-grain { + position: absolute; + inset: 0; + z-index: var(--z-raised); + pointer-events: none; + opacity: 0.55; + mix-blend-mode: multiply; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='90' height='90'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='90' height='90' filter='url(%23g)' opacity='0.07'/%3E%3C/svg%3E"); +} +.riso-plate__cap { align-self: end; } + +/* ════════════════════════════════════════════════════════════ + BANDS (section heads — stacked single column, no eyebrow tags) + ════════════════════════════════════════════════════════════ */ +.band { + max-width: var(--page-max); + margin-inline: auto; + padding-inline: var(--page-gutter); + display: grid; + gap: var(--space-xs); + margin-bottom: var(--space-xl); +} +.band__title { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-3xl); + line-height: var(--lh-snug); + letter-spacing: var(--tracking-tight); + margin: 0; + overflow-wrap: anywhere; +} +.band--invert { + background: var(--color-ink); + color: var(--color-paper); + max-width: none; + padding-block: var(--space-xl); +} +.band--invert .band__title { color: var(--color-paper); } +.band--invert .band__count { color: var(--color-paper-3); } +.band--invert > * { max-width: var(--page-max); margin-inline: auto; width: 100%; } + +/* ════════════════════════════════════════════════════════════ + CATALOGUE + ════════════════════════════════════════════════════════════ */ +.catalogue { + padding: var(--section-gap) 0; +} + +/* Filter chips */ +.filters { + max-width: var(--page-max); + margin: 0 auto var(--space-md); + padding-inline: var(--page-gutter); + display: flex; + flex-wrap: wrap; + gap: var(--space-sm); +} +.chip { + font-family: var(--font-label); + font-size: var(--text-sm); + letter-spacing: 0.04em; + color: var(--color-ink-2); + background: transparent; + border: var(--rule-card) solid var(--color-rule-2); + border-radius: var(--radius-pill); + padding: var(--space-xs) var(--space-md); + min-height: 44px; + cursor: pointer; + transition: background-color var(--dur-short) var(--ease-out), color var(--dur-short) var(--ease-out), border-color var(--dur-short) var(--ease-out); +} +.chip__n { color: var(--color-muted); font-size: var(--text-xs); } +.chip:hover { border-color: var(--color-ink); color: var(--color-ink); } +.chip:active { transform: translateY(1px); } +.chip:disabled { opacity: 0.55; cursor: not-allowed; } +.chip.is-active { + background: var(--color-ink); + color: var(--color-paper); + border-color: var(--color-ink); +} +.chip.is-active .chip__n { color: var(--color-accent-2); } + +.filters__status { + max-width: var(--page-max); + margin: 0 auto var(--space-xl); + padding-inline: var(--page-gutter); + font-family: var(--font-label); + font-size: var(--text-xs); + letter-spacing: 0.04em; + color: var(--color-muted); +} + +/* Uniform card grid */ +.grid { + max-width: var(--page-max); + margin: 0 auto; + padding-inline: var(--page-gutter); + list-style: none; + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: var(--space-2xl) var(--space-xl); +} +.card { + display: grid; + gap: var(--space-md); + align-content: start; +} +.card.is-hidden { display: none; } +.card__media { + display: block; + aspect-ratio: 4 / 3; + position: relative; + isolation: isolate; + overflow: hidden; + background: var(--color-paper-2); + border: var(--rule-card) solid var(--color-rule); + transition: transform var(--dur-short) var(--ease-out), border-color var(--dur-short) var(--ease-out); +} +.card__media:hover { transform: translateY(-3px); border-color: var(--color-ink); } +.card__meta { display: grid; gap: var(--space-2xs); } +.card__name { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-xl); + line-height: 1.05; + letter-spacing: var(--tracking-tight); + margin: 0; + overflow-wrap: anywhere; +} +.card__line { + font-size: var(--text-sm); + line-height: var(--lh-normal); + color: var(--color-ink-2); + margin: 0; +} + +/* ── Per-card CSS "prints" — every card is a different riso composition. + Each .print stacks 2–3 multiply-blended ink layers via background + + a tiny misregister, then a grain overlay (::after). Hand-built, no photos. */ +.print { position: absolute; inset: 0; z-index: var(--z-base); } +.print::after { + content: ""; + position: absolute; + inset: 0; + z-index: var(--z-raised); + pointer-events: none; + opacity: 0.5; + mix-blend-mode: multiply; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80'%3E%3Cfilter id='c'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.05' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='80' height='80' filter='url(%23c)' opacity='0.06'/%3E%3C/svg%3E"); +} + +/* 1 · concentric cyan rings, yellow disc behind, pink ghost — "working press" */ +.print--1 { + background: + radial-gradient(circle at 38% 56%, var(--ink-pink) 0 22%, transparent 23%), + repeating-radial-gradient(circle at 44% 50%, var(--ink-cyan) 0 6%, transparent 6% 14%), + radial-gradient(circle at 50% 50%, var(--ink-yellow) 0 60%, transparent 61%), + var(--color-paper-2); + background-blend-mode: multiply, multiply, multiply, normal; +} +/* 2 · stacked horizontal bars (a zine spread), cyan over yellow */ +.print--2 { + background: + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 8% 22% / 60% 12% no-repeat, + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 8% 46% / 84% 12% no-repeat, + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 8% 70% / 40% 12% no-repeat, + linear-gradient(135deg, var(--ink-yellow) 0 70%, transparent 70%), + var(--color-paper-2); + background-blend-mode: multiply, multiply, multiply, multiply, normal; +} +/* 3 · big pink triangle + cyan grid — gig poster */ +.print--3 { + background: + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 0 0 / 100% 8% repeat-y, + conic-gradient(from -30deg at 50% 88%, var(--ink-pink) 0 60deg, transparent 60deg), + var(--color-accent-2); + background-blend-mode: multiply, multiply, normal; + background-size: 14% 100%, 100% 100%, auto; +} +/* 4 · panel grid (comic), yellow blocks + cyan gutter */ +.print--4 { + background: + linear-gradient(var(--ink-cyan), var(--ink-cyan)), + repeating-linear-gradient(90deg, var(--ink-yellow) 0 31%, transparent 31% 34%), + repeating-linear-gradient(0deg, var(--ink-yellow) 0 44%, transparent 44% 50%), + var(--color-paper-2); + background-blend-mode: multiply, multiply, multiply, normal; + background-size: 100% 100%, 100% 100%, 100% 100%, auto; + background-position: 0 0; + opacity: 0.96; +} +/* 5 · botanical sprig — cyan stem, yellow leaves, teal-on-teal feel */ +.print--5 { + background: + radial-gradient(ellipse 14% 26% at 36% 40%, var(--ink-cyan) 0 70%, transparent 72%), + radial-gradient(ellipse 14% 26% at 64% 58%, var(--ink-cyan) 0 70%, transparent 72%), + linear-gradient(8deg, transparent 47%, var(--ink-cyan) 47% 53%, transparent 53%), + radial-gradient(circle at 50% 50%, var(--ink-yellow) 0 64%, transparent 65%), + var(--color-paper-2); + background-blend-mode: multiply, multiply, multiply, multiply, normal; +} +/* 6 · halftone dot field, cyan over yellow — "big dots on purpose" */ +.print--6 { + background: + radial-gradient(var(--ink-cyan) 30%, transparent 32%), + radial-gradient(var(--ink-yellow) 30%, transparent 32%), + var(--color-paper-2); + background-blend-mode: multiply, multiply, normal; + background-size: 18% 18%, 18% 18%, auto; + background-position: 0 0, 9% 9%, 0 0; +} +/* 7 · open book / pink spread with cyan spine — graphic novel */ +.print--7 { + background: + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 49% 12% / 3% 76% no-repeat, + linear-gradient(95deg, var(--ink-pink) 0 47%, transparent 47% 53%, var(--ink-pink) 53%), + var(--color-paper-2); + background-blend-mode: multiply, multiply, normal; +} +/* 8 · single bold letterform-ish mark — cyan slab + yellow counter */ +.print--8 { + background: + radial-gradient(circle at 52% 46%, var(--color-paper-2) 0 16%, transparent 17%), + linear-gradient(var(--ink-cyan), var(--ink-cyan)) 30% 18% / 40% 64% no-repeat, + radial-gradient(circle at 52% 78%, var(--ink-yellow) 0 26%, transparent 27%), + var(--color-paper-2); + background-blend-mode: normal, multiply, multiply, normal; +} +/* 9 · overlapping circles (anthology), pink + cyan venn over yellow */ +.print--9 { + background: + radial-gradient(circle at 40% 50%, var(--ink-pink) 0 32%, transparent 33%), + radial-gradient(circle at 60% 50%, var(--ink-cyan) 0 32%, transparent 33%), + radial-gradient(circle at 50% 38%, var(--ink-yellow) 0 30%, transparent 31%), + var(--color-paper-2); + background-blend-mode: multiply, multiply, multiply, normal; +} + +.catalogue__more { + max-width: var(--page-max); + margin: var(--space-2xl) auto 0; + padding-inline: var(--page-gutter); + font-size: var(--text-md); + line-height: var(--lh-relaxed); + color: var(--color-ink-2); + max-width: 52ch; + padding-inline: var(--page-gutter); +} +.catalogue__more em { font-style: italic; color: var(--color-ink); } + +/* ════════════════════════════════════════════════════════════ + PROGRAMME (tabular) + ════════════════════════════════════════════════════════════ */ +.programme { padding-bottom: var(--section-gap); } +.programme .band--invert { margin-bottom: var(--space-2xl); } +.prog__day { + max-width: var(--page-max); + margin: 0 auto var(--space-2xl); + padding-inline: var(--page-gutter); +} +.prog__heading { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-2xl); + letter-spacing: var(--tracking-tight); + margin: 0 0 var(--space-md); +} +.prog__heading .riso-mis { display: inline-block; } +.timetable { margin: 0; } +.slot { + display: grid; + grid-template-columns: 7rem 1fr; + gap: var(--space-lg); + align-items: baseline; + padding: var(--space-md) 0; + border-top: var(--rule-hair) solid var(--color-rule); +} +.slot:last-child { border-bottom: var(--rule-hair) solid var(--color-rule); } +.slot__time { + font-family: var(--font-label); + font-weight: 700; + font-size: var(--text-md); + font-variant-numeric: tabular-nums; + color: var(--color-accent-ink); + margin: 0; +} +.slot__body { display: flex; flex-wrap: wrap; gap: var(--space-xs) var(--space-md); align-items: baseline; justify-content: space-between; margin: 0; } +.slot__title { font-size: var(--text-md); color: var(--color-ink); } + +/* ════════════════════════════════════════════════════════════ + WORKSHOPS (varied list, not icon tiles) + ════════════════════════════════════════════════════════════ */ +.workshops { padding-bottom: var(--section-gap); } +.work-list { + max-width: var(--page-max); + margin: 0 auto; + padding-inline: var(--page-gutter); + list-style: none; + display: grid; + gap: 0; +} +.work { + display: grid; + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.4fr) minmax(0, 0.5fr); + gap: var(--space-lg) var(--space-2xl); + align-items: start; + padding: var(--space-xl) 0; + border-top: var(--rule-hair) solid var(--color-rule-2); +} +.work:last-child { border-bottom: var(--rule-hair) solid var(--color-rule-2); } +.work__name { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-xl); + line-height: 1.08; + letter-spacing: var(--tracking-tight); + margin: 0; + overflow-wrap: anywhere; +} +.work__body { + font-size: var(--text-base); + line-height: var(--lh-relaxed); + color: var(--color-ink-2); + margin: 0; + max-width: var(--measure); +} +.work__cap { text-align: right; } + +/* ════════════════════════════════════════════════════════════ + VISIT + ════════════════════════════════════════════════════════════ */ +.visit { + max-width: var(--page-max); + margin-inline: auto; + padding: var(--section-gap) var(--page-gutter); + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr); + gap: var(--space-3xl); + align-items: start; +} +.visit__title { + font-family: var(--font-display); + font-weight: var(--display-weight); + font-size: var(--text-3xl); + line-height: var(--lh-snug); + letter-spacing: var(--tracking-tight); + margin: 0 0 var(--space-md); + overflow-wrap: anywhere; +} +.visit__body { + font-size: var(--text-md); + line-height: var(--lh-relaxed); + color: var(--color-ink-2); + max-width: var(--measure); + margin: 0 0 var(--space-xl); +} + +/* Signup (C2 inline form) */ +.signup { display: grid; gap: var(--space-xs); } +.signup__label { + font-family: var(--font-label); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-accent-ink); +} +.signup__row { display: flex; flex-wrap: wrap; align-items: stretch; gap: var(--space-sm); } +.signup__input { + flex: 1 1 16rem; + min-width: 0; + font-family: var(--font-body); + font-size: var(--text-md); + color: var(--color-ink); + background: var(--color-paper-2); + border: var(--rule-card) solid var(--color-rule-2); + border-radius: var(--radius-input); + padding: var(--space-sm) var(--space-md); + min-height: 44px; + transition: border-color var(--dur-short) var(--ease-out), background-color var(--dur-short) var(--ease-out); +} +.signup__input::placeholder { color: var(--color-muted); } +.signup__input:hover { border-color: var(--color-rule-2); } +.signup__input:focus-visible { border-color: var(--color-focus); background: var(--color-paper); } +.signup__input[aria-invalid="true"] { border-color: var(--color-accent-3); background: var(--color-paper); } +.signup__input:disabled { opacity: 0.55; cursor: not-allowed; } +.signup__submit { flex: 0 0 auto; } +.signup__help { margin: var(--space-2xs) 0 0; min-height: 1.5lh; font-size: var(--text-sm); color: var(--color-muted); } +.signup__help[data-tone="error"] { color: var(--color-accent-3); font-family: var(--font-label); font-size: var(--text-xs); letter-spacing: 0.02em; } + +/* Visit info */ +.visit__info { + display: grid; + gap: var(--space-lg); + margin: 0; + padding-top: var(--space-xs); +} +.info { display: grid; gap: var(--space-3xs); padding-bottom: var(--space-lg); border-bottom: var(--rule-hair) solid var(--color-rule); } +.info:last-child { border-bottom: 0; padding-bottom: 0; } +.info dt { + font-family: var(--font-label); + font-size: var(--text-xs); + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--color-muted); +} +.info dd { margin: 0; font-size: var(--text-base); line-height: var(--lh-normal); color: var(--color-ink); } +.info a { + color: var(--color-accent-ink); + text-underline-offset: 3px; + text-decoration-thickness: var(--rule-hair); +} +.info a:hover { text-decoration-thickness: var(--rule-fine); } + +/* ════════════════════════════════════════════════════════════ + Ft4 · DENSE COLOPHON + ════════════════════════════════════════════════════════════ */ +.colophon { + border-top: var(--rule-fine) solid var(--color-ink); + padding: var(--space-2xl) var(--page-gutter) var(--space-3xl); +} +.colophon__body { + max-width: 70ch; + margin: 0 auto; + font-family: var(--font-label); + font-size: var(--text-sm); + line-height: 1.75; + color: var(--color-ink-2); +} +.colophon__mark { + font-family: var(--font-display); + font-weight: 900; + letter-spacing: var(--tracking-tight); + font-size: var(--text-md); + display: inline-block; + margin-right: var(--space-2xs); +} + +/* ════════════════════════════════════════════════════════════ + MOTION · one orchestrated reveal on load (stagger by DOM index) + ════════════════════════════════════════════════════════════ */ +.reveal { + opacity: 0; + transform: translateY(10px); + animation: reveal var(--dur-long) var(--ease-out) forwards; + animation-delay: calc(var(--i, 0) * 70ms); +} +@keyframes reveal { to { opacity: 1; transform: none; } } + +/* JS-disabled / pre-JS: never leave content hidden */ +.no-js .reveal { opacity: 1; transform: none; animation: none; } + +/* ════════════════════════════════════════════════════════════ + RESPONSIVE + ════════════════════════════════════════════════════════════ */ +@media (max-width: 60rem) { + .hero { + grid-template-columns: 1fr; + gap: var(--space-2xl); + min-height: 0; + padding-block: var(--space-xl) var(--space-2xl); + } + .hero__art { justify-items: start; order: -1; } + .riso-plate { max-width: 20rem; aspect-ratio: 4 / 3; } + + .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-xl) var(--space-lg); } + + .visit { grid-template-columns: 1fr; gap: var(--space-2xl); } + + .work { grid-template-columns: 1fr; gap: var(--space-sm); } + .work__when { order: -1; } + .work__cap { text-align: left; } +} + +@media (max-width: 40rem) { + .masthead { padding-top: var(--space-md); } + .masthead__name { font-size: clamp(2rem, 12vw, 3.25rem); } + + .hero__title { font-size: var(--text-display-s); } + .hero__actions { gap: var(--space-md); } + .btn--solid, .btn--text { width: 100%; justify-content: center; } + .fact { grid-template-columns: 1fr; gap: var(--space-3xs); } + .fact dt { font-size: var(--text-xs); } + + .grid { grid-template-columns: 1fr; gap: var(--space-xl); } + .card__media { aspect-ratio: 16 / 10; } + + .band__title { font-size: var(--text-2xl); } + .visit__title { font-size: var(--text-2xl); } + + .slot { grid-template-columns: 1fr; gap: var(--space-2xs); } + .slot__body { justify-content: flex-start; } + + .signup__row { flex-direction: column; } + .signup__submit { width: 100%; justify-content: center; } +} + +/* ════════════════════════════════════════════════════════════ + REDUCED MOTION + ════════════════════════════════════════════════════════════ */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } + .reveal { opacity: 1; transform: none; animation: none; } + .card__media:hover { transform: none; } + .btn--solid:hover, .btn--solid:active { transform: none; } +} diff --git a/site/examples/riso-01/tokens.css b/site/examples/riso-01/tokens.css new file mode 100644 index 0000000..5b748fe --- /dev/null +++ b/site/examples/riso-01/tokens.css @@ -0,0 +1,110 @@ +/* Hallmark · tokens — RISO theme (catalog) + Risograph print · warm peach paper · riso cyan + yellow accents. + Public Sans 800 display, off-register CMYK misregistration via text-shadow. + Newsreader body serif. Space Mono outlier for catalogue labels. + Diversification axes — paper-band: light (L 91%) · display-style: risograph-bold · accent-hue: cool (cyan 220). + Every colour + font in styles.css references a token here. No inline values. */ + +:root { + /* ── Palette (riso) ───────────────────────────────────────── */ + --color-paper: oklch(91% 0.034 30); + --color-paper-2: oklch(87% 0.040 30); + --color-paper-3: oklch(82% 0.046 30); + --color-rule: oklch(70% 0.040 30); + --color-rule-2: oklch(56% 0.060 30); + --color-muted: oklch(46% 0.060 30); + --color-neutral: oklch(34% 0.060 30); + --color-ink-2: oklch(22% 0.060 30); + --color-ink: oklch(18% 0.060 30); + --color-accent: oklch(58% 0.170 220); /* riso cyan / teal — hairlines, underlines, hovers */ + --color-accent-deep: oklch(48% 0.170 220); /* deeper cyan for filled buttons (AA text ≥ 5:1) */ + --color-accent-2: oklch(78% 0.180 95); /* riso yellow */ + --color-accent-3: oklch(60% 0.200 12); /* riso fluoro-pink (single moment) */ + --color-accent-ink: oklch(42% 0.135 224); /* deep cyan — AA text on peach paper (≥ 4.5:1) */ + --color-focus: oklch(48% 0.170 220); /* darker cyan — AA-contrast focus ring (≥ 3:1) */ + --color-on-accent: oklch(98% 0.012 30); /* paper-tint text on a cyan fill */ + --color-on-accent-2: oklch(18% 0.060 30); /* ink on a yellow fill */ + + /* overprint tints — used by the hand-built riso art (multiply blend) */ + --ink-cyan: oklch(58% 0.170 220 / 0.92); + --ink-yellow: oklch(80% 0.180 95 / 0.90); + --ink-pink: oklch(62% 0.200 12 / 0.88); + + /* ── Type ─────────────────────────────────────────────────── */ + --font-display: "Public Sans", "Albert Sans", ui-sans-serif, system-ui, sans-serif; + --font-body: "Newsreader", "Tiempos", ui-serif, Georgia, serif; + --font-serif: "Newsreader", ui-serif, Georgia, serif; + --font-label: "Space Mono", "Geist Mono", ui-monospace, monospace; /* catalogue labels / outlier */ + --font-mono: "Space Mono", "Geist Mono", ui-monospace, monospace; + + --display-weight: 800; + --display-style: normal; + + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1.0625rem; + --text-md: 1.1875rem; + --text-lg: 1.375rem; + --text-xl: 1.75rem; + --text-2xl: 2.25rem; + --text-3xl: 2.875rem; + --text-display: clamp(3rem, 4.6vw + 1rem, 5.25rem); /* capped for the 52-char hero — gravity, not drama */ + --text-display-s: clamp(2.25rem, 3.5vw + 1rem, 3.5rem); + + --lh-tight: 0.92; + --lh-snug: 1.10; + --lh-normal: 1.55; + --lh-relaxed: 1.65; + + --tracking-display: -0.04em; + --tracking-tight: -0.014em; + --tracking-normal: 0; + --tracking-label: 0.18em; + + /* ── Space (4pt scale) ────────────────────────────────────── */ + --space-3xs: 0.125rem; + --space-2xs: 0.25rem; + --space-xs: 0.5rem; + --space-sm: 0.75rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2.5rem; + --space-2xl: 4rem; + --space-3xl: 6rem; + --space-4xl: 9rem; + + --section-gap: 6rem; + --section-head-gap: 1.5rem; + + --page-max: 78rem; + --page-gutter: clamp(1.25rem, 4vw, 3rem); + --measure: 62ch; + + --rule-hair: 1px; + --rule-fine: 2px; + + /* ── Shape ────────────────────────────────────────────────── */ + --radius-card: 2px; + --radius-pill: 2px; + --radius-input: 2px; + --rule-card: 1px; + --shadow-card: none; + + /* ── Motion ───────────────────────────────────────────────── */ + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --dur-micro: 120ms; + --dur-short: 220ms; + --dur-long: 420ms; + + /* ── Z-index (named) ──────────────────────────────────────── */ + --z-base: 1; + --z-raised: 10; + --z-dropdown: 100; + --z-sticky: 200; + --z-sticky-nav: 300; + --z-modal: 400; + --z-toast: 500; + --z-tooltip: 600; +} diff --git a/site/index.html b/site/index.html index 2422bb3..6e7351c 100644 --- a/site/index.html +++ b/site/index.html @@ -56,10 +56,10 @@ - - - - + + + + + \ No newline at end of file diff --git a/skills/hallmark/references/interaction-and-states.md b/skills/hallmark/references/interaction-and-states.md index 23ed584..0745321 100644 --- a/skills/hallmark/references/interaction-and-states.md +++ b/skills/hallmark/references/interaction-and-states.md @@ -130,6 +130,7 @@ The outline starts transparent at 2 px so when the focus ring appears, the box g ## Modals and overlays - Use the native `` element. It handles focus trap, escape to close, and `::backdrop` styling for free. +- **Centre it explicitly.** A `` opened with `showModal()` centres on its own — but the moment you add custom positioning (a `margin-top`, a `top`, a stray `transform`) it can snap to the viewport's top-left corner. Pin it: `position: fixed; inset: 0; margin: auto; height: fit-content; max-height: min(80vh, 40rem);`. A non-`` overlay centres via its container instead: `position: fixed; inset: 0; display: grid; place-items: center;`. Never ship a modal or command palette stuck in the corner. - Set `inert` on the page content behind a modal so tab order doesn't leak. - Close on: escape key, backdrop click, explicit close button. - First focus goes to the first interactive element, not the close button. diff --git a/skills/hallmark/references/layout-and-space.md b/skills/hallmark/references/layout-and-space.md index f6e4b3c..8844d51 100644 --- a/skills/hallmark/references/layout-and-space.md +++ b/skills/hallmark/references/layout-and-space.md @@ -41,11 +41,12 @@ Layout is where "AI-generated" gets caught. Equal columns, everything centred, e ## Asymmetry techniques -- **Wide left margin.** Treat the left as a permanent negative space — narrow column of labels, wide column of content. **Labels must NOT be section eyebrows / numbers paired with the heading** — that's gate-66-banned. Reserve this technique for body-level micro-labels (caption, footnote, date) alongside body copy. +- **Wide left margin.** Treat the left as a permanent negative space — narrow column of labels, wide column of content. **Labels must NOT be section eyebrows / numbers paired with the heading** — that's gate-54-banned. Reserve this technique for body-level micro-labels (caption, footnote, date) alongside body copy. - **Hanging headers.** ⚠️ **Opt-in only.** Section labels sit in the left margin; content flows to the right. Permitted only when the user explicitly asks for an editorial / hanging-header layout AND no eyebrow / number / chapter tag sits in the left margin. The eyebrow-left / heading-right pattern is banned by slop-test gate 54 — it's the most reliable templated-editorial AI tell. Default to a stacked single-column section head. - **Offset grids.** Odd columns wider than even. Or the other way. - **Grid-breaks.** One element that deliberately extends past a column boundary: a pull-quote, a photograph, a rule, a number. - **Generous top, tight bottom** (or vice-versa). Sections don't need to be evenly padded. +- **Alignment coherence.** A section head's horizontal alignment should be a deliberate choice that *coheres* with the body it introduces — match it (left head over left-flush body; centred head over symmetric / centred body) or break from it on purpose (a head that escapes an asymmetric grid). What reads as an AI mistake is the *accidental* mismatch: a narrow head block auto-centred (`margin-inline: auto` plus a `max-width` / `ch` cap) left floating over full-width, left-flush content beneath it. Centred, hanging, bottom-aligned, and asymmetric heads all stay on the table — the guard is intentionality, not uniformity. ## Depth diff --git a/skills/hallmark/references/macrostructures/04-stat-led.md b/skills/hallmark/references/macrostructures/04-stat-led.md index b2a7501..7d99d10 100644 --- a/skills/hallmark/references/macrostructures/04-stat-led.md +++ b/skills/hallmark/references/macrostructures/04-stat-led.md @@ -2,7 +2,7 @@ The hero is a giant number — a metric, a count, a percentage. Everything that follows supports or qualifies it. Data is the narrative. -- **Heading:** numeric display (8–12 rem), tabular figures, small qualifier line below. +- **Heading:** a large numeric display (8–12 rem, tabular figures) **paired with a worded headline** — the figure is the biggest element, but it is *never the hero's only words*. State what the number means right beside or beneath it as a real headline, not a tiny label. Read the figure and its words together (*"4 seconds. From the alert link to the slow span."*). - **Body:** sections each anchored by a supporting stat or chart. - **Divider:** hairline rules between stat blocks; tabular-nums everywhere. - **Button:** outlined chip aligned beneath the qualifier. @@ -13,6 +13,8 @@ Reach for it when the brief is "we have proof in numbers" — enterprise/B2B, fu Avoid for products without a defensible single metric. A fake big number is worse than no number. +**The hero header always contains words.** The lead figure never stands alone as the headline — pair it with a worded line that completes it. A bare number as the dominant hero text fails slop-test gate 46. + Reference: Ahrefs, Stripe Sessions stat blocks, climate-impact dashboards, venture firm portfolio pages. **Sample opening lines** (imitate the *specificity*, not the wording — the number does the work): diff --git a/skills/hallmark/references/slop-test.md b/skills/hallmark/references/slop-test.md index 638df11..eeae520 100644 --- a/skills/hallmark/references/slop-test.md +++ b/skills/hallmark/references/slop-test.md @@ -143,7 +143,7 @@ The CSS stamp at Step 6 should record the result alongside contrast: `· nav: N# Universal — apply to every genre. The page must not invent facts about the user's product, team, or market. -46. **Invented metric.** Does the page contain any quantitative claim — "10× faster", "saves 5 hours per week", "trusted by 50,000+ teams", "99.9 % uptime", "+47 % conversion" — that the user did not supply, that has no source, and that the model fabricated to fill a stat-led layout, comparison row, or proof bar? If yes, fail. The fix is one of: replace the number with `—` and a labelled grey block, replace it with a question to the user ("metric to confirm"), or rebuild the section without the proof slot. Stat-led macrostructures are slop the moment their stats become decorative. *(See [anti-patterns.md § Invented metrics](anti-patterns.md).)* +46. **Invented metric.** Does the page contain any quantitative claim — "10× faster", "saves 5 hours per week", "trusted by 50,000+ teams", "99.9 % uptime", "+47 % conversion" — that the user did not supply, that has no source, and that the model fabricated to fill a stat-led layout, comparison row, or proof bar? If yes, fail. The fix is one of: replace the number with `—` and a labelled grey block, replace it with a question to the user ("metric to confirm"), or rebuild the section without the proof slot. Stat-led macrostructures are slop the moment their stats become decorative. **A stat is also never the hero's *sole* headline** — a lead figure carries the hero only alongside a worded headline; a giant number with no words as the dominant hero text is a bare-number tell, so pair it with a line that says what it means. *(See [anti-patterns.md § Invented metrics](anti-patterns.md).)* ## Re-drawn UI chrome diff --git a/skills/hallmark/references/structure.md b/skills/hallmark/references/structure.md index 9f6cd37..e4e5e32 100644 --- a/skills/hallmark/references/structure.md +++ b/skills/hallmark/references/structure.md @@ -23,6 +23,8 @@ Where does a section's title live in space? Pick one per page. | **Numbered display** | ⚠️ **Opt-in only — never default.** "01." with a rule line and the heading right beside it. Procedural, sequenced. Banned for default SaaS / consumer / dev-tool pages by slop-test gate 54 (the tag-beside-heading pattern is a templated tell). Permitted only when the user explicitly asks for ordinal / chaptered numbering AND the macrostructure is Long Document, Manifesto, or Catalogue numbered. Even then, prefer the stacked variant: number on its own line above the heading. | Rauno Freiberg's portfolio — when the user explicitly invokes that voice. | | **Inline with body** | No section break — the heading emerges from the paragraph flow. Conversational. | Medium articles; long-form essays. | +**Coherence, not a fixed side.** Any of these placements is fair game — the head's alignment just has to *cohere* with the body it introduces rather than mismatch it by accident. A narrow centred head stranded over full-width, left-flush content (often a `margin-inline:auto` head above a wide grid) is the tell — not centring or left-flushing as such. See [`layout-and-space.md`](layout-and-space.md) § Asymmetry techniques. + ### 2. Body composition How does long-form content lay out beyond "single column at 65ch"? diff --git a/skills/hallmark/references/themes/carnival.md b/skills/hallmark/references/themes/carnival.md index 1f51c41..d3b8767 100644 --- a/skills/hallmark/references/themes/carnival.md +++ b/skills/hallmark/references/themes/carnival.md @@ -42,11 +42,11 @@ Loud + neon. 90s zine, summer, electric. Lime against magenta — the hardest co - `--color-ink: oklch(20% 0.07 145)` — deep forest - `--color-ink-2: oklch(32% 0.05 140)` - `--color-muted: oklch(48% 0.04 130)` -- `--color-rule: oklch(48% 0.24 350)` — magenta rules +- `--color-rule: oklch(28% 0.28 350)` — deep magenta rules - `--color-accent: oklch(82% 0.20 130)` — chartreuse-lime -- `--color-accent-2: oklch(50% 0.24 350)` — magenta +- `--color-accent-2: oklch(28% 0.28 350)` — deep magenta (reads AA on paper, and as text) - `--color-accent-ink: oklch(20% 0.07 145)` -- `--color-focus: oklch(50% 0.24 350)` +- `--color-focus: oklch(28% 0.28 350)` **When to pick:** zine collectives · summer drops · skate / DIY culture · briefs that want LOUD without referencing music. @@ -78,11 +78,11 @@ Cool + cool. Dusk warmth, cyan + plum. Late-night booth, blue hour, podcast stud - `--color-ink: oklch(20% 0.05 270)` — deep navy-black - `--color-ink-2: oklch(32% 0.045 265)` - `--color-muted: oklch(48% 0.04 260)` -- `--color-rule: oklch(42% 0.16 320)` — plum rules +- `--color-rule: oklch(24% 0.18 320)` — deep plum rules - `--color-accent: oklch(78% 0.18 220)` — cyan -- `--color-accent-2: oklch(42% 0.16 320)` — plum +- `--color-accent-2: oklch(24% 0.18 320)` — deep plum (reads AA on paper, and as text) - `--color-accent-ink: oklch(20% 0.05 270)` -- `--color-focus: oklch(42% 0.16 320)` +- `--color-focus: oklch(24% 0.18 320)` **When to pick:** late-night radio · podcasts about anything · music + atmosphere · briefs mentioning "late", "after dark", "blue hour", "moonlight". @@ -96,11 +96,11 @@ Cool + warm. Turquoise against coral. Summer pool, motel sign, vacation. - `--color-ink: oklch(20% 0.06 200)` — deep teal - `--color-ink-2: oklch(32% 0.05 198)` - `--color-muted: oklch(48% 0.04 195)` -- `--color-rule: oklch(60% 0.20 35)` — coral rules +- `--color-rule: oklch(36% 0.24 35)` — deep coral rules - `--color-accent: oklch(72% 0.16 195)` — turquoise -- `--color-accent-2: oklch(60% 0.20 35)` — coral +- `--color-accent-2: oklch(36% 0.24 35)` — deep coral (reads AA on paper, and as text) - `--color-accent-ink: oklch(20% 0.06 200)` -- `--color-focus: oklch(60% 0.20 35)` +- `--color-focus: oklch(36% 0.24 35)` **When to pick:** summer brands · vacation / hospitality · skate / surf / pool · briefs mentioning summer, beach, motel, sun. @@ -126,6 +126,19 @@ Warm + cool. Amber-gold against slate-blue. 1950s journalism, print shop, broads The diversification log (`/.hallmark/log.json`) records the drop alongside the theme: `"theme": "carnival", "drop": "studio-night"`. **A new Carnival build picks a drop that hasn't appeared in the last 3 entries.** If only Cold Snap is in the log, any of the other five is valid. If the brief contains a strong drop signal (see "when to pick" above), honour the signal even if it tightens diversification. +### Pick the drop by domain first + +Match the brief's **domain** before reaching for the loudest palette: + +- food / drink / hospitality / street market → **Diner Sign** (or Cold Snap) +- independent music / labels / EPs → **Cold Snap** (or Pressroom) +- late-night / radio / podcast / after-dark → **Studio Night** +- summer / pool / beach / vacation → **Aqua Park** +- zine / skate / DIY / deliberately chaotic → **Citrus Riot** +- journalism / newsletter / editorial → **Pressroom** + +Citrus Riot and Aqua Park are the highest-chroma drops — reach for them when the brief genuinely *wants* maximum loudness, not as a default. A food market reads better in Diner Sign than in lime-on-magenta. + ### When to construct a custom drop instead If the brief explicitly names a brand colour that doesn't fit any catalog drop — e.g. *"our brand is teal and beige"* — route to the **custom theme** branch (see `custom-theme.md`) instead of stretching a Carnival drop. Drops are curated, not infinite. Six is the right number for now. @@ -162,7 +175,7 @@ The theme's seven tics. A Carnival build should exhibit at least five of them. 4. **Hard-offset drop shadow.** Every card, CTA, and image gets `box-shadow: 4px 4px 0 var(--color-ink)`. No soft shadows ever. The shadow is **flat ink, offset**. This is the single most-recognisable Carnival move. -5. **All-caps headlines with Big Shoulders variable-width** pulled wide. `font-variation-settings: "wdth" 110, "wght" 800;` for hero headlines. Tracking 0.04em. Looks like a marquee poster. +5. **All-caps headlines with Big Shoulders variable-width, tightly tracked.** `font-variation-settings: "wdth" 110, "wght" 800;`. Track them **tight**, not loose: `letter-spacing: -0.005em` on the hero word, `0.02em` on section heads. The loose `0.04em` look reads as AI-spread and is reserved **only** for the marquee banner (where horizontal spread is the point). Keep line-height tight too: `0.82` for single hero words, `0.92` for multi-line heads. The effect is a marquee poster set by a typographer, not a stretched default. 6. **Halftone pattern fills** in placeholder image regions. Pure CSS, no images: @@ -196,6 +209,10 @@ Carnival's dense visual language has known traps. Read this list **before** writ 5. **Big Shoulders width axis used without `font-variation-settings` → no width variation.** Setting `font-stretch` on Google's Big Shoulders Display does not work — only `font-variation-settings: "wdth" 110` does. If you see a hero that's supposed to be width-110 but renders at width-100, this is the bug. +6. **Section head detached from, or mis-centred over, its body.** The section head (heading + any lede) sits **tight above** the content it names — about `1.25rem` (`--section-head-gap`) — and **shares that content's alignment**. Don't cap the head at a narrow `max-width` / `ch` and then `margin-inline: auto` it: that strands a centred head over a full-width, left-flush grid (the classic accidental mismatch). Left-flush grid → left-flush head. (See the alignment-coherence note in `layout-and-space.md`.) + +7. **Newsletter / CTA button not aligned with the input beside it.** When a button sits next to a form input in one row, they must share the **same height, vertical padding, and border width**, and the row must use `align-items: center` — otherwise the button floats above the input's baseline. If a label is stacked above the input it makes that column taller; centre the row on the *control*, not the column (give the field `align-items: center` or pull the label out of the flex row). + ## Macrostructure affinity **Carnival loves these.** @@ -273,7 +290,7 @@ When emitting a Carnival page, the first 12 lines of CSS should establish the fo ```css body { background: var(--color-paper); color: var(--color-ink); font-family: var(--font-body); } -h1, h2 { font-family: var(--font-display); font-weight: 800; font-variation-settings: "wdth" 110; letter-spacing: 0.04em; text-transform: uppercase; } +h1, h2 { font-family: var(--font-display); font-weight: 800; font-variation-settings: "wdth" 110; letter-spacing: 0.02em; line-height: 0.92; text-transform: uppercase; } /* hero word goes tighter: letter-spacing: -0.005em; line-height: 0.82 */ .card, .cta { box-shadow: 4px 4px 0 var(--color-ink); border: 2px solid var(--color-ink); } .section:nth-of-type(odd) { background: var(--color-accent); color: var(--color-accent-ink); } .section:nth-of-type(even) { background: var(--color-accent-2); color: var(--color-paper); }