Carnival rework, sharper anti-slop guards, and three new worked examples

Carnival: fixed the three palette drops whose second accent failed contrast,
tightened the display tracking, and added section-rhythm, alignment, and
drop-selection guidance so the theme lands well-aligned and readable by
default. Lightened and tightened the Carnival headers on the landing.

Anti-slop guards (skill references): heading-to-body alignment coherence as a
principle rather than a rigid rule; centered overlays and command palettes must
center via their container; and a hero headline is never only a number, a lead
figure always pairs with words.

Worked Examples: added three generated pages to the landing carousel, a
small-batch honey farm, a risograph print fair, and an experimental type
studio, after fixing their nav centering and section alignment.

Media: removed the Soroe example card along with the unused scroll-videos,
poster stills, and orphaned thumbnails it left behind, and bumped the asset
cache-bust so the restyled CSS reloads.
This commit is contained in:
Youssef
2026-06-03 15:27:32 +01:00
parent 72f3dc052e
commit 7a890a365d
44 changed files with 4500 additions and 39 deletions
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 814 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 232 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

+3 -2
View File
@@ -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 */
+386
View File
@@ -0,0 +1,386 @@
<!DOCTYPE html>
<html lang="en" data-theme="garden">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hollowback Apiary — small-batch honey from one Vermont ridge</title>
<meta name="description" content="Nine hives on a single ridge in northern Vermont. Unfiltered, single-season honey, hand-poured in lots of a few hundred jars. Reserve from this season's lot.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Young+Serif&family=Hanken+Grotesk:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./tokens.css">
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<a class="skip-link" href="#lot">Skip to this season&rsquo;s lot</a>
<!-- ── N6 · Newspaper masthead ─────────────────────────────── -->
<header class="masthead" role="banner">
<div class="masthead__inner">
<p class="masthead__dateline">Field Almanac &middot; Northern Vermont &middot; Season&nbsp;2026</p>
<a class="masthead__wordmark" href="#top">Hollowback Apiary</a>
<nav class="masthead__nav" aria-label="Primary">
<a href="#ridge">The&nbsp;ridge</a>
<a href="#lot">This season&rsquo;s&nbsp;lot</a>
<a href="#poured">How it&rsquo;s&nbsp;poured</a>
<a class="masthead__cta" href="#standing">Reserve a&nbsp;jar</a>
</nav>
</div>
</header>
<main id="top">
<!-- ── Hero · typographic + hand-built honeycomb/jar craft ── -->
<section class="hero" aria-labelledby="hero-title">
<div class="hero__copy">
<p class="hero__eyebrow reveal" style="--i:0">Nine hives. One ridge. No blending.</p>
<h1 class="hero__title reveal" id="hero-title" style="--i:1">
Honey that tastes like one&nbsp;hillside.
</h1>
<p class="hero__lede reveal" style="--i:2">
We keep bees on a single south-facing ridge above the Lamoille valley and
pour what they make, one season at a time &mdash; unfiltered, unblended, never
warmed past the temperature of the hive.
</p>
<div class="hero__actions reveal" style="--i:3">
<a class="btn btn--solid" href="#lot">See this season&rsquo;s lot</a>
<a class="btn btn--ghost" href="#ridge">Read about the ridge</a>
</div>
<p class="hero__note reveal" style="--i:4">
<span class="hero__note-mark" aria-hidden="true"></span>
Lot No.&nbsp;14 &middot; poured 4&ndash;5 September &middot; 312 jars
</p>
</div>
<!-- Tier-A CSS honeycomb wash + Tier-B hand-built SVG jar.
Decorative, but anchored: it is the jar the page is selling. -->
<figure class="hero__craft reveal" style="--i:3" aria-label="An illustration of a glass jar of amber honey resting on a field of honeycomb">
<div class="comb" aria-hidden="true">
<span style="--c:0"></span><span style="--c:1"></span><span style="--c:2"></span>
<span style="--c:3"></span><span style="--c:4"></span><span style="--c:5"></span>
<span style="--c:6"></span><span style="--c:7"></span><span style="--c:8"></span>
<span style="--c:9"></span><span style="--c:10"></span><span style="--c:11"></span>
</div>
<svg class="jar" viewBox="0 0 200 240" aria-hidden="true" focusable="false">
<!-- jar glass -->
<path class="jar__glass" d="M44 78 q0 -14 14 -14 h84 q14 0 14 14 v126 q0 18 -18 18 H62 q-18 0 -18 -18 Z"/>
<!-- honey fill, clipped to the glass -->
<clipPath id="jarClip">
<path d="M44 78 q0 -14 14 -14 h84 q14 0 14 14 v126 q0 18 -18 18 H62 q-18 0 -18 -18 Z"/>
</clipPath>
<g clip-path="url(#jarClip)">
<rect class="jar__honey" x="40" y="96" width="120" height="150"/>
<path class="jar__meniscus" d="M40 100 q40 -10 60 -2 q30 5 60 -4 v160 H40 Z"/>
</g>
<!-- lid + collar -->
<rect class="jar__lid" x="56" y="40" width="88" height="22" rx="5"/>
<rect class="jar__collar" x="62" y="58" width="76" height="10" rx="3"/>
<!-- paper label -->
<rect class="jar__label" x="64" y="120" width="72" height="64" rx="3"/>
<line class="jar__rule" x1="74" y1="140" x2="126" y2="140"/>
<line class="jar__rule jar__rule--short" x1="74" y1="152" x2="112" y2="152"/>
<line class="jar__rule jar__rule--short" x1="74" y1="164" x2="118" y2="164"/>
<!-- wooden dipper leaning in the jar -->
<g class="jar__dipper">
<line class="jar__dipper-stem" x1="150" y1="30" x2="120" y2="150"/>
<ellipse class="jar__dipper-head" cx="118" cy="156" rx="9" ry="13"/>
</g>
</svg>
</figure>
</section>
<!-- ── The ridge · provenance prose (Long-Document beat) ──── -->
<section class="ridge" id="ridge" aria-labelledby="ridge-title">
<div class="ridge__head">
<h2 class="section__title" id="ridge-title">
<svg class="sprig" viewBox="0 0 40 80" aria-hidden="true">
<path class="sprig__stem" d="M20 76 Q18 56 21 36 Q22 22 20 8"/>
<g transform="translate(8 38) rotate(-25)">
<ellipse class="sprig__leaf" cx="0" cy="0" rx="6" ry="11"/>
<path class="sprig__vein" d="M0 -10 Q1 0 0 10"/>
</g>
<g transform="translate(28 52) rotate(30)">
<ellipse class="sprig__leaf" cx="0" cy="0" rx="6" ry="11"/>
<path class="sprig__vein" d="M0 -10 Q-1 0 0 10"/>
</g>
<path class="sprig__stem" d="M20 22 Q16 19 13 22"/>
</svg>
A ridge, and what grows on it.
</h2>
<p class="ridge__standfirst">
Everything in a jar of Hollowback honey was within a bee&rsquo;s morning flight of
the same nine hives. That is the whole idea, and it is also the whole limit.
</p>
</div>
<div class="ridge__prose">
<p>
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.
</p>
<p>
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
<a href="#lot">summer lot</a> says so. We would rather sell less and have it be
honestly from here.
</p>
<p>
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 &mdash; that is honey doing what raw honey does. Warm the jar in a bowl of
tap-hot water and it comes back clear.
</p>
</div>
<dl class="ridge__facts" aria-label="The apiary, in brief">
<div class="fact">
<dt>Hives</dt>
<dd>9</dd>
<p>All on one ridge, all overwintered in place.</p>
</div>
<div class="fact">
<dt>Elevation</dt>
<dd>1,180&nbsp;ft</dd>
<p>South-facing, above the valley frost line.</p>
</div>
<div class="fact">
<dt>Forage</dt>
<dd>Wild</dd>
<p>Raspberry, clover, basswood, goldenrod, aster.</p>
</div>
<div class="fact">
<dt>Pour</dt>
<dd>By&nbsp;hand</dd>
<p>Cold-spun, strained once, jarred the same week.</p>
</div>
</dl>
</section>
<!-- ── This season's lot · Catalogue (F6 product-card grid) ── -->
<section class="lot" id="lot" aria-labelledby="lot-title">
<div class="lot__head">
<h2 class="section__title" id="lot-title">This season&rsquo;s lot</h2>
<p class="lot__intro">
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.
</p>
</div>
<ol class="lot__grid" role="list">
<li class="card reveal" style="--i:0">
<figure class="card__specimen" aria-hidden="true">
<svg class="vial" viewBox="0 0 120 150" preserveAspectRatio="xMidYMax meet" aria-hidden="true" focusable="false">
<clipPath id="vialA"><path d="M40 28 h40 v18 l16 78 a8 8 0 0 1 -8 9 H32 a8 8 0 0 1 -8 -9 l16 -78 Z"/></clipPath>
<g clip-path="url(#vialA)"><rect class="vial__fill vial__fill--spring" x="20" y="58" width="80" height="80"/></g>
<path class="vial__glass" d="M40 28 h40 v18 l16 78 a8 8 0 0 1 -8 9 H32 a8 8 0 0 1 -8 -9 l16 -78 Z"/>
<rect class="vial__cork" x="42" y="18" width="36" height="14" rx="3"/>
</svg>
</figure>
<div class="card__body">
<p class="card__tag">Lot 14-A &middot; late May</p>
<h3 class="card__name">Raspberry &amp; Clover</h3>
<p class="card__notes">
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.
</p>
<div class="card__foot">
<p class="card__price"><span class="card__amount">$16</span> <span class="card__unit">/ 12&nbsp;oz</span></p>
<a class="card__link" href="#standing">Reserve<span aria-hidden="true"> &rarr;</span></a>
</div>
<p class="card__stock" data-stock="low">14 jars left</p>
</div>
</li>
<li class="card reveal" style="--i:1">
<figure class="card__specimen" aria-hidden="true">
<svg class="vial" viewBox="0 0 120 150" preserveAspectRatio="xMidYMax meet" aria-hidden="true" focusable="false">
<clipPath id="vialB"><path d="M40 28 h40 v18 l16 78 a8 8 0 0 1 -8 9 H32 a8 8 0 0 1 -8 -9 l16 -78 Z"/></clipPath>
<g clip-path="url(#vialB)"><rect class="vial__fill vial__fill--basswood" x="20" y="48" width="80" height="90"/></g>
<path class="vial__glass" d="M40 28 h40 v18 l16 78 a8 8 0 0 1 -8 9 H32 a8 8 0 0 1 -8 -9 l16 -78 Z"/>
<rect class="vial__cork" x="42" y="18" width="36" height="14" rx="3"/>
</svg>
</figure>
<div class="card__body">
<p class="card__tag">Lot 14-B &middot; early July</p>
<h3 class="card__name">Basswood</h3>
<p class="card__notes">
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.
</p>
<div class="card__foot">
<p class="card__price"><span class="card__amount">$19</span> <span class="card__unit">/ 12&nbsp;oz</span></p>
<a class="card__link" href="#standing">Reserve<span aria-hidden="true"> &rarr;</span></a>
</div>
<p class="card__stock" data-stock="out">Sold out for 2026</p>
</div>
</li>
<li class="card reveal" style="--i:2">
<figure class="card__specimen" aria-hidden="true">
<svg class="vial" viewBox="0 0 120 150" preserveAspectRatio="xMidYMax meet" aria-hidden="true" focusable="false">
<clipPath id="vialC"><path d="M40 28 h40 v18 l16 78 a8 8 0 0 1 -8 9 H32 a8 8 0 0 1 -8 -9 l16 -78 Z"/></clipPath>
<g clip-path="url(#vialC)"><rect class="vial__fill vial__fill--summer" x="20" y="52" width="80" height="86"/></g>
<path class="vial__glass" d="M40 28 h40 v18 l16 78 a8 8 0 0 1 -8 9 H32 a8 8 0 0 1 -8 -9 l16 -78 Z"/>
<rect class="vial__cork" x="42" y="18" width="36" height="14" rx="3"/>
</svg>
</figure>
<div class="card__body">
<p class="card__tag">Lot 14-C &middot; high summer</p>
<h3 class="card__name">Ridge Summer</h3>
<p class="card__notes">
Everything blooming at once &mdash; the household jar. Round and even, equal
parts clover and wildflower. The one we put on toast and never label fussily.
</p>
<div class="card__foot">
<p class="card__price"><span class="card__amount">$15</span> <span class="card__unit">/ 12&nbsp;oz</span></p>
<a class="card__link" href="#standing">Reserve<span aria-hidden="true"> &rarr;</span></a>
</div>
<p class="card__stock" data-stock="in">In the pantry now</p>
</div>
</li>
<li class="card reveal" style="--i:3">
<figure class="card__specimen" aria-hidden="true">
<svg class="vial" viewBox="0 0 120 150" preserveAspectRatio="xMidYMax meet" aria-hidden="true" focusable="false">
<clipPath id="vialD"><path d="M40 28 h40 v18 l16 78 a8 8 0 0 1 -8 9 H32 a8 8 0 0 1 -8 -9 l16 -78 Z"/></clipPath>
<g clip-path="url(#vialD)"><rect class="vial__fill vial__fill--autumn" x="20" y="46" width="80" height="92"/></g>
<path class="vial__glass" d="M40 28 h40 v18 l16 78 a8 8 0 0 1 -8 9 H32 a8 8 0 0 1 -8 -9 l16 -78 Z"/>
<rect class="vial__cork" x="42" y="18" width="36" height="14" rx="3"/>
</svg>
</figure>
<div class="card__body">
<p class="card__tag">Lot 14-D &middot; before frost</p>
<h3 class="card__name">Goldenrod &amp; Aster</h3>
<p class="card__notes">
The last and darkest pour, brought in cool. Malty, almost smoky, with the
bite the goldenrod leaves behind. For cheese, dark bread, strong tea.
</p>
<div class="card__foot">
<p class="card__price"><span class="card__amount">$17</span> <span class="card__unit">/ 12&nbsp;oz</span></p>
<a class="card__link" href="#standing">Reserve<span aria-hidden="true"> &rarr;</span></a>
</div>
<p class="card__stock" data-stock="in">In the pantry now</p>
</div>
</li>
</ol>
<p class="lot__foot">
Every jar carries its lot letter and pour date. Shipped in straw, padded by hand;
flat-rate within New&nbsp;England, at cost beyond it.
</p>
</section>
<!-- ── How it's poured · short process (stacked, no eyebrows) ─ -->
<section class="poured" id="poured" aria-labelledby="poured-title">
<div class="poured__head">
<h2 class="section__title" id="poured-title">How a jar is filled</h2>
<p class="poured__intro">
Four steps, and not one of them is in a hurry. The whole point of small-batch is
that no part of it scales.
</p>
</div>
<div class="poured__steps">
<article class="step">
<h3 class="step__name">Spun cold</h3>
<p>
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.
</p>
</article>
<article class="step">
<h3 class="step__name">Strained once</h3>
<p>
One pass through a square of cloth takes out the wax and a stray bit of comb.
That is all. We don&rsquo;t fine it bright or push it through paper &mdash;
the pollen and the cloud are the honey, not a flaw in it.
</p>
</article>
<article class="step">
<h3 class="step__name">Settled a week</h3>
<p>
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.
</p>
</article>
<article class="step">
<h3 class="step__name">Poured by hand</h3>
<p>
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 &mdash; and
then that pour is closed.
</p>
</article>
</div>
</section>
<!-- ── The standing jar · CTA (C2 inline form-as-CTA) ──────── -->
<section class="standing" id="standing" aria-labelledby="standing-title">
<div class="standing__inner">
<div class="standing__copy">
<h2 class="standing__title" id="standing-title">Keep a jar standing.</h2>
<p class="standing__lede">
A standing jar is the simplest way we know to sell honey: tell us your address
once, and one jar of whatever we&rsquo;re pouring arrives at the turn of each
season &mdash; four jars a year, billed the week each ships. Pause or stop with
one reply. No tiers, no app, no club.
</p>
</div>
<form class="standing__form" id="standing-form" novalidate>
<div class="field">
<label class="field__label" for="email">Email for the harvest note</label>
<div class="field__row">
<input
class="field__input"
type="email"
id="email"
name="email"
inputmode="email"
autocomplete="email"
placeholder="you@example.com"
aria-describedby="email-help"
required>
<button class="btn btn--solid" type="submit">Start a standing jar</button>
</div>
<p class="field__help" id="email-help" data-state="idle" role="status" aria-live="polite">
We write once a season, the week a pour ships. Nothing else.
</p>
</div>
</form>
</div>
</section>
</main>
<!-- ── Ft6 · Letter close ─────────────────────────────────────── -->
<footer class="colophon" role="contentinfo">
<div class="colophon__inner">
<p class="colophon__close">
That&rsquo;s the whole farm &mdash; nine hives, one ridge, and however much the
summer felt like giving. Thank you for buying honey that has to be honest about
the weather.
</p>
<p class="colophon__sign">Yours, from the ridge,<br><span class="colophon__name">Tess &amp; Roland Marsh</span></p>
<p class="colophon__ps">
P.S. &mdash; Beekeepers and the curious are welcome up in the off weeks. Write first;
we&rsquo;ll put the kettle on.
</p>
<div class="colophon__meta">
<span class="colophon__wordmark">Hollowback Apiary</span>
<span>Hollowback Ridge, Johnson, Vermont</span>
<span>Season 2026 &middot; Lot No.&nbsp;14</span>
<a href="#top">Back to top</a>
</div>
</div>
</footer>
<script src="./script.js" defer></script>
</body>
</html>
+76
View File
@@ -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);
});
})();
+859
View File
@@ -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 (4041) · honest: pass (46) · chrome: pass (47) · tokens: pass (48) · responsive: pass (49)
* icons: pass (30, none) · mobile: pass (34, 49, 5057) · slop: pass (4245) · 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; }
}
+138
View File
@@ -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 */
}
+306
View File
@@ -0,0 +1,306 @@
<!doctype html>
<html lang="en" data-theme="press">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>Bríd Halloran — experimental typographer · concrete poetry, variable fonts, broken-grid posters</title>
<meta name="description" content="Bríd Halloran sets type that won't hold still: concrete poems, variable-font specimens you can pull by hand, and posters built on a grid she breaks on purpose. A working studio in Cork." />
<!-- inline mark — a red square, the studio's only logo; avoids a favicon 404 -->
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' fill='%23151013'/%3E%3Crect x='3' y='3' width='10' height='10' fill='%23e0452f'/%3E%3C/svg%3E" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preload" as="style"
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&family=Fraunces:ital,opsz,wght@0,9..144,300..600;1,9..144,300..500&family=JetBrains+Mono:wght@400;500&display=swap" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,200..800&family=Fraunces:ital,opsz,wght@0,9..144,300..600;1,9..144,300..500&family=JetBrains+Mono:wght@400;500&display=swap" />
<link rel="stylesheet" href="./tokens.css" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<!-- N3 · Side-rail — rotated wordmark + section dot index. The vertical
type is itself a small typographic argument. -->
<nav class="rail" aria-label="Primary">
<a class="rail__mark" href="#top">
<span class="rail__name">Bríd Halloran</span>
<span class="rail__role" aria-hidden="true">typographer</span>
</a>
<ul class="rail__dots" aria-label="Sections">
<li><a href="#living" data-label="Living specimen"><span class="rail__dot"></span></a></li>
<li><a href="#weights" data-label="The family"><span class="rail__dot"></span></a></li>
<li><a href="#concrete" data-label="Concrete poetry"><span class="rail__dot"></span></a></li>
<li><a href="#poster" data-label="Broken grid"><span class="rail__dot"></span></a></li>
<li><a href="#work" data-label="Selected work"><span class="rail__dot"></span></a></li>
<li><a href="#studio" data-label="The studio"><span class="rail__dot"></span></a></li>
</ul>
<a class="rail__year" href="#studio">Cork · MMXXVI</a>
</nav>
<div class="shell" id="top">
<main id="main">
<!-- ───────── HERO · the living specimen ───────── -->
<section class="hero" id="living" aria-labelledby="hero-h">
<p class="hero__slug reveal" style="--i:0">
Press Quaternary <span aria-hidden="true"></span>a working type studio. Bríd Halloran sets letters that refuse to sit still.
</p>
<h1 class="hero__h" id="hero-h">
<span class="reveal" style="--i:1">A typeface is</span>
<span class="reveal" style="--i:2">a verb, not</span>
<span class="reveal" style="--i:3">a noun.</span>
</h1>
<!-- The colossal word IS the imagery — a live variable specimen the
reader can pull by hand. Ambient breathing on the weight/width axes;
the slider overrides it. Pure CSS + a small JS bridge. -->
<figure class="specimen-live reveal" style="--i:4" aria-labelledby="live-cap">
<div class="specimen-live__stage">
<span class="specimen-live__word" id="liveWord" aria-hidden="true">Restless</span>
</div>
<figcaption class="specimen-live__panel" id="live-cap">
<div class="axis">
<div class="axis__row">
<label class="axis__label" for="axWght">Weight<span class="axis__val" id="wghtVal">320</span></label>
<input class="axis__range" id="axWght" type="range" min="200" max="800" value="320" step="1"
aria-describedby="axis-help" />
</div>
<div class="axis__row">
<label class="axis__label" for="axWdth">Width<span class="axis__val" id="wdthVal">100</span></label>
<input class="axis__range" id="axWdth" type="range" min="75" max="100" value="100" step="1"
aria-describedby="axis-help" />
</div>
<div class="axis__row">
<label class="axis__label" for="axOpsz">Optical<span class="axis__val" id="opszVal">96</span></label>
<input class="axis__range" id="axOpsz" type="range" min="12" max="96" value="96" step="1"
aria-describedby="axis-help" />
</div>
<p class="axis__help" id="axis-help">
Pull the axes. The word reads <em>Restless</em> by design — that is the point of a variable face.
</p>
</div>
</figcaption>
</figure>
<a class="link-arrow hero__cta reveal" style="--i:5" href="#work">
<span>See the work</span>
<svg class="link-arrow__glyph" viewBox="0 0 24 12" width="32" height="16" aria-hidden="true">
<path d="M0 6h22M17 1l5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.5" />
</svg>
</a>
</section>
<!-- ───────── SPECIMEN · the family, set at sizes ───────── -->
<section class="proof" id="weights" aria-labelledby="weights-h">
<header class="plate-head">
<p class="plate-head__no" aria-hidden="true">Plate 01</p>
<h2 class="plate-head__title" id="weights-h">Quaternary Grotesque</h2>
<p class="plate-head__sub">Six masters on one weight axis, one width axis, one optical size. Drawn 20242026. Latin Extended, set here at the sizes it was made to do work at.</p>
</header>
<ol class="weights" aria-label="Weights, light to bold">
<li class="weights__row" data-w="240">
<span class="weights__tag">Thin · 240</span>
<p class="weights__line" style="font-variation-settings:'wght' 240,'opsz' 96;">Hairlines hold at billboard size.</p>
</li>
<li class="weights__row" data-w="340">
<span class="weights__tag">Book · 340</span>
<p class="weights__line" style="font-variation-settings:'wght' 340,'opsz' 64;">Reads quiet in a paragraph, loud on a wall.</p>
</li>
<li class="weights__row" data-w="480">
<span class="weights__tag">Medium · 480</span>
<p class="weights__line" style="font-variation-settings:'wght' 480,'opsz' 48;">A working middle, for captions and credits.</p>
</li>
<li class="weights__row" data-w="620">
<span class="weights__tag">Semibold · 620</span>
<p class="weights__line" style="font-variation-settings:'wght' 620,'opsz' 40;">Where a headline starts to push back.</p>
</li>
<li class="weights__row" data-w="800">
<span class="weights__tag">Black · 800</span>
<p class="weights__line" style="font-variation-settings:'wght' 800,'opsz' 24,'wdth' 92;">Set it once. Let it shout.</p>
</li>
</ol>
<dl class="glyphset" aria-label="Glyph and feature coverage">
<div class="glyphset__cell">
<dt>Character set</dt>
<dd>Latin Extended-A · 642 glyphs</dd>
</div>
<div class="glyphset__cell">
<dt>Figures</dt>
<dd>Lining, oldstyle, tabular, fractions</dd>
</div>
<div class="glyphset__cell">
<dt>Axes</dt>
<dd>Weight 200800 · Width 75100 · Optical 1296</dd>
</div>
<div class="glyphset__cell">
<dt>Format</dt>
<dd>Variable OTF · WOFF2 · one file</dd>
</div>
</dl>
</section>
<!-- ───────── CONCRETE POETRY · a calligramme, set by hand ───────── -->
<section class="concrete" id="concrete" aria-labelledby="concrete-h">
<header class="plate-head plate-head--ink">
<p class="plate-head__no" aria-hidden="true">Plate 02</p>
<h2 class="plate-head__title" id="concrete-h">Concrete poetry</h2>
<p class="plate-head__sub">The shape carries the meaning. Below: <em>“Rain on the Lee,”</em> set so the lines fall the way the rain does — typography as the picture, not its caption.</p>
</header>
<!-- Calligramme: the poem arranged into falling rain using pure CSS
transforms. Decorative as art, but fully readable as text. -->
<figure class="poem" aria-labelledby="poem-cap">
<p class="poem__visually-hidden">
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.
</p>
<div class="poem__field" aria-hidden="true">
<span class="poem__w" style="--c:1;--r:1;--rot:-4deg;--w:300">slow</span>
<span class="poem__w" style="--c:4;--r:1;--rot:-7deg;--w:320">at</span>
<span class="poem__w" style="--c:7;--r:1;--rot:-5deg;--w:340">first</span>
<span class="poem__w" style="--c:2;--r:2;--rot:-9deg;--w:360">the</span>
<span class="poem__w" style="--c:5;--r:2;--rot:-8deg;--w:380">grey</span>
<span class="poem__w" style="--c:8;--r:2;--rot:-6deg;--w:360">comes</span>
<span class="poem__w" style="--c:1;--r:3;--rot:-11deg;--w:420">down</span>
<span class="poem__w" style="--c:4;--r:3;--rot:-10deg;--w:440">the</span>
<span class="poem__w" style="--c:7;--r:3;--rot:-9deg;--w:420">quays</span>
<span class="poem__w" style="--c:3;--r:4;--rot:-13deg;--w:520">harder</span>
<span class="poem__w" style="--c:6;--r:4;--rot:-12deg;--w:540">now</span>
<span class="poem__w" style="--c:2;--r:5;--rot:-15deg;--w:640">faster</span>
<span class="poem__w" style="--c:6;--r:5;--rot:-14deg;--w:660">heavier</span>
<span class="poem__w poem__w--pool" style="--c:4;--r:6;--rot:0deg;--w:800">river</span>
</div>
<figcaption class="poem__cap" id="poem-cap">
<span class="poem__title">Rain on the Lee</span>
<span class="poem__meta">Calligramme · Quaternary Grotesque · set by hand, 2025</span>
</figcaption>
</figure>
</section>
<!-- ───────── BROKEN-GRID POSTER · the plate she breaks on purpose ───────── -->
<section class="proof" id="poster" aria-labelledby="poster-h">
<header class="plate-head">
<p class="plate-head__no" aria-hidden="true">Plate 03</p>
<h2 class="plate-head__title" id="poster-h">Broken-grid posters</h2>
<p class="plate-head__sub">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.</p>
</header>
<figure class="poster" aria-labelledby="poster-cap">
<div class="poster__plate">
<span class="poster__baseline" aria-hidden="true"></span>
<span class="poster__kicker">Lent Series · Triskel</span>
<h3 class="poster__title" aria-hidden="true">
<span class="poster__word poster__word--a">OFF</span>
<span class="poster__word poster__word--b">THE</span>
<span class="poster__word poster__word--c">GRID</span>
</h3>
<span class="poster__vis-hidden">Poster reading: Off the Grid — a night of new typography, Triskel Arts Centre.</span>
<p class="poster__line poster__line--date">Fri 14 March · 8pm</p>
<p class="poster__line poster__line--place">Triskel Arts Centre, Tobin Street</p>
<span class="poster__bleed" aria-hidden="true">14</span>
</div>
<figcaption class="poster__cap" id="poster-cap">
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.
</figcaption>
</figure>
</section>
<!-- ───────── SELECTED WORK · tabular index (F3) ───────── -->
<section class="work" id="work" aria-labelledby="work-h">
<header class="plate-head plate-head--ink">
<p class="plate-head__no" aria-hidden="true">Index</p>
<h2 class="plate-head__title" id="work-h">Selected work</h2>
<p class="plate-head__sub">Eleven years, set in a ledger. Type design, identities, poster series, and the occasional book — each with whoever commissioned it.</p>
</header>
<ol class="ledger" aria-label="Selected work, newest first">
<li class="ledger__row">
<span class="ledger__year">2026</span>
<span class="ledger__name">Quaternary Grotesque</span>
<span class="ledger__kind">Variable typeface · self-released</span>
</li>
<li class="ledger__row">
<span class="ledger__year">2025</span>
<span class="ledger__name">Rain on the Lee</span>
<span class="ledger__kind">Concrete poem · Cork Midsummer</span>
</li>
<li class="ledger__row">
<span class="ledger__year">2025</span>
<span class="ledger__name">Off the Grid · poster series</span>
<span class="ledger__kind">Identity &amp; print · Triskel Arts Centre</span>
</li>
<li class="ledger__row">
<span class="ledger__year">2024</span>
<span class="ledger__name">Strand &amp; Marsh</span>
<span class="ledger__kind">Bespoke wordmark · a saltmarsh distillery</span>
</li>
<li class="ledger__row">
<span class="ledger__year">2023</span>
<span class="ledger__name">The Compositors Almanac</span>
<span class="ledger__kind">204-page book · designed &amp; typeset</span>
</li>
<li class="ledger__row">
<span class="ledger__year">2022</span>
<span class="ledger__name">Marsh Mono</span>
<span class="ledger__kind">Monospace family · for a print zine</span>
</li>
</ol>
</section>
<!-- ───────── THE STUDIO · the close + contact ───────── -->
<section class="studio" id="studio" aria-labelledby="studio-h">
<h2 class="studio__h" id="studio-h">Set by hand, in Cork.</h2>
<div class="studio__cols">
<p class="studio__lede">
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.
</p>
<!-- Inline form-as-CTA (C2). Eight input states; helper slot reserved. -->
<form class="enquire" id="enquire" novalidate>
<label class="enquire__label" for="enq-email">Start a conversation</label>
<div class="enquire__row">
<input class="enquire__input" id="enq-email" name="email" type="email"
inputmode="email" autocomplete="email" placeholder="you@studio.com"
aria-describedby="enq-help" required />
<button class="btn enquire__btn" type="submit">
<span class="btn__label">Send a note</span>
</button>
</div>
<p class="enquire__help" id="enq-help" aria-live="polite">A real reply, from me, within a few days.</p>
</form>
</div>
</section>
</main>
<!-- ───────── Ft1 · Mast-headed footer ───────── -->
<footer class="foot" role="contentinfo">
<p class="foot__mark">Press Quaternary</p>
<p class="foot__tag">The studio of Bríd Halloran — type, poems, and posters, set against the grid.</p>
<p class="foot__set">
Set in Bricolage Grotesque &amp; Fraunces, with JetBrains Mono for the margins.
Proofed in Cork. <span class="foot__sep" aria-hidden="true">·</span> © 2026 Bríd Halloran.
</p>
<p class="foot__links">
<a href="#living">Top</a>
<span class="foot__sep" aria-hidden="true">·</span>
<a href="mailto:studio@pressquaternary.example">studio@pressquaternary.example</a>
<span class="foot__sep" aria-hidden="true">·</span>
<a href="#work">Specimen PDF</a>
</p>
</footer>
</div>
<script src="./script.js" defer></script>
</body>
</html>
+139
View File
@@ -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. Ill reply from studio@pressquaternary within a few days.", "success");
}, 900);
});
}
})();
+995
View File
@@ -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 (4041) · honest: pass (46) · chrome: pass (47) · tokens: pass (48)
* responsive: pass (49) · icons: pass (30) · mobile: pass (34, 49, 5057) · slop: pass (4245)
*/
/* ── 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: "<number>"; inherits: true; initial-value: 320; }
@property --wd { syntax: "<number>"; inherits: true; initial-value: 100; }
@property --op { syntax: "<number>"; 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; }
}
+121
View File
@@ -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;
}
+352
View File
@@ -0,0 +1,352 @@
<!DOCTYPE html>
<html lang="en" data-theme="riso" class="no-js">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OFF-REGISTER · A Risograph Print Fair · Pulp Yard, 1819 Oct 2026</title>
<meta name="description" content="OFF-REGISTER is a two-day risograph print fair at the Pulp Yard, Govanhill. Thirty-four studios, eight presses, soy ink everywhere. Free entry, Saturday and Sunday." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Public+Sans:wght@400;600;800;900&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400&family=Space+Mono:wght@400;700&display=swap" />
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23f0cdbf'/%3E%3Ccircle cx='15' cy='17' r='9' fill='%23f0c33a'/%3E%3Ccircle cx='13' cy='15' r='9' fill='none' stroke='%231aa3c2' stroke-width='3'/%3E%3Ccircle cx='18' cy='19' r='6' fill='none' stroke='%23e8497a' stroke-width='3'/%3E%3C/svg%3E" />
<link rel="stylesheet" href="./tokens.css" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<a class="skip-link" href="#catalogue">Skip to the exhibitor list</a>
<!-- ░░ N6 · Newspaper masthead ░░ -->
<header class="masthead" role="banner">
<p class="masthead__issue">Edition 04 · Govanhill · 1819 October 2026 · Free entry</p>
<a class="masthead__name riso-mis" href="#top" data-text="OFF-REGISTER" aria-label="Off-Register, home">OFF-REGISTER</a>
<nav class="masthead__nav" aria-label="Primary">
<ul>
<li><a href="#catalogue">Exhibitors</a></li>
<li><a href="#programme">Programme</a></li>
<li><a href="#workshops">Workshops</a></li>
<li><a href="#visit">Visit</a></li>
</ul>
</nav>
<hr class="masthead__rule" aria-hidden="true" />
</header>
<main id="top">
<!-- ░░ HERO · Catalogue inventory header + Tier-A CSS riso mark ░░ -->
<section class="hero" aria-labelledby="hero-title">
<div class="hero__copy">
<p class="kicker">A risograph print fair · Two days · One drum at a time</p>
<h1 id="hero-title" class="hero__title">
<span class="riso-mis" data-text="Thirty-four">Thirty-four</span>
studios. Eight presses. Soy&nbsp;ink everywhere.
</h1>
<p class="hero__lede">
design like print: warm, off-register, intentional. For one weekend the
Pulp Yard fills with spot colour — fluorescent pink that won&rsquo;t sit
still, teal that bleeds a hair past its edge, paper you can smell. Come
turn the drum.
</p>
<div class="hero__actions">
<a class="btn btn--solid" href="#visit">Plan your visit</a>
<a class="btn btn--text" href="#catalogue">Read the exhibitor list <span aria-hidden="true">&rarr;</span></a>
</div>
<dl class="hero__facts">
<div class="fact"><dt>Where</dt><dd>The Pulp Yard, 31 Calder St, Govanhill</dd></div>
<div class="fact"><dt>When</dt><dd>Sat &amp; Sun, 11:00&ndash;18:00</dd></div>
<div class="fact"><dt>Entry</dt><dd>Free &mdash; first 200 get a printed map</dd></div>
</dl>
</div>
<!-- Pure-CSS risograph centrepiece: three overprinted ink layers,
deliberately misregistered, multiply-blended, with paper grain. -->
<figure class="hero__art" aria-label="A risographed poster mark: overlapping cyan, yellow and pink shapes printed slightly out of register.">
<div class="riso-plate">
<span class="riso-ink riso-ink--yellow"></span>
<span class="riso-ink riso-ink--cyan"></span>
<span class="riso-ink riso-ink--pink"></span>
<span class="riso-grain" aria-hidden="true"></span>
</div>
<figcaption class="riso-plate__cap">Plate 04 &middot; 3 colours &middot; 280 gsm</figcaption>
</figure>
</section>
<!-- ░░ EXHIBITOR CATALOGUE · the core — uniform filterable grid ░░ -->
<section id="catalogue" class="catalogue" aria-labelledby="cat-title">
<div class="band">
<h2 id="cat-title" class="band__title">The exhibitor list</h2>
<p class="band__count">Thirty-four studios &middot; Hall A &amp; the Mezzanine &middot; updated 2 Oct</p>
</div>
<div class="filters" role="group" aria-label="Filter exhibitors by medium">
<button type="button" class="chip is-active" data-filter="all" aria-pressed="true">All&nbsp;<span class="chip__n">34</span></button>
<button type="button" class="chip" data-filter="zine" aria-pressed="false">Zines&nbsp;<span class="chip__n">14</span></button>
<button type="button" class="chip" data-filter="poster" aria-pressed="false">Posters&nbsp;<span class="chip__n">9</span></button>
<button type="button" class="chip" data-filter="comic" aria-pressed="false">Comics&nbsp;<span class="chip__n">7</span></button>
<button type="button" class="chip" data-filter="press" aria-pressed="false">Working presses&nbsp;<span class="chip__n">4</span></button>
</div>
<p class="filters__status" aria-live="polite" data-empty="No studios match that medium yet.">Showing all 34 studios.</p>
<ul class="grid" role="list">
<li class="card" data-medium="press">
<a class="card__media" href="#visit" aria-label="Brae &amp; Drum, working two-colour press, stand A1">
<span class="print print--1" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Working press · Stand A1</p>
<h3 class="card__name">Brae &amp; Drum</h3>
<p class="card__line">A live two-colour MZ. Pull a print, keep it wet.</p>
</div>
</li>
<li class="card" data-medium="zine">
<a class="card__media" href="#visit" aria-label="Saltmarket Editions, zines, stand A4">
<span class="print print--2" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Zines · Stand A4</p>
<h3 class="card__name">Saltmarket Editions</h3>
<p class="card__line">Perfect-bound photo zines, two spot colours, no black.</p>
</div>
</li>
<li class="card" data-medium="poster">
<a class="card__media" href="#visit" aria-label="Mono No Aware Press, posters, stand A6">
<span class="print print--3" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Posters · Stand A6</p>
<h3 class="card__name">Mono No Aware Press</h3>
<p class="card__line">A2 gig posters. Fluoro pink that glows under the strip lights.</p>
</div>
</li>
<li class="card" data-medium="comic">
<a class="card__media" href="#visit" aria-label="Tenement Comics, comics, stand A9">
<span class="print print--4" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Comics · Stand A9</p>
<h3 class="card__name">Tenement Comics</h3>
<p class="card__line">Riso-printed shorts about the close, the bin store, the No.&nbsp;6 bus.</p>
</div>
</li>
<li class="card" data-medium="zine">
<a class="card__media" href="#visit" aria-label="Verdigris &amp; Co, zines, stand A11">
<span class="print print--5" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Zines · Stand A11</p>
<h3 class="card__name">Verdigris &amp; Co</h3>
<p class="card__line">Botanical risographs on grey speckle stock. Teal on teal.</p>
</div>
</li>
<li class="card" data-medium="poster">
<a class="card__media" href="#visit" aria-label="Halftone Social Club, posters, stand A13">
<span class="print print--6" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Posters · Stand A13</p>
<h3 class="card__name">Halftone Social Club</h3>
<p class="card__line">Big dot screens, on purpose. Bring a loupe.</p>
</div>
</li>
<li class="card" data-medium="comic">
<a class="card__media" href="#visit" aria-label="Closemouth Press, comics, stand M2">
<span class="print print--7" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Comics · Mezzanine M2</p>
<h3 class="card__name">Closemouth Press</h3>
<p class="card__line">A risograph graphic novel, four years in, finally bound.</p>
</div>
</li>
<li class="card" data-medium="press">
<a class="card__media" href="#visit" aria-label="The Spirit Master, working press, stand M5">
<span class="print print--8" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Working press · Mezzanine M5</p>
<h3 class="card__name">The Spirit Master</h3>
<p class="card__line">We cut a master from your drawing while you wait. Bring a felt&nbsp;tip.</p>
</div>
</li>
<li class="card" data-medium="zine">
<a class="card__media" href="#visit" aria-label="Riso Riot, zines, stand M7">
<span class="print print--9" aria-hidden="true"></span>
</a>
<div class="card__meta">
<p class="card__tag">Zines · Mezzanine M7</p>
<h3 class="card__name">Riso Riot</h3>
<p class="card__line">A queer riso anthology, eighty pages, sold by donation.</p>
</div>
</li>
</ul>
<p class="catalogue__more">
&hellip; and twenty-five more, from <em>Govan Letterpress</em> to <em>Fluoro Bakery</em>.
The full A&ndash;Z and the floor plan go up the week before.
</p>
</section>
<!-- ░░ PROGRAMME · F3 tabular spec sheet (genuinely ordinal — a timetable) ░░ -->
<section id="programme" class="programme" aria-labelledby="prog-title">
<div class="band band--invert">
<h2 id="prog-title" class="band__title">Two days, hour by hour</h2>
<p class="band__count">Talks on the Mezzanine · printing in Hall A · all free, drop in</p>
</div>
<div class="prog__day">
<h3 class="prog__heading"><span class="riso-mis" data-text="Saturday">Saturday</span> &middot; 18 Oct</h3>
<dl class="timetable">
<div class="slot">
<dt class="slot__time"><time datetime="2026-10-18T11:00">11:00</time></dt>
<dd class="slot__body"><span class="slot__title">Doors &amp; first pull</span><span class="slot__where">Hall A</span></dd>
</div>
<div class="slot">
<dt class="slot__time"><time datetime="2026-10-18T12:30">12:30</time></dt>
<dd class="slot__body"><span class="slot__title">Why pink moves: a talk on misregistration</span><span class="slot__where">Mezzanine</span></dd>
</div>
<div class="slot">
<dt class="slot__time"><time datetime="2026-10-18T14:00">14:00</time></dt>
<dd class="slot__body"><span class="slot__title">Bind-your-own, with Closemouth Press</span><span class="slot__where">Hall A · table 3</span></dd>
</div>
<div class="slot">
<dt class="slot__time"><time datetime="2026-10-18T16:30">16:30</time></dt>
<dd class="slot__body"><span class="slot__title">Trade &amp; swap: bring a print, take a print</span><span class="slot__where">The yard</span></dd>
</div>
</dl>
</div>
<div class="prog__day">
<h3 class="prog__heading"><span class="riso-mis" data-text="Sunday">Sunday</span> &middot; 19 Oct</h3>
<dl class="timetable">
<div class="slot">
<dt class="slot__time"><time datetime="2026-10-19T11:00">11:00</time></dt>
<dd class="slot__body"><span class="slot__title">Doors &amp; slow coffee</span><span class="slot__where">The yard</span></dd>
</div>
<div class="slot">
<dt class="slot__time"><time datetime="2026-10-19T13:00">13:00</time></dt>
<dd class="slot__body"><span class="slot__title">Soy ink &amp; paper: what actually composts</span><span class="slot__where">Mezzanine</span></dd>
</div>
<div class="slot">
<dt class="slot__time"><time datetime="2026-10-19T15:00">15:00</time></dt>
<dd class="slot__body"><span class="slot__title">Kids&rsquo; print hour &mdash; one colour, two hands</span><span class="slot__where">Hall A · table 1</span></dd>
</div>
<div class="slot">
<dt class="slot__time"><time datetime="2026-10-19T17:30">17:30</time></dt>
<dd class="slot__body"><span class="slot__title">Last drum spins &amp; pack-down</span><span class="slot__where">Hall A</span></dd>
</div>
</dl>
</div>
</section>
<!-- ░░ WORKSHOPS · three honest cards, varied — not an icon-tile grid ░░ -->
<section id="workshops" class="workshops" aria-labelledby="work-title">
<div class="band">
<h2 id="work-title" class="band__title">Get your hands inky</h2>
<p class="band__count">Small groups · materials included · book at the door</p>
</div>
<ul class="work-list" role="list">
<li class="work">
<p class="work__when">Sat 13:00 &middot; 90 min &middot; &pound;18</p>
<h3 class="work__name">Make a one-colour mini-zine</h3>
<p class="work__body">
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 &mdash; we mean it.
</p>
<p class="work__cap">8 places &middot; ages 12+</p>
</li>
<li class="work">
<p class="work__when">Sat 15:30 &middot; 2 hrs &middot; &pound;26</p>
<h3 class="work__name">Two-colour overprint &amp; the art of being slightly wrong</h3>
<p class="work__body">
The whole point of riso is the misregister. We&rsquo;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.
</p>
<p class="work__cap">6 places &middot; ages 16+</p>
</li>
<li class="work">
<p class="work__when">Sun 15:00 &middot; 60 min &middot; free</p>
<h3 class="work__name">Kids&rsquo; print hour</h3>
<p class="work__body">
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.
</p>
<p class="work__cap">Drop in &middot; under 12 with an adult</p>
</li>
</ul>
</section>
<!-- ░░ VISIT · C2 inline form CTA + location detail ░░ -->
<section id="visit" class="visit" aria-labelledby="visit-title">
<div class="visit__lede">
<h2 id="visit-title" class="visit__title">Getting there is the easy part</h2>
<p class="visit__body">
Free entry, both days. No ticket needed &mdash; but leave your email and we&rsquo;ll
send the floor plan, the full A&ndash;Z of studios, and a heads-up when workshop
places open. One message, the week before. Nothing after.
</p>
<form class="signup" novalidate>
<label class="signup__label" for="email">Email address</label>
<div class="signup__row">
<input class="signup__input" type="email" id="email" name="email"
inputmode="email" autocomplete="email"
placeholder="you@studio.co" required
aria-describedby="signup-help" />
<button class="btn btn--solid signup__submit" type="submit">Send me the map</button>
</div>
<p class="signup__help" id="signup-help">We print the map on the same presses. It&rsquo;s pink.</p>
</form>
</div>
<dl class="visit__info">
<div class="info">
<dt>Address</dt>
<dd>The Pulp Yard<br />31 Calder Street<br />Govanhill, Glasgow G42 7RA</dd>
</div>
<div class="info">
<dt>Getting in</dt>
<dd>Level access throughout. The Mezzanine has a lift. Guide dogs welcome; the presses are loud.</dd>
</div>
<div class="info">
<dt>Travel</dt>
<dd>4&nbsp;min from Queen&rsquo;s Park station. Bus 6, 38, 75. Bike racks in the yard.</dd>
</div>
<div class="info">
<dt>Ask us</dt>
<dd><a href="mailto:hello@offregister.print">hello@offregister.print</a></dd>
</div>
</dl>
</section>
</main>
<!-- ░░ Ft4 · Dense colophon ░░ -->
<footer class="colophon" role="contentinfo">
<p class="colophon__body">
<span class="colophon__mark riso-mis" data-text="OFF-REGISTER">OFF-REGISTER</span>
&mdash; a risograph print fair, run by volunteers from the Govanhill print room since 2022.
Edition 04 &middot; 18&ndash;19 October 2026 &middot; The Pulp Yard, 31 Calder Street, Glasgow G42 7RA.
Set in Public&nbsp;Sans, Newsreader and Space&nbsp;Mono. Every mark on this page is hand-built in CSS &mdash;
no photographs, printed on screen at 72&nbsp;dpi. Soy ink, recycled stock, no black plate.
Programme subject to which drums survive the weekend. Free entry, all welcome.
&copy; 2026 the fair &amp; its makers.
</p>
</footer>
<script src="./script.js" defer></script>
</body>
</html>
+128
View File
@@ -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 doesnt 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 = "Youre on the list. Watch for a pink map the week before.";
help.removeAttribute("data-tone");
}
});
}
})();
+813
View File
@@ -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 (4041) · nav: N6 · footer: Ft4 · slop: pass (4245)
* honest: pass (46) · chrome: pass (47) · tokens: pass (48) · responsive: pass (49) · icons: pass (30)
* mobile: pass (34, 49, 5057) · 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 23 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; }
}
+110
View File
@@ -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;
}
+37 -23
View File
@@ -56,10 +56,10 @@
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Albert+Sans:wght@400;500;600;700;800&family=Anton&family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;0,9..144,900;1,9..144,400;1,9..144,500&family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&family=Hanken+Grotesk:wght@400;500;600;700&family=Inter:wght@400;500;600;700&family=Inter+Tight:ital,wght@0,500;0,600;0,700;0,800;1,500;1,600;1,700&family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&family=Playfair+Display:ital,wght@1,400;1,500;1,600&family=Public+Sans:wght@400;500;600;700&family=Young+Serif&family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" />
<link rel="stylesheet" href="css/tokens.css?v=22" />
<link rel="stylesheet" href="css/base.css?v=22" />
<link rel="stylesheet" href="css/components.css?v=22" />
<link rel="stylesheet" href="css/sections.css?v=22" />
<link rel="stylesheet" href="css/tokens.css?v=23" />
<link rel="stylesheet" href="css/base.css?v=23" />
<link rel="stylesheet" href="css/components.css?v=23" />
<link rel="stylesheet" href="css/sections.css?v=23" />
<script>
// Load theme before paint to avoid flash. Order: ?theme= → localStorage → default.
@@ -242,28 +242,28 @@
<video data-hover-play src="_tests/_thumbs/<slug>-scroll.mp4" poster="..."> once rendered. -->
<a class="ex-card" style="--scroll-dur:2.5s" role="listitem" href="examples/cobalt-01/" target="_blank" rel="noopener" aria-label="Open the Distil example — content-extraction API, Cobalt — in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/cobalt-01.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/cobalt-01.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a content-extraction API, Cobalt</p>
</a>
<a class="ex-card" style="--scroll-dur:2.7s" role="listitem" href="examples/carnival-01/" target="_blank" rel="noopener" aria-label="Open the Cold Snap example — record-label EP page, Carnival — in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/carnival-01.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/carnival-01.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a record-label EP page, Carnival</p>
</a>
<a class="ex-card" style="--scroll-dur:3.1s" role="listitem" href="examples/lumen-01/" target="_blank" rel="noopener" aria-label="Open the Cinder example — AI reasoning tool, Lumen — in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/lumen-01.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/lumen-01.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build an AI reasoning tool, Lumen</p>
</a>
<a class="ex-card" style="--scroll-dur:3.3s" role="listitem" href="examples/hum-07/" target="_blank" rel="noopener" aria-label="Open the Bubble example, a guided sourdough app, Hum, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/hum-07.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/hum-07.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a guided sourdough app, Hum</p>
</a>
@@ -273,56 +273,70 @@
once rendered, swap the <img> for <video data-hover-play src="..." poster="...">. -->
<a class="ex-card" style="--scroll-dur:4s" role="listitem" href="examples/custom-04/" target="_blank" rel="noopener" aria-label="Open the Mend Assembly example, a repair-café manifesto poster, custom, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/custom-04.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/custom-04.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a repair-café manifesto poster, custom</p>
</a>
<a class="ex-card" style="--scroll-dur:3.6s" role="listitem" href="examples/garden-01/" target="_blank" rel="noopener" aria-label="Open the Hollowback Apiary example, a small-batch honey farm, Garden, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/garden-01.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a landing page for a small-batch honey farm</p>
</a>
<a class="ex-card" style="--scroll-dur:4.6s" role="listitem" href="examples/riso-01/" target="_blank" rel="noopener" aria-label="Open the Off-Register example, an indie risograph print fair, Riso, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/riso-01.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a page for an indie risograph print fair</p>
</a>
<a class="ex-card" style="--scroll-dur:4.7s" role="listitem" href="examples/press-01/" target="_blank" rel="noopener" aria-label="Open the Press Quaternary example, an experimental type studio, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/press-01.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a portfolio for an experimental typographer</p>
</a>
<a class="ex-card" style="--scroll-dur:3.6s" role="listitem" href="examples/tally/" target="_blank" rel="noopener" aria-label="Open the Tally example, a SaaS product page, modern-minimal, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/tally.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/tally.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a SaaS product page, modern-minimal</p>
</a>
<a class="ex-card" style="--scroll-dur:3s" role="listitem" href="examples/wayfare/" target="_blank" rel="noopener" aria-label="Open the Wayfare example, a travel booking site, atmospheric, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/wayfare.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/wayfare.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a travel booking site, atmospheric</p>
</a>
<a class="ex-card" style="--scroll-dur:3.2s" role="listitem" href="examples/bananastudio/" target="_blank" rel="noopener" aria-label="Open the BananaStudio example, a creative studio with playful motion, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/bananastudio.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/bananastudio.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a creative studio with playful motion</p>
</a>
<a class="ex-card" style="--scroll-dur:1.8s" role="listitem" href="_tests/06-anya-portfolio/" target="_blank" rel="noopener" aria-label="Open the Anya Reis example, a software architect personal site, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/anya.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/anya.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a software architect personal site</p>
</a>
<a class="ex-card" style="--scroll-dur:2s" role="listitem" href="examples/najm/" target="_blank" rel="noopener" aria-label="Open the NAJM example, a Moroccan fashion brand landing page, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/najm.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/najm.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a Moroccan fashion brand landing page</p>
</a>
<a class="ex-card" style="--scroll-dur:1.7s" role="listitem" href="_tests/11-soroe-ceramics/" target="_blank" rel="noopener" aria-label="Open the Soroe example, a small ceramics studio site, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/soroe.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a small ceramics studio site</p>
</a>
<a class="ex-card" style="--scroll-dur:3.5s" role="listitem" href="examples/hyperlane/" target="_blank" rel="noopener" aria-label="Open the Hyperlane example, a developer infrastructure landing page, in a new tab">
<div class="ex-card__thumb" aria-hidden="true">
<img src="_tests/_thumbs/hyperlane.jpg?v=22" alt="" loading="lazy" width="1600" height="1000" />
<img src="_tests/_thumbs/hyperlane.jpg?v=23" alt="" loading="lazy" width="1600" height="1000" />
</div>
<p class="ex-card__prompt"><span class="ex-card__prompt-mark">$</span> /hallmark build a developer infrastructure landing page</p>
</a>
@@ -1113,7 +1127,7 @@
</div>
</template>
<script type="module" src="js/main.js?v=22"></script>
<script type="module" src="js/main.js?v=23"></script>
</body>
</html>
@@ -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 `<dialog>` element. It handles focus trap, escape to close, and `::backdrop` styling for free.
- **Centre it explicitly.** A `<dialog>` 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-`<dialog>` 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.
@@ -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
@@ -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 (812 rem), tabular figures, small qualifier line below.
- **Heading:** a large numeric display (812 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):
+1 -1
View File
@@ -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
+2
View File
@@ -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"?
+28 -11
View File
@@ -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); }