2026-08-05 04:18:37 +02:00
|
|
|
/*
|
|
|
|
|
catcrafts.net — stylesheet
|
|
|
|
|
|
|
|
|
|
Plain CSS, one file, no preprocessor. One file rather than several because the
|
|
|
|
|
DOM partition cannot add <link> elements (only the SSR'd head can), so extra
|
|
|
|
|
files would mean either extra head plumbing or a blocking @import — and a
|
|
|
|
|
single file compresses better anyway.
|
|
|
|
|
|
|
|
|
|
@layer is doing real work here: it makes the cascade predictable without
|
|
|
|
|
specificity games, so a components rule always beats a base rule regardless of
|
|
|
|
|
selector weight. That is the preprocessor-free way to keep a growing stylesheet
|
|
|
|
|
maintainable.
|
|
|
|
|
|
|
|
|
|
Design direction: this is an open-source systems shop, so the visual language
|
|
|
|
|
is terminal-adjacent — near-black surfaces, hairline borders, one accent, a
|
|
|
|
|
monospace face for structural text. Deliberately not the purple-gradient
|
|
|
|
|
treatment it replaces, which read as the wrong category for the work.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
@layer reset, tokens, base, layout, components, utilities, overrides;
|
|
|
|
|
|
|
|
|
|
/* ─── reset ─────────────────────────────────────────────────────────── */
|
|
|
|
|
@layer reset {
|
|
|
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
|
|
|
* { margin: 0; padding: 0; }
|
|
|
|
|
|
|
|
|
|
img, picture, svg, video, canvas {
|
|
|
|
|
display: block;
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
button, input, select, textarea { font: inherit; color: inherit; }
|
|
|
|
|
ul, ol { list-style: none; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ─── tokens ────────────────────────────────────────────────────────── */
|
|
|
|
|
@layer tokens {
|
|
|
|
|
:root {
|
|
|
|
|
color-scheme: dark;
|
|
|
|
|
|
|
|
|
|
/* Semantic surface names, not "dark-secondary" — so a light theme later
|
|
|
|
|
is a token swap rather than a rename across the file. */
|
|
|
|
|
--surface-0: #0b0d10;
|
|
|
|
|
--surface-1: #12151a;
|
|
|
|
|
--surface-2: #181c23;
|
|
|
|
|
--surface-3: #1f242c;
|
|
|
|
|
|
|
|
|
|
--border: color-mix(in oklab, white 9%, transparent);
|
|
|
|
|
--border-strong: color-mix(in oklab, white 18%, transparent);
|
|
|
|
|
|
|
|
|
|
--text: #e6e8ec;
|
|
|
|
|
--text-muted: #98a1ae;
|
|
|
|
|
--text-faint: #6b7482;
|
|
|
|
|
--text-on-accent: #06202b;
|
|
|
|
|
|
|
|
|
|
--accent: #4cc2ff;
|
|
|
|
|
--accent-hover: #7ad4ff;
|
|
|
|
|
--accent-dim: color-mix(in oklab, #4cc2ff 18%, transparent);
|
|
|
|
|
|
|
|
|
|
--ok: #48d597;
|
|
|
|
|
--warn: #e8b339;
|
|
|
|
|
--danger: #ff6b6b;
|
|
|
|
|
|
|
|
|
|
/* Fluid type scale, 1.2 ratio. clamp() means no font-size media queries. */
|
|
|
|
|
--step--1: clamp(0.84rem, 0.81rem + 0.15vw, 0.92rem);
|
|
|
|
|
--step-0: clamp(1rem, 0.96rem + 0.2vw, 1.08rem);
|
|
|
|
|
--step-1: clamp(1.2rem, 1.12rem + 0.4vw, 1.4rem);
|
|
|
|
|
--step-2: clamp(1.44rem, 1.3rem + 0.7vw, 1.8rem);
|
|
|
|
|
--step-3: clamp(1.73rem, 1.5rem + 1.15vw, 2.4rem);
|
|
|
|
|
--step-4: clamp(2.07rem, 1.7rem + 1.9vw, 3.2rem);
|
|
|
|
|
|
|
|
|
|
/* Spacing scale — replaces the ad-hoc rem values this file used to have
|
|
|
|
|
scattered through it. */
|
|
|
|
|
--s-3: 0.25rem; --s-2: 0.5rem; --s-1: 0.75rem; --s0: 1rem;
|
|
|
|
|
--s1: 1.5rem; --s2: 2rem; --s3: 3rem; --s4: 4.5rem;
|
|
|
|
|
|
|
|
|
|
--radius: 10px;
|
|
|
|
|
--radius-sm: 6px;
|
|
|
|
|
--radius-pill: 999px;
|
|
|
|
|
|
|
|
|
|
--measure: 68ch;
|
|
|
|
|
--container: 68rem;
|
|
|
|
|
|
|
|
|
|
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
|
|
|
|
"Helvetica Neue", Arial, sans-serif;
|
|
|
|
|
--font-mono: ui-monospace, "JetBrains Mono", "Cascadia Code", Menlo,
|
|
|
|
|
Consolas, monospace;
|
|
|
|
|
|
|
|
|
|
--dur-fast: 110ms;
|
|
|
|
|
--dur: 200ms;
|
|
|
|
|
--ease: cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
:root { --dur-fast: 1ms; --dur: 1ms; }
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* ─── base ──────────────────────────────────────────────────────────── */
|
|
|
|
|
@layer base {
|
|
|
|
|
html { -webkit-text-size-adjust: 100%; }
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
body {
|
|
|
|
|
font-family: var(--font-sans);
|
|
|
|
|
font-size: var(--step-0);
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
background: var(--surface-0);
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
/* A single very soft radial instead of the old three-stop gradient: it
|
|
|
|
|
reads as depth without competing with the content. */
|
|
|
|
|
background-image: radial-gradient(
|
|
|
|
|
ellipse 80% 50% at 50% -10%,
|
|
|
|
|
color-mix(in oklab, var(--accent) 7%, transparent),
|
|
|
|
|
transparent 70%);
|
|
|
|
|
background-repeat: no-repeat;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
h1, h2, h3 { line-height: 1.2; text-wrap: balance; }
|
|
|
|
|
p { text-wrap: pretty; }
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
a { color: var(--accent); text-decoration: none; }
|
|
|
|
|
a:hover { color: var(--accent-hover); text-decoration: underline; }
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
code, kbd, samp, pre { font-family: var(--font-mono); font-size: 0.92em; }
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
code {
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
padding: 0.1em 0.35em;
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
pre {
|
|
|
|
|
background: var(--surface-0);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
padding: var(--s0);
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* :focus-visible rather than :focus so a mouse click leaves no ring, and
|
|
|
|
|
:where() keeps specificity at zero so components can override without
|
|
|
|
|
!important. There was no focus style at all before this. */
|
|
|
|
|
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
|
|
|
|
|
outline: 2px solid var(--accent);
|
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
::selection { background: var(--accent-dim); }
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
|
|
|
::-webkit-scrollbar-track { background: var(--surface-1); }
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
|
|
background: var(--surface-3);
|
|
|
|
|
border-radius: var(--radius-pill);
|
|
|
|
|
border: 2px solid var(--surface-1);
|
|
|
|
|
}
|
|
|
|
|
::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* ─── layout ────────────────────────────────────────────────────────── */
|
|
|
|
|
@layer layout {
|
|
|
|
|
#catcrafts-root {
|
|
|
|
|
display: contents;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
#cc-header {
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: 0;
|
|
|
|
|
z-index: 100;
|
|
|
|
|
background: color-mix(in oklab, var(--surface-1) 88%, transparent);
|
|
|
|
|
backdrop-filter: blur(12px);
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.nav-container {
|
|
|
|
|
max-width: var(--container);
|
|
|
|
|
margin-inline: auto;
|
|
|
|
|
padding: var(--s-1) var(--s1);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: var(--s1);
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
main {
|
|
|
|
|
flex: 1;
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-width: var(--container);
|
|
|
|
|
margin-inline: auto;
|
|
|
|
|
padding: var(--s3) var(--s1) var(--s4);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.section { margin-top: var(--s3); }
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
#cc-footer {
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
background: var(--surface-1);
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.footer-content {
|
|
|
|
|
max-width: var(--container);
|
|
|
|
|
margin-inline: auto;
|
|
|
|
|
padding: var(--s2) var(--s1);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--s-1);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* ─── components ────────────────────────────────────────────────────── */
|
|
|
|
|
@layer components {
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* nav */
|
|
|
|
|
.logo {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--s-2);
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step-1);
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text);
|
|
|
|
|
letter-spacing: -0.01em;
|
|
|
|
|
}
|
|
|
|
|
.logo:hover { color: var(--text); text-decoration: none; }
|
|
|
|
|
/* The mark is an inline SVG (see RenderNav): the cat face draws in
|
|
|
|
|
currentColor, so this one declaration colours it. */
|
|
|
|
|
.logo__mark { color: var(--accent); display: inline-flex; }
|
|
|
|
|
.logo__mark svg { width: 1.4em; height: 1.4em; }
|
|
|
|
|
/* The cat's mouth is the old ">_" prompt, and its underscore blinks like a
|
|
|
|
|
real terminal cursor: step-end, not a fade — cursors snap. Guarded below
|
|
|
|
|
for prefers-reduced-motion, which the --dur trick can't catch because this
|
|
|
|
|
animation has a literal duration. */
|
|
|
|
|
.logo-cursor { animation: logo-blink 1.2s step-end infinite; }
|
|
|
|
|
@keyframes logo-blink { 0% { opacity: 1; } 50% { opacity: 0; } }
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
.logo-cursor { animation: none; }
|
|
|
|
|
}
|
|
|
|
|
/* On a narrow viewport the wordmark is the first thing worth dropping —
|
|
|
|
|
the cat mark still identifies the site and the nav gets the room. */
|
|
|
|
|
@media (max-width: 26rem) {
|
|
|
|
|
.logo__text { display: none; }
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.nav-list { display: flex; gap: var(--s-3); flex-wrap: wrap; }
|
|
|
|
|
|
|
|
|
|
.nav-link {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: var(--s-3) var(--s-1);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
transition: color var(--dur-fast) var(--ease),
|
|
|
|
|
background-color var(--dur-fast) var(--ease);
|
|
|
|
|
}
|
|
|
|
|
.nav-link:hover {
|
|
|
|
|
color: var(--text);
|
|
|
|
|
background: var(--surface-3);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
.nav-link--active { color: var(--accent); }
|
|
|
|
|
.nav-link--active:hover { color: var(--accent-hover); }
|
|
|
|
|
|
|
|
|
|
/* hero — no headline by choice: the h1 is visually hidden (see RenderHome)
|
|
|
|
|
and this paragraph opens the page, so it gets full text colour and a size
|
|
|
|
|
step up from body copy rather than the muted supporting-text treatment it
|
|
|
|
|
had when a title sat above it. */
|
|
|
|
|
.hero { max-width: var(--measure); }
|
|
|
|
|
.hero__lede {
|
|
|
|
|
font-size: var(--step-1);
|
|
|
|
|
color: var(--text);
|
|
|
|
|
text-wrap: balance;
|
|
|
|
|
}
|
|
|
|
|
.hero__actions {
|
|
|
|
|
margin-top: var(--s1);
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: var(--s-1);
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.section__title {
|
|
|
|
|
font-size: var(--step-2);
|
|
|
|
|
letter-spacing: -0.015em;
|
|
|
|
|
padding-bottom: var(--s-2);
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
margin-bottom: var(--s1);
|
|
|
|
|
}
|
2025-11-12 21:56:18 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* buttons */
|
|
|
|
|
.btn {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
padding: var(--s-2) var(--s0);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
border: 1px solid var(--border-strong);
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
color: var(--text);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
font-weight: 550;
|
|
|
|
|
transition: background-color var(--dur-fast) var(--ease),
|
|
|
|
|
border-color var(--dur-fast) var(--ease);
|
|
|
|
|
}
|
|
|
|
|
.btn:hover {
|
|
|
|
|
background: var(--surface-3);
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
color: var(--text);
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
.btn--primary {
|
|
|
|
|
background: var(--accent);
|
|
|
|
|
border-color: var(--accent);
|
|
|
|
|
color: var(--text-on-accent);
|
|
|
|
|
}
|
|
|
|
|
.btn--primary:hover {
|
|
|
|
|
background: var(--accent-hover);
|
|
|
|
|
border-color: var(--accent-hover);
|
|
|
|
|
color: var(--text-on-accent);
|
|
|
|
|
}
|
2025-11-12 21:56:18 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.link-more {
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
}
|
|
|
|
|
.link-more::after { content: " \2192"; }
|
2025-11-12 21:56:18 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* featured cards on the home page */
|
|
|
|
|
.mini-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: var(--s0);
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
|
|
|
|
|
}
|
|
|
|
|
.mini-card {
|
|
|
|
|
padding: var(--s0);
|
|
|
|
|
background: var(--surface-1);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
transition: border-color var(--dur-fast) var(--ease);
|
|
|
|
|
}
|
|
|
|
|
.mini-card:hover { border-color: var(--border-strong); }
|
|
|
|
|
.mini-card__title { font-size: var(--step-0); font-family: var(--font-mono); }
|
|
|
|
|
.mini-card__blurb {
|
|
|
|
|
margin-top: var(--s-2);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
/* Clamp so uneven blurb lengths don't make a ragged grid. */
|
|
|
|
|
display: -webkit-box;
|
|
|
|
|
-webkit-line-clamp: 4;
|
|
|
|
|
-webkit-box-orient: vertical;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
2025-11-12 21:56:18 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* recent-posts list */
|
|
|
|
|
.recent { display: flex; flex-direction: column; }
|
|
|
|
|
.recent__item {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: var(--s0);
|
|
|
|
|
padding: var(--s-2) 0;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
.recent__date {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* page headers */
|
|
|
|
|
.page-header { max-width: var(--measure); margin-bottom: var(--s2); }
|
|
|
|
|
.page-header__title {
|
|
|
|
|
font-size: var(--step-3);
|
|
|
|
|
letter-spacing: -0.02em;
|
|
|
|
|
}
|
|
|
|
|
.page-header__lede {
|
|
|
|
|
margin-top: var(--s-1);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* projects — cards, deliberately shallow. The Forgejo page is the canonical
|
|
|
|
|
home for each project, so a card carries a name, one sentence and a
|
|
|
|
|
language chip, and the whole thing points there. */
|
|
|
|
|
.project-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: var(--s0);
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr));
|
|
|
|
|
}
|
|
|
|
|
.project-card {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
padding: var(--s0);
|
|
|
|
|
background: var(--surface-1);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
transition: border-color var(--dur-fast) var(--ease);
|
|
|
|
|
}
|
|
|
|
|
.project-card:hover { border-color: var(--accent); }
|
|
|
|
|
.project-card__title { font-size: var(--step-0); font-family: var(--font-mono); }
|
|
|
|
|
.project-card__blurb {
|
|
|
|
|
margin-top: var(--s-2);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
.project-card__lang {
|
|
|
|
|
/* Pushed to the card's bottom edge so the chips align across a row even
|
|
|
|
|
when blurb lengths differ. */
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
padding-top: var(--s-1);
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* status badges */
|
|
|
|
|
.badge {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
padding: 0.1em 0.6em;
|
|
|
|
|
border-radius: var(--radius-pill);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
.badge--active {
|
|
|
|
|
color: var(--ok);
|
|
|
|
|
border-color: color-mix(in oklab, var(--ok) 35%, transparent);
|
|
|
|
|
background: color-mix(in oklab, var(--ok) 10%, transparent);
|
|
|
|
|
}
|
|
|
|
|
.badge--experiment {
|
|
|
|
|
color: var(--warn);
|
|
|
|
|
border-color: color-mix(in oklab, var(--warn) 35%, transparent);
|
|
|
|
|
background: color-mix(in oklab, var(--warn) 10%, transparent);
|
|
|
|
|
}
|
2025-11-12 22:11:56 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* posts */
|
|
|
|
|
.post-list { display: grid; gap: var(--s0); }
|
2025-11-12 22:40:33 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.post-card {
|
|
|
|
|
padding: var(--s1);
|
|
|
|
|
background: var(--surface-1);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
transition: border-color var(--dur-fast) var(--ease);
|
|
|
|
|
}
|
|
|
|
|
.post-card:hover { border-color: var(--border-strong); }
|
|
|
|
|
|
|
|
|
|
.post-card__header { display: grid; gap: var(--s-3); }
|
|
|
|
|
.post-card__title { font-size: var(--step-1); letter-spacing: -0.01em; }
|
|
|
|
|
.post-card__title a { color: var(--text); }
|
|
|
|
|
.post-card__title a:hover { color: var(--accent); text-decoration: none; }
|
|
|
|
|
|
|
|
|
|
.post-card__meta {
|
|
|
|
|
margin-top: var(--s-3);
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: var(--s-1);
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
}
|
|
|
|
|
.post-card__community { color: var(--text-muted); }
|
2025-11-12 22:40:33 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.post-card__excerpt {
|
|
|
|
|
margin-top: var(--s-1);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
max-width: var(--measure);
|
|
|
|
|
}
|
2025-11-12 22:40:33 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.post-card__link {
|
|
|
|
|
margin-top: var(--s-2);
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
/* A pasted URL has no spaces to break at and would otherwise force
|
|
|
|
|
horizontal page scroll on a phone. */
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
}
|
2025-11-12 22:40:33 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* Post media — screenshots and screen recordings of the work, mirrored to our
|
|
|
|
|
own origin by tools/fetch-media.sh. */
|
|
|
|
|
.post-media {
|
|
|
|
|
margin-top: var(--s0);
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: var(--s-2);
|
|
|
|
|
}
|
|
|
|
|
/* More than one file gets a two-up grid so a post with four screenshots does
|
|
|
|
|
not become a mile of scrolling. */
|
|
|
|
|
.post-media:has(> :nth-child(2)) {
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
|
|
|
|
|
}
|
|
|
|
|
.post-media__item {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: auto;
|
|
|
|
|
/* The intrinsic size can be a 1829px-tall screenshot; cap it so one image
|
|
|
|
|
cannot dominate the page, and letterbox rather than crop — these are
|
|
|
|
|
screenshots where the edges carry information. */
|
|
|
|
|
max-height: 26rem;
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
background: var(--surface-0);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
}
|
|
|
|
|
video.post-media__item { max-height: 32rem; }
|
|
|
|
|
|
|
|
|
|
.post-card__footer {
|
|
|
|
|
margin-top: var(--s0);
|
|
|
|
|
padding-top: var(--s-2);
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: var(--s0);
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
.stat {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
}
|
|
|
|
|
.stat--link { color: var(--accent); }
|
2025-11-12 22:40:33 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* ── shop ─────────────────────────────────────────────────────────── */
|
2026-07-19 01:13:30 +02:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.product-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: var(--s1);
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
|
|
|
|
|
}
|
2026-07-19 01:13:30 +02:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.product-card {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
background: var(--surface-1);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
overflow: hidden; /* the media corner-clips to the card radius */
|
|
|
|
|
transition: border-color var(--dur-fast) var(--ease);
|
|
|
|
|
}
|
|
|
|
|
.product-card:hover { border-color: var(--accent); }
|
|
|
|
|
.product-card__media {
|
|
|
|
|
display: block;
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
}
|
|
|
|
|
.product-card__img {
|
|
|
|
|
display: block;
|
|
|
|
|
width: 100%;
|
|
|
|
|
max-height: 18rem;
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
padding: var(--s0);
|
|
|
|
|
}
|
|
|
|
|
.product-card__body { padding: var(--s0); }
|
|
|
|
|
.product-card__tagline { margin-top: var(--s-2); color: var(--text-muted); }
|
|
|
|
|
.product-card__status { margin-top: var(--s-1); }
|
|
|
|
|
|
|
|
|
|
/* The price block: both the EU-inclusive figure and its derived export twin
|
|
|
|
|
are always in the markup — identical HTML for every visitor, so the pages
|
|
|
|
|
stay cacheable and crawlers see everything. The timezone hint script tags
|
|
|
|
|
<html> with cc-eu or cc-noneu when it is CONFIRMED which region applies,
|
|
|
|
|
and then the other line is simply hidden: a Dutch buyer never reads about
|
|
|
|
|
import charges that cannot apply to them. No JS means neither class, and
|
|
|
|
|
the dual display with the EU line leading is the honest fallback. */
|
|
|
|
|
.price { margin-top: var(--s-1); }
|
|
|
|
|
|
|
|
|
|
/* The single headline number, on the card and at the top of the product
|
|
|
|
|
page alike. Its text starts as the euro price and the hint script may
|
|
|
|
|
swap it for a pre-rendered local-currency string — same element either
|
|
|
|
|
way, so one rule covers both. */
|
|
|
|
|
.price__single {
|
|
|
|
|
display: block;
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step-1);
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
}
|
2026-07-19 01:13:30 +02:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* The supporting lines under the product-page number: always secondary,
|
|
|
|
|
and the one that does not apply disappears once the region is confirmed.
|
|
|
|
|
Without JS both remain — complete and honest. */
|
|
|
|
|
.price__line {
|
|
|
|
|
display: block;
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
max-width: var(--measure);
|
|
|
|
|
}
|
|
|
|
|
.price__amount { font-family: var(--font-mono); }
|
|
|
|
|
.cc-eu .price__line--world { display: none; }
|
|
|
|
|
.cc-noneu .price__line--eu { display: none; }
|
|
|
|
|
|
|
|
|
|
.checkout__customs {
|
|
|
|
|
max-width: var(--measure);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
margin-bottom: var(--s1);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.price__from {
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* The live total the script fills in once colour, quantity and country are
|
|
|
|
|
known. It is the answer to "what will I actually pay", so it earns the
|
|
|
|
|
accent — the one place on the form where a number shouts. */
|
|
|
|
|
.checkout__total {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step-0);
|
|
|
|
|
font-weight: 650;
|
|
|
|
|
color: var(--accent);
|
|
|
|
|
padding: var(--s-1) 0;
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.product__photo {
|
|
|
|
|
display: block;
|
|
|
|
|
max-width: min(100%, 22rem);
|
|
|
|
|
height: auto;
|
|
|
|
|
margin: var(--s1) 0;
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
background: var(--surface-1);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.product__summary {
|
|
|
|
|
max-width: var(--measure);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-size: var(--step-1);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
|
|
|
|
|
.checkout__shipnote {
|
|
|
|
|
max-width: var(--measure);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
margin-bottom: var(--s-1);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
|
|
|
|
|
/* Order status page. */
|
|
|
|
|
.order__amount { font-family: var(--font-mono); }
|
|
|
|
|
.order__vat { color: var(--text-faint); font-size: var(--step--1); margin-top: var(--s-2); }
|
|
|
|
|
.order__indicative { color: var(--text-muted); font-size: var(--step--1); max-width: var(--measure); }
|
|
|
|
|
.order__note { color: var(--text-muted); font-size: var(--step--1); max-width: var(--measure); }
|
|
|
|
|
.order__keep {
|
|
|
|
|
margin-top: var(--s2);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
max-width: var(--measure);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
|
|
|
|
|
.section__lede {
|
|
|
|
|
max-width: var(--measure);
|
|
|
|
|
margin-bottom: var(--s0);
|
|
|
|
|
color: var(--text-muted);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
|
|
|
|
|
/* The spec table.
|
|
|
|
|
Wrapped in its own scroll container rather than allowed to widen the
|
|
|
|
|
page: a wide table must scroll itself, never make the body scroll
|
|
|
|
|
horizontally. */
|
|
|
|
|
.spec-table {
|
|
|
|
|
width: 100%;
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
display: block;
|
|
|
|
|
overflow-x: auto;
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
.spec-table th,
|
|
|
|
|
.spec-table td {
|
|
|
|
|
text-align: left;
|
|
|
|
|
padding: var(--s-2) var(--s-1);
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
vertical-align: top;
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
.spec-table th { color: var(--text-muted); font-weight: 500; white-space: nowrap; }
|
|
|
|
|
|
|
|
|
|
/* ── forms ────────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.checkout {
|
|
|
|
|
margin-top: var(--s3);
|
|
|
|
|
padding: var(--s1);
|
|
|
|
|
background: var(--surface-1);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
.checkout__lede {
|
|
|
|
|
max-width: var(--measure);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
margin-bottom: var(--s1);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.form { display: grid; gap: var(--s1); max-width: 32rem; }
|
|
|
|
|
|
|
|
|
|
.field { display: grid; gap: var(--s-3); }
|
|
|
|
|
.field label { font-weight: 550; font-size: var(--step--1); }
|
|
|
|
|
.field__req { color: var(--text-faint); font-weight: 400; }
|
|
|
|
|
.field__opt { color: var(--text-faint); font-weight: 400; }
|
|
|
|
|
|
|
|
|
|
.field input,
|
|
|
|
|
.field textarea {
|
|
|
|
|
/* 44px is the minimum comfortable touch target; anything less is a miss on
|
|
|
|
|
a phone, which is most of this audience. */
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
padding: var(--s-2) var(--s-1);
|
|
|
|
|
background: var(--surface-2);
|
|
|
|
|
color: var(--text);
|
|
|
|
|
border: 1px solid var(--border-strong);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
font: inherit;
|
|
|
|
|
width: 100%;
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
.field textarea { min-height: 5rem; resize: vertical; }
|
|
|
|
|
.field input:focus-visible,
|
|
|
|
|
.field textarea:focus-visible { border-color: var(--accent); }
|
|
|
|
|
|
|
|
|
|
/* :user-invalid, not :invalid — never mark a field the visitor hasn't
|
|
|
|
|
touched yet as wrong. */
|
|
|
|
|
.field input:user-invalid { border-color: var(--danger); }
|
|
|
|
|
|
|
|
|
|
.field__hint { font-size: var(--step--1); color: var(--text-faint); }
|
|
|
|
|
.field__error { font-size: var(--step--1); color: var(--danger); }
|
|
|
|
|
.field__error:empty { display: none; }
|
|
|
|
|
|
|
|
|
|
.notice {
|
|
|
|
|
padding: var(--s-1) var(--s0);
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
border: 1px solid var(--border-strong);
|
|
|
|
|
font-size: var(--step--1);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
.notice--ok {
|
|
|
|
|
color: var(--ok);
|
|
|
|
|
border-color: color-mix(in oklab, var(--ok) 35%, transparent);
|
|
|
|
|
background: color-mix(in oklab, var(--ok) 8%, transparent);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
.notice--error {
|
|
|
|
|
color: var(--danger);
|
|
|
|
|
border-color: color-mix(in oklab, var(--danger) 40%, transparent);
|
|
|
|
|
background: color-mix(in oklab, var(--danger) 10%, transparent);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 07:05:00 +02:00
|
|
|
/* The one-line person-company link under the hero actions. Muted: it is a
|
|
|
|
|
fact, not a call to action. */
|
|
|
|
|
.hero__byline {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* The coming-soon price list: the launch prices per colour, shown where the
|
|
|
|
|
colour selector will be once orders open. */
|
|
|
|
|
.checkout__colors {
|
|
|
|
|
list-style: none;
|
|
|
|
|
padding: 0;
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: var(--s-2);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
.checkout__colors li {
|
|
|
|
|
padding: var(--s-2) var(--s-1);
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* The emergency-calling note on the product page. Warn colours rather than
|
|
|
|
|
danger: it flags an unverified path, not a known-broken one. */
|
|
|
|
|
.notice--warn {
|
|
|
|
|
color: var(--warn);
|
|
|
|
|
border-color: color-mix(in oklab, var(--warn) 40%, transparent);
|
|
|
|
|
background: color-mix(in oklab, var(--warn) 10%, transparent);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
|
|
|
|
|
/* Honeypot: positioned off-screen rather than display:none, because some bots
|
|
|
|
|
skip hidden inputs entirely and would sail past the trap. Paired with
|
|
|
|
|
aria-hidden and tabindex="-1" in the markup so no real visitor — pointer,
|
|
|
|
|
keyboard or screen reader — can reach it. */
|
|
|
|
|
.honeypot {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -9999px;
|
|
|
|
|
width: 1px;
|
|
|
|
|
height: 1px;
|
|
|
|
|
overflow: hidden;
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* ── legal pages ──────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
/* Constrained to a readable measure. These are the longest prose on the site
|
|
|
|
|
and the only pages someone reads start to finish, so line length matters
|
|
|
|
|
more here than anywhere else. */
|
|
|
|
|
.legal { max-width: var(--measure); }
|
|
|
|
|
.legal__section { margin-top: var(--s2); }
|
|
|
|
|
.legal__heading {
|
|
|
|
|
font-size: var(--step-1);
|
|
|
|
|
letter-spacing: -0.01em;
|
|
|
|
|
padding-bottom: var(--s-3);
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
margin-bottom: var(--s-1);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
2026-08-05 04:18:37 +02:00
|
|
|
.legal__section p + p { margin-top: var(--s-1); }
|
|
|
|
|
.legal__section p { color: var(--text-muted); }
|
|
|
|
|
.legal__updated {
|
|
|
|
|
margin-top: var(--s-1);
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* ── demos ────────────────────────────────────────────────────────── */
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.demo-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
gap: var(--s1);
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
|
|
|
|
|
}
|
|
|
|
|
.demo-card {
|
|
|
|
|
padding: var(--s1);
|
|
|
|
|
background: var(--surface-1);
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
transition: border-color var(--dur-fast) var(--ease);
|
|
|
|
|
}
|
|
|
|
|
.demo-card:hover { border-color: var(--border-strong); }
|
|
|
|
|
.demo-card__title { font-size: var(--step-1); letter-spacing: -0.01em; }
|
|
|
|
|
.demo-card__blurb { margin-top: var(--s-2); color: var(--text-muted); }
|
|
|
|
|
.demo-card__meta { margin-top: var(--s-1); }
|
|
|
|
|
.demo-card__tech {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.breadcrumb {
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
margin-bottom: var(--s-1);
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.demo { margin-top: var(--s1); }
|
|
|
|
|
|
|
|
|
|
/* Host box the Crafter.Graphics render canvas is reparented into. It needs a
|
|
|
|
|
resolved height for getBoundingClientRect() — the canvas is absolutely
|
|
|
|
|
positioned inside it — so the size comes from aspect-ratio, which the
|
|
|
|
|
content file supplies per demo. The dark fill frames it before the first
|
|
|
|
|
traced frame lands. */
|
|
|
|
|
.demo-stage {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
|
|
|
|
aspect-ratio: 16 / 9; /* overridden inline per demo */
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: #05060a;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
.demo__requires {
|
|
|
|
|
margin-top: var(--s-2);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
.demo-facts {
|
|
|
|
|
margin-top: var(--s2);
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: auto 1fr;
|
|
|
|
|
gap: var(--s-3) var(--s0);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
}
|
|
|
|
|
.demo-facts dt { color: var(--text-faint); }
|
|
|
|
|
.demo-facts dd { font-family: var(--font-mono); }
|
|
|
|
|
|
|
|
|
|
/* Host box the Crafter.Graphics render canvas is reparented into. It needs
|
|
|
|
|
a resolved height for getBoundingClientRect() — the canvas is absolutely
|
|
|
|
|
positioned inside it — so the size comes from aspect-ratio. The dark fill
|
|
|
|
|
frames it before the first traced frame lands. */
|
|
|
|
|
.webgpu-demo-canvas {
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
|
|
|
|
aspect-ratio: 16 / 9;
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
background: #05060a;
|
|
|
|
|
border: 1px solid var(--border);
|
|
|
|
|
}
|
|
|
|
|
.webgpu-demo-caption {
|
|
|
|
|
margin-top: var(--s-2);
|
|
|
|
|
font-size: var(--step--1);
|
|
|
|
|
color: var(--text-faint);
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* footer */
|
|
|
|
|
.footer-tagline { font-size: var(--step--1); color: var(--text-muted); }
|
|
|
|
|
.footer-links { display: flex; gap: var(--s0); flex-wrap: wrap; }
|
|
|
|
|
.footer-links a { font-size: var(--step--1); font-family: var(--font-mono); }
|
|
|
|
|
.footer-legal { font-size: var(--step--1); color: var(--text-faint); }
|
|
|
|
|
|
|
|
|
|
.empty {
|
|
|
|
|
padding: var(--s1);
|
|
|
|
|
border: 1px dashed var(--border-strong);
|
|
|
|
|
border-radius: var(--radius);
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* ─── utilities ─────────────────────────────────────────────────────── */
|
|
|
|
|
@layer utilities {
|
|
|
|
|
.visually-hidden {
|
|
|
|
|
position: absolute;
|
|
|
|
|
width: 1px; height: 1px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
clip-path: inset(50%);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
.text-muted { color: var(--text-muted); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ─── overrides ─────────────────────────────────────────────────────── */
|
|
|
|
|
@layer overrides {
|
|
|
|
|
/* Backstop for anything added later. Individual entrance animations are
|
|
|
|
|
already opt-in via (prefers-reduced-motion: no-preference). */
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
*, *::before, *::after {
|
|
|
|
|
animation-duration: 0.01ms !important;
|
|
|
|
|
animation-iteration-count: 1 !important;
|
|
|
|
|
transition-duration: 0.01ms !important;
|
|
|
|
|
scroll-behavior: auto !important;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
@media print {
|
|
|
|
|
body { background: white; color: black; }
|
|
|
|
|
#cc-header, #cc-footer, .hero__actions { display: none; }
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
|
2026-08-05 04:18:37 +02:00
|
|
|
/* Windows high-contrast: colour-based state must also be conveyed
|
|
|
|
|
structurally, or the active nav item and status badges vanish. */
|
|
|
|
|
@media (forced-colors: active) {
|
|
|
|
|
.nav-link--active { text-decoration: underline; }
|
|
|
|
|
.badge { border: 1px solid currentColor; }
|
|
|
|
|
}
|
2025-11-12 21:24:23 +01:00
|
|
|
}
|