commit 4ac8749e910f9ba4598dafeeb47d6b3ef6a0676e
parent 1db3229ac2d29fe44c96953a52ff09d52608e8de
Author: $: DAΞMON <zer0sec.xp@icloud.com>
Date: Mon, 10 Aug 2026 01:57:07 +0100
🔧 Update [site-aesthetic] | 10 Aug 2026 01:57:07 BST
📊 Stats: +2520 / -755 across 30 file(s)
📁 Breakdown: ➕6 ✏️21 🗑️3 🔀0
👤 Author: $: DAΞMON
📝 Changes:
✏️ Modified astro.config.mjs
🗑️ Deleted src/components/CategoryCard.astro
✏️ Modified src/components/CategoryNav.astro
➕ Added src/components/DomainPlate.astro
✏️ Modified src/components/Footer.astro
✏️ Modified src/components/Header.astro
➕ Added src/components/Hero.astro
✏️ Modified src/components/Icon.astro
➕ Added src/components/Marquee.astro
➕ Added src/components/RecordRow.astro
✏️ Modified src/components/SearchModal.astro
➕ Added src/components/SectionBanner.astro
🗑️ Deleted src/components/SheetCard.astro
✏️ Modified src/layouts/Base.astro
✏️ Modified src/pages/404.astro
✏️ Modified src/pages/[category]/index.astro
✏️ Modified src/pages/credits.astro
✏️ Modified src/pages/index.astro
✏️ Modified src/pages/payloads/[...slug].astro
✏️ Modified src/pages/payloads/index.astro
✏️ Modified src/pages/sheets/[...slug].astro
✏️ Modified src/pages/tags/[tag].astro
✏️ Modified src/pages/tags/index.astro
✏️ Modified src/scripts/app.ts
✏️ Modified src/styles/app.css
➕ Added src/styles/chrome.css
🗑️ Deleted src/styles/glass.css
✏️ Modified src/styles/global.css
✏️ Modified src/styles/prose.css
✏️ Modified src/styles/tokens.css
📈 Line changes per file:
• astro.config.mjs +69 -5
• src/components/CategoryCard.astro +0 -21
• src/components/CategoryNav.astro +4 -17
• src/components/DomainPlate.astro +32 -0
• src/components/Footer.astro +11 -5
• src/components/Header.astro +48 -6
• src/components/Hero.astro +243 -0
• src/components/Icon.astro +8 -1
• src/components/Marquee.astro +134 -0
• src/components/RecordRow.astro +42 -0
• src/components/SearchModal.astro +41 -16
• src/components/SectionBanner.astro +272 -0
• src/components/SheetCard.astro +0 -25
• src/layouts/Base.astro +4 -2
• src/pages/404.astro +27 -20
• src/pages/[category]/index.astro +47 -27
• src/pages/credits.astro +6 -7
• src/pages/index.astro +67 -92
• src/pages/payloads/[...slug].astro +2 -2
• src/pages/payloads/index.astro +42 -32
• src/pages/sheets/[...slug].astro +14 -10
• src/pages/tags/[tag].astro +35 -9
• src/pages/tags/index.astro +48 -13
• src/scripts/app.ts +34 -14
• src/styles/app.css +1 -1
• src/styles/chrome.css +254 -0
• src/styles/glass.css +0 -121
• src/styles/global.css +776 -204
• src/styles/prose.css +107 -76
• src/styles/tokens.css +152 -29
Diffstat:
30 files changed, 2520 insertions(+), 755 deletions(-)
diff --git a/astro.config.mjs b/astro.config.mjs
@@ -30,6 +30,70 @@ function rehypeBaseLinks() {
return (tree) => walk(tree);
}
+/**
+ * The drop cap, as the main site sets it: the opening letter of a sheet
+ * floated three lines deep in EB Garamond.
+ *
+ * The letter is split out of the paragraph's first text node and wrapped
+ * in a `<span class="cap">` rather than left to `::first-letter`. That
+ * keeps it real text — selectable, searchable, and read aloud as part of
+ * the word it opens — and it is the only way to give it a different family
+ * and weight that renders the same across engines.
+ *
+ * Only the first top-level paragraph of the document takes one, and only
+ * if it is long enough to have three lines for the letter to sit in.
+ * Short openers — an image caption, a one-line note, a lede that is really
+ * a subtitle — are left alone, because a cap floated into a two-line
+ * paragraph pushes the text into a column beside it.
+ */
+const CAP_MIN_CHARS = 80;
+
+function rehypeDropCap() {
+ return (tree) => {
+ const paragraph = tree.children.find(
+ (n) => n.type === 'element' && n.tagName === 'p' && textLength(n) >= CAP_MIN_CHARS,
+ );
+ if (!paragraph) return;
+
+ // The first non-empty text node, wherever it sits — the paragraph may
+ // legitimately open with a <strong> or an <a>, and the cap belongs to
+ // the first *letter* rather than to the first direct child.
+ const lead = firstText(paragraph);
+ if (!lead) return;
+ const trimmed = lead.value.trimStart();
+ const letter = trimmed[0];
+ // Punctuation and quotes make poor caps: floated at 72px an opening
+ // quote reads as a stray mark rather than as a letter.
+ if (!letter || !/[A-Za-z0-9]/.test(letter)) return;
+
+ lead.value = trimmed.slice(1);
+ paragraph.children.unshift({
+ type: 'element',
+ tagName: 'span',
+ properties: { className: ['cap'] },
+ children: [{ type: 'text', value: letter }],
+ });
+ };
+
+ function textLength(node) {
+ if (node.type === 'text') return node.value.trim().length;
+ if (!node.children) return 0;
+ return node.children.reduce((n, c) => n + textLength(c), 0);
+ }
+
+ function firstText(node) {
+ if (!node.children) return null;
+ for (const child of node.children) {
+ if (child.type === 'text' && child.value.trim()) return child;
+ if (child.type === 'element') {
+ const found = firstText(child);
+ if (found) return found;
+ }
+ }
+ return null;
+ }
+}
+
// Project page: https://daemon-404.github.io/daemon-sec-cheatsheet
export default defineConfig({
site: 'https://daemon-404.github.io',
@@ -37,12 +101,12 @@ export default defineConfig({
trailingSlash: 'ignore',
integrations: [mdx(), sitemap()],
markdown: {
- rehypePlugins: [rehypeBaseLinks],
+ rehypePlugins: [rehypeBaseLinks, rehypeDropCap],
shikiConfig: {
- // Dual theme -> CSS variables we flip via [data-theme]. defaultColor:false
- // means Shiki writes both palettes as --shiki-light / --shiki-dark.
- themes: { light: 'rose-pine-dawn', dark: 'rose-pine-moon' },
- defaultColor: false,
+ // One theme, not two. Code blocks are dark plates in both modes (see
+ // prose.css) — a listing that turns cream in light mode stops reading
+ // as terminal output — so there is no second palette to flip to.
+ theme: 'rose-pine-moon',
wrap: false,
},
},
diff --git a/src/components/CategoryCard.astro b/src/components/CategoryCard.astro
@@ -1,21 +0,0 @@
----
-import type { CategoryDef } from '../lib/taxonomy';
-import { url } from '../lib/url';
-import Icon from './Icon.astro';
-interface Props { cat: CategoryDef; count: number }
-const { cat, count } = Astro.props;
----
-<a class="card pane glow" href={url(cat.slug)} style={`--acc: var(--${cat.accent});`}>
- <div class="pane__bar">
- <span class="pane__dots"><i></i><i></i><i></i></span>
- <span class="pane__label">[{cat.tag}]</span>
- </div>
- <div class="pane__body">
- <h3 class="card-title" style={`color: var(--${cat.accent});`}>{cat.title}</h3>
- <p class="card-desc">{cat.blurb}</p>
- <div class="card-meta">
- <span class="card-count">{count} {count === 1 ? 'sheet' : 'sheets'}</span>
- <span class="card-arrow"><Icon name="arrow" /></span>
- </div>
- </div>
-</a>
diff --git a/src/components/CategoryNav.astro b/src/components/CategoryNav.astro
@@ -4,6 +4,10 @@ import { url } from '../lib/url';
interface Props { active?: string }
const { active } = Astro.props;
---
+<!-- Twelve chips over a 1px ink ground, so the row reads as one ruled
+ strip rather than as twelve floating pills — the same construction as
+ the domain grid on the home page. Styling lives in global.css
+ (`.cat-nav` / `.cat-chip`) because the sheet pages use it too. -->
<nav class="cat-nav" aria-label="Categories">
{CATEGORIES.map((c) => (
<a
@@ -16,20 +20,3 @@ const { active } = Astro.props;
</a>
))}
</nav>
-
-<style>
- .cat-nav { display: flex; flex-wrap: wrap; gap: 0.5rem; }
- .cat-chip {
- display: inline-flex; align-items: center; gap: 0.45rem;
- padding: 0.4rem 0.75rem; border-radius: 999px;
- border: 1px solid var(--glass-brd); background: var(--glass-bg);
- font-size: 0.85rem; color: var(--fg-dim);
- transition: color 0.18s, border-color 0.18s, transform 0.18s;
- }
- .cat-chip:hover { color: var(--fg); transform: translateY(-1px); border-color: color-mix(in oklab, var(--acc) 45%, var(--glass-brd)); }
- .cat-chip__tag { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.08em; color: var(--acc); }
- .cat-chip[aria-current='page'] {
- color: var(--fg); border-color: color-mix(in oklab, var(--acc) 60%, transparent);
- box-shadow: 0 0 0 1px color-mix(in oklab, var(--acc) 25%, transparent), 0 4px 18px color-mix(in oklab, var(--acc) 15%, transparent);
- }
-</style>
diff --git a/src/components/DomainPlate.astro b/src/components/DomainPlate.astro
@@ -0,0 +1,32 @@
+---
+import type { CategoryDef } from '../lib/taxonomy';
+import { url } from '../lib/url';
+import Icon from './Icon.astro';
+
+interface Props { cat: CategoryDef; count: number; n: string }
+const { cat, count, n } = Astro.props;
+
+/**
+ * One of the twelve domains, as a tile.
+ *
+ * The hue lives on the top edge and nowhere else. Twelve fully-coloured
+ * cards would be a paint chart, and the colour is here to key the domain
+ * against the masthead and the marquee, not to fill the grid.
+ *
+ * The tiles carry no gutters — the grid draws 1px rules between them
+ * (`.grid-plates`) so the twelve read as one ruled table rather than as
+ * twelve floating objects.
+ */
+---
+<a class="plate-tile" href={url(cat.slug)} style={`--acc: var(--${cat.accent});`}>
+ <span class="plate-tile__head">
+ <span class="plate-tile__tag">{cat.tag}</span>
+ <span class="plate-tile__n">{n}</span>
+ </span>
+ <h3 class="plate-tile__title">{cat.title}</h3>
+ <p class="plate-tile__blurb">{cat.blurb}</p>
+ <span class="plate-tile__meta">
+ <span>{count} {count === 1 ? 'sheet' : 'sheets'}</span>
+ <span class="plate-tile__arrow"><Icon name="arrow" /></span>
+ </span>
+</a>
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
@@ -1,25 +1,31 @@
---
import { url } from '../lib/url';
const GITHUB = 'https://github.com/DAEMON-404/daemon-sec-cheatsheet';
+const SITE = 'https://daemon-404.github.io';
const year = 2026;
---
+<!-- A dark band closing the cream page: the same object as the marquee and
+ the section banners, in its last position. `.plate` pins the night
+ palette for the whole subtree, so this stays dark in both modes. -->
<footer class="site-footer plate">
+ <span class="grain grain--local" aria-hidden="true"></span>
<div class="wrap">
- <div class="stack" style="--gap:0.5rem;">
- <a class="brand" href={url('')}><b>DÆMON</b><span class="slash">//</span>SEC</a>
+ <div>
+ <a class="brand" href={url('')}>DÆMON<span class="slash">//</span>SEC</a>
<p class="footer-note">
A curated vault of IT & security cheatsheets. For authorized testing,
CTFs, and education only. Know your scope; get permission first.
</p>
</div>
- <div class="stack" style="--gap:0.4rem; text-align:right;">
- <a class="footer-mono" href={GITHUB} target="_blank" rel="noopener">github.com/DAEMON-404/daemon-sec-cheatsheet</a>
+ <div class="footer-col">
+ <a class="footer-mono" href={SITE} rel="noopener">daemon-404.github.io ↗</a>
+ <a class="footer-mono" href={GITHUB} target="_blank" rel="noopener">github / daemon-sec-cheatsheet ↗</a>
<span class="footer-mono muted">
Payloads mirrored from <a href="https://github.com/swisskyrepo/PayloadsAllTheThings" target="_blank" rel="noopener">PayloadsAllTheThings</a>
(MIT) · <a href={url('credits')}>credits</a>
</span>
<span class="footer-mono muted">© {year} DÆMON//SEC · MIT</span>
- <span class="footer-mono muted">Rose Pine · glass · built with Astro</span>
+ <span class="footer-mono muted">Rosé Pine · Astro · Pagefind</span>
</div>
</div>
</footer>
diff --git a/src/components/Header.astro b/src/components/Header.astro
@@ -1,19 +1,57 @@
---
import Icon from './Icon.astro';
import { url } from '../lib/url';
+
const GITHUB = 'https://github.com/DAEMON-404/daemon-sec-cheatsheet';
+const SITE = 'https://daemon-404.github.io';
+
+/**
+ * The masthead, in the main site's grammar.
+ *
+ * Each entry owns a hue and its index wears it, so the bar reads as the
+ * colour legend the rest of the site keys back to rather than as five grey
+ * words. The index keeps its space at rest rather than appearing on hover
+ * — revealing it would reflow the whole bar every time the pointer crossed
+ * an item.
+ *
+ * The accents are written out as full custom-property names rather than
+ * built up from the slug, because the value is read straight into `--acc`
+ * and a half-built token silently resolves to nothing.
+ */
+const NAV = [
+ { n: '01', label: 'Vault', href: url(''), acc: 'love', exact: true },
+ { n: '02', label: 'Payloads', href: url('payloads'), acc: 'iris' },
+ { n: '03', label: 'Tags', href: url('tags'), acc: 'foam' },
+ { n: '04', label: 'Credits', href: url('credits'), acc: 'gold' },
+];
+
+/* Trailing slashes are `ignore`d by the build, so both spellings of a path
+ reach here and an exact match would leave the bar saying nowhere. */
+const here = Astro.url.pathname.replace(/\/+$/, '') || '/';
+const isActive = (href: string, exact?: boolean) => {
+ const target = href.replace(/\/+$/, '') || '/';
+ return exact ? here === target : here === target || here.startsWith(target + '/');
+};
---
<header class="site-header">
<div class="wrap">
<a class="brand" href={url('')} aria-label="DÆMON//SEC home">
- <b>DÆMON</b><span class="slash">//</span>SEC<span class="blink">_</span>
+ DÆMON<span class="slash">//</span>SEC<span class="blink">_</span>
</a>
- <nav class="header-nav" aria-label="Primary">
- <a href={url('')}>Vault</a>
- <a href={url('payloads')}>Payloads</a>
+
+ <nav class="mast-nav" aria-label="Primary">
+ {NAV.map((item) => (
+ <a
+ href={item.href}
+ style={`--acc: var(--${item.acc});`}
+ aria-current={isActive(item.href, item.exact) ? 'page' : undefined}
+ >
+ <span class="n">{item.n}</span><span class="lbl">{item.label}</span>
+ </a>
+ ))}
</nav>
- <div class="header-spacer"></div>
- <div class="header-actions">
+
+ <div class="mast-actions">
<button class="search-trigger" data-search-open type="button" aria-label="Search cheatsheets">
<Icon name="search" />
<span class="st-label">Search</span>
@@ -23,6 +61,9 @@ const GITHUB = 'https://github.com/DAEMON-404/daemon-sec-cheatsheet';
<span class="icon-sun"><Icon name="sun" /></span>
<span class="icon-moon"><Icon name="moon" /></span>
</button>
+ <a class="icon-btn" href={SITE} rel="noopener" aria-label="DÆMON main site">
+ <Icon name="external" />
+ </a>
<a class="icon-btn" href={GITHUB} target="_blank" rel="noopener" aria-label="GitHub repository">
<Icon name="github" />
</a>
@@ -36,4 +77,5 @@ const GITHUB = 'https://github.com/DAEMON-404/daemon-sec-cheatsheet';
.icon-moon { display: none; }
:global(:root[data-theme='dark']) .icon-sun { display: none; }
:global(:root[data-theme='dark']) .icon-moon { display: inline-flex; }
+ .search-trigger svg { width: 15px; height: 15px; }
</style>
diff --git a/src/components/Hero.astro b/src/components/Hero.astro
@@ -0,0 +1,243 @@
+---
+import Icon from './Icon.astro';
+import { url } from '../lib/url';
+
+interface Props {
+ sheets: number;
+ domains: number;
+ payloads: number;
+}
+const { sheets, domains, payloads } = Astro.props;
+
+/**
+ * The hero, carried over from the main site's retired paper hero.
+ *
+ * Two display lines over a bordered ledger card. Both lines sit at their
+ * font's narrowest usable width with a vertical scale on top — the comp's
+ * poster face is not licensed here — and `scaleY` overflows its own line
+ * box, so each line lives in a fixed-height `cqw` wrapper that clips it
+ * (`.clip` / `.stretch` in chrome.css).
+ *
+ * Sized in `cqw` against the text column rather than in `vw` against the
+ * window. In `vw` the lockup is sized against a viewport the column stops
+ * tracking at `--maxw`, and past that width the type drifts out of line
+ * with everything under it — which is a thing you only see at 1900px.
+ *
+ * ── The spelling ──
+ * The main site sets the wordmark in substitution glyphs (DΛΣMӨП, three
+ * scripts). The vault self-hosts EB Garamond's *latin* subset only, so Λ Σ
+ * Ө П would fall back per-character to a different face at a different
+ * width and the lockup would render as six mismatched letters. The Latin
+ * spelling is the site's own documented fallback for exactly this case,
+ * and it is what the six-glyph hover key is built against.
+ */
+const WORDMARK = 'DÆMON';
+const GLYPHS = [...WORDMARK];
+
+/** Per-glyph entrance step, ms. Six glyphs at 54ms is what the ink rule's
+ * 780ms delay is timed against — change one and the rule lands wrong. */
+const STEP = 54;
+
+const STATS = [
+ { value: sheets, label: 'Curated<br />cheatsheets', tone: 'iris' },
+ { value: domains, label: 'Operator<br />domains', tone: 'love' },
+ { value: payloads, label: 'Mirrored<br />payload pages', tone: 'gold' },
+];
+---
+<section class="hero" id="top">
+ <div class="wrap hero__inner">
+ <div class="hero__lockup" data-lockup>
+ <div class="clip hero__line">
+ <h1 class="stretch hero__word" style="transform: scaleY(1.28);">
+ {GLYPHS.map((g, i) => (
+ <span class="hero__glyph" style={`animation-delay: ${i * STEP}ms;`}>{g}</span>
+ ))}
+ </h1>
+ </div>
+
+ <span class="hero__rule" aria-hidden="true"></span>
+
+ <div class="clip hero__subline">
+ <p class="stretch hero__sub"><span>The cheatsheet vault</span></p>
+ </div>
+ </div>
+
+ <div class="hero__ledger">
+ <div class="hero__card">
+ <div>
+ <div class="hero__eyebrow">Vault / 2026</div>
+ <h2 class="hero__h2">Zero fluff. Copy-ready.</h2>
+ <p class="hero__blurb">
+ {sheets} hand-picked cheatsheets across {domains} domains — Active Directory,
+ enumeration, exploitation, priv-esc, web, DFIR — stripped of vault-specific
+ syntax and refreshed for current tool flags. Plus the full
+ PayloadsAllTheThings mirror, searchable offline.
+ </p>
+ <div class="hero__actions">
+ <button class="btn" data-search-open type="button">
+ <Icon name="search" /> Search the vault
+ </button>
+ <a class="btn btn--ghost" href={url('payloads')}>
+ <Icon name="layers" /> Payloads
+ </a>
+ </div>
+ </div>
+
+ <div>
+ {/* The identity readout, carried over from the main site's hero.
+ Borderless on purpose: the card around it is the printed
+ spec, this is the terminal output inside it, so the two read
+ as different registers rather than as a box in a box. Every
+ value is a fact the vault can actually assert. */}
+ <div class="whoami">
+ <div class="whoami__cmd">
+ <span class="u">daemon@sec</span><span>~</span><span>%</span>
+ <span class="c">whoami --vault</span>
+ <span class="whoami__caret" aria-hidden="true"></span>
+ </div>
+ <dl class="whoami__rows">
+ <dt>scope</dt><dd>Offensive security · red team · DFIR</dd>
+ <dt>format</dt><dd>Copy-ready commands, current tool flags</dd>
+ <dt>search</dt><dd>Pagefind · offline · zero tracking</dd>
+ <dt>license</dt><dd>MIT · payloads mirrored from swisskyrepo</dd>
+ </dl>
+ </div>
+
+ <dl class="hero__stats" style="margin-top:1.6rem;">
+ {STATS.map((s) => (
+ <div class={`hero__stat hero__stat--${s.tone}`}>
+ <dt class="sr-only" set:html={s.label.replace('<br />', ' ')} />
+ <dd>
+ {/* Padded to two digits and counted up from zero. The
+ initial text is the final value, so with JS off or
+ reduced motion on, the number is simply correct. */}
+ <b data-count={s.value}>{String(s.value).padStart(2, '0')}</b>
+ <span set:html={` ${s.label}`} />
+ </dd>
+ </div>
+ ))}
+ </dl>
+ </div>
+ </div>
+ </div>
+ </div>
+</section>
+
+<script>
+ /**
+ * Proximity ripple across the lockup.
+ *
+ * The letter under the pointer lifts most and its neighbours fall off
+ * from it, so the wordmark answers the cursor as one object rather than
+ * as six independent hover targets.
+ *
+ * Written straight to the nodes from a rAF. `scale` and `translate` are
+ * separate CSS properties from `transform`, which the entrance animation
+ * owns and fills `both` — so the three compose instead of overwriting
+ * each other. Using `transform` here would cancel the entrance.
+ */
+ const LIFT = 0.075; // peak scale added to the letter under the pointer
+ const RISE = 0.022; // peak rise, in em, so it tracks the type size
+ const REACH = 2.1; // how far the falloff carries, in glyph widths
+
+ function initLockup(): void {
+ const host = document.querySelector<HTMLElement>('[data-lockup]');
+ if (!host || host.dataset.bound) return;
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+ host.dataset.bound = '1';
+
+ const glyphs = () => Array.from(host.querySelectorAll<HTMLElement>('.hero__glyph'));
+ let raf = 0;
+ let point: { x: number } | null = null;
+
+ const paint = () => {
+ raf = 0;
+ for (const g of glyphs()) {
+ if (!point) {
+ g.style.scale = '';
+ g.style.translate = '';
+ continue;
+ }
+ const r = g.getBoundingClientRect();
+ // Falloff measured in glyph widths, so it scales with the type
+ // rather than with a pixel constant that only suits one viewport.
+ const d = Math.abs(point.x - (r.left + r.width / 2)) / r.width;
+ const fade = Math.max(0, 1 - (d / REACH) ** 2);
+ g.style.scale = `${1 + LIFT * fade}`;
+ // Four places, not two: at 0.01em steps the rise quantises to
+ // roughly two pixels at this type size, and the falloff stepped
+ // between neighbours instead of easing across them.
+ g.style.translate = `0 ${(-RISE * fade).toFixed(4)}em`;
+ }
+ };
+
+ const queue = () => { if (!raf) raf = requestAnimationFrame(paint); };
+
+ const onMove = (e: PointerEvent) => {
+ // Coarse pointers have no hover to track; a tap would otherwise
+ // leave one letter stuck mid-lift.
+ if (e.pointerType !== 'mouse') return;
+ const r = host.getBoundingClientRect();
+ // A band around the lockup rather than the box itself: the letters
+ // should start answering as the pointer approaches, not the instant
+ // it crosses an invisible edge.
+ const near = e.clientY > r.top - r.height * 0.6 && e.clientY < r.bottom + r.height * 0.6;
+ point = near ? { x: e.clientX } : null;
+ queue();
+ };
+ const onLeave = () => { point = null; queue(); };
+
+ addEventListener('pointermove', onMove, { passive: true });
+ addEventListener('pointerleave', onLeave, { passive: true });
+ addEventListener('blur', onLeave);
+ }
+
+ /**
+ * The stats climb as their row lands rather than on page load, so the
+ * numbers are moving while they are being looked at rather than
+ * finishing behind the hero's entrance.
+ */
+ function initCounters(): void {
+ const nodes = document.querySelectorAll<HTMLElement>('[data-count]:not([data-counted])');
+ if (!nodes.length) return;
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+
+ const run = (el: HTMLElement) => {
+ el.dataset.counted = '1';
+ const target = Number(el.dataset.count ?? '0');
+ const pad = (n: number) => String(n).padStart(2, '0');
+ const t0 = performance.now();
+ const DURATION = 1100;
+ const tick = (now: number) => {
+ const p = Math.min(1, (now - t0) / DURATION);
+ // Ease-out cubic: the figure should decelerate onto its value
+ // rather than stop dead on it.
+ const eased = 1 - (1 - p) ** 3;
+ el.textContent = pad(Math.round(target * eased));
+ if (p < 1) requestAnimationFrame(tick);
+ };
+ requestAnimationFrame(tick);
+ };
+
+ const obs = new IntersectionObserver(
+ (entries) => {
+ for (const e of entries) {
+ if (!e.isIntersecting) continue;
+ run(e.target as HTMLElement);
+ obs.unobserve(e.target);
+ }
+ },
+ { threshold: 0.6 },
+ );
+ nodes.forEach((n) => obs.observe(n));
+ }
+
+ function init(): void {
+ initLockup();
+ initCounters();
+ }
+
+ document.addEventListener('astro:page-load', init);
+ if (document.readyState !== 'loading') init();
+ else document.addEventListener('DOMContentLoaded', init);
+</script>
diff --git a/src/components/Icon.astro b/src/components/Icon.astro
@@ -8,10 +8,17 @@ const paths: Record<string, string> = {
github: '<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.9a3.4 3.4 0 0 0-.9-2.6c3-.3 6.2-1.5 6.2-6.7A5.2 5.2 0 0 0 20 4.8 4.9 4.9 0 0 0 19.9 1S18.7.6 16 2.5a13.4 13.4 0 0 0-7 0C6.3.6 5.1 1 5.1 1A4.9 4.9 0 0 0 5 4.8a5.2 5.2 0 0 0-1.4 3.7c0 5.2 3.2 6.4 6.2 6.7a3.4 3.4 0 0 0-.9 2.5V22"/>',
arrow: '<path d="M5 12h14M13 6l6 6-6 6"/>',
chevron: '<path d="m9 6 6 6-6 6"/>',
- terminal: '<path d="m7 9 3 3-3 3M13 15h4"/><rect x="3" y="4" width="18" height="16" rx="2"/>',
+ terminal: '<path d="m7 9 3 3-3 3M13 15h4"/><rect x="3" y="4" width="18" height="16"/>',
hash: '<path d="M4 9h16M4 15h16M10 3 8 21M16 3l-2 18"/>',
wrench: '<path d="M14.7 6.3a4 4 0 0 0-5.4 5.2L3 18l3 3 6.5-6.3a4 4 0 0 0 5.2-5.4l-2.6 2.6-2.1-.5-.5-2.1z"/>',
close: '<path d="M18 6 6 18M6 6l12 12"/>',
+ copy: '<rect x="9" y="9" width="12" height="12"/><path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"/>',
+ check: '<path d="M20 6 9 17l-5-5"/>',
+ external: '<path d="M14 4h6v6M20 4 10 14M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/>',
+ /* The kind mark carried by every record row and marquee entry. A sheet
+ is a document; a payload is a fragment of one. */
+ doc: '<path d="M14 3H6a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8z"/><path d="M14 3v5h5M9 13h6M9 17h4"/>',
+ layers: '<path d="m12 3 9 5-9 5-9-5 9-5z"/><path d="m3 13 9 5 9-5"/>',
};
const d = paths[name] ?? '';
---
diff --git a/src/components/Marquee.astro b/src/components/Marquee.astro
@@ -0,0 +1,134 @@
+---
+import Icon from './Icon.astro';
+import type { CategoryDef } from '../lib/taxonomy';
+
+interface Props {
+ items: { cat: CategoryDef; count: number }[];
+}
+const { items } = Astro.props;
+
+/**
+ * The domain band — the archive's index grammar, run sideways.
+ *
+ * ── Grammar ──
+ * The band could have opened each entry with the section eyebrow, but that
+ * makes it read as twelve small banners sliding past. The site's core
+ * object is the record row: number, kind mark, title, meta, in that order,
+ * 10px mono with an 11px kind icon. Each entry carries exactly that line
+ * horizontally — the index, the kind, the domain in display type, the
+ * sheet count trailing in the meta slot — so the band reads as the live
+ * index in motion rather than as decoration. Like the row, the trailing
+ * count drops on narrow viewports, and like the index, every entry sets at
+ * the same weight: a row is a row.
+ *
+ * The `^:` mark survives in exactly one place — the band's own tab. It is
+ * the site's self-naming mark, and the band names itself once; repeated on
+ * every entry it was furniture. The tab counts its entries too, because
+ * every index on this site counts itself, and a count derived from the
+ * data cannot go stale.
+ *
+ * ── Colour ──
+ * The ground is `--band`: iris walked down to a weight that carries text
+ * (see tokens.css). The accent carries the domain title and its kind icon
+ * while every small word stays cream — record-row grammar, not a contrast
+ * ruling.
+ *
+ * Two identical runs, translated as one unit, so the reset is invisible.
+ */
+
+/* Rendered twice. The second run is `aria-hidden` — the band is one index,
+ not two, and a screen reader should not read twelve domains and then
+ read them again. */
+const RUN = items;
+---
+<!-- `.plate` pins the night palette for the whole band. Without it `--acc`
+ resolves to the *dawn* accents, which are reading weights drawn for
+ cream — on this ground pine and rose land near 1:1 and the kind marks
+ disappear. The band is dark in both modes, so it takes the dark
+ palette in both modes. -->
+<section class="marquee plate bleed" aria-label="Domains" data-marquee>
+ <span class="marquee__tab marquee__tab--l">
+ <span class="dot" aria-hidden="true"><i></i><u></u></span>
+ ^: Index
+ </span>
+
+ <div class="marquee__track" data-marquee-track>
+ {[0, 1].map((run) => (
+ <div class="marquee__run" aria-hidden={run === 1 ? 'true' : undefined}>
+ {RUN.map((item, i) => (
+ <span class="marquee__item" style={`--acc: var(--${item.cat.accent});`}>
+ <span class="marquee__n">{String(i + 1).padStart(2, '0')}</span>
+ <span class="marquee__kind"><Icon name="doc" /> {item.cat.tag}</span>
+ <span class="marquee__title">{item.cat.title}</span>
+ <span class="marquee__qual">{item.count} {item.count === 1 ? 'sheet' : 'sheets'}</span>
+ </span>
+ ))}
+ </div>
+ ))}
+ </div>
+
+ <span class="marquee__tab marquee__tab--r">{String(items.length).padStart(2, '0')} domains</span>
+</section>
+
+<script>
+ /**
+ * Driven from rAF rather than from a CSS keyframe, which is the whole
+ * reason this is script at all: a CSS animation cannot ease toward a
+ * fraction of its speed under the pointer, and a hard `paused` reads as
+ * a broken animation rather than as a deliberate slow-down.
+ */
+ const SPEED = 70; // px/s
+ const HOVER = 0.22; // multiplier under the pointer — never zero
+ const EASE = 0.08; // how fast the current speed chases the target
+
+ function initMarquee(): void {
+ const host = document.querySelector<HTMLElement>('[data-marquee]');
+ const track = document.querySelector<HTMLElement>('[data-marquee-track]');
+ if (!host || !track || host.dataset.bound) return;
+ host.dataset.bound = '1';
+
+ // Static under reduced motion: the first run is already the complete
+ // index, so it stays readable with nothing moving.
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
+
+ const run = track.firstElementChild as HTMLElement | null;
+ if (!run) return;
+
+ let offset = 0;
+ let speed = SPEED;
+ let target = SPEED;
+ let last = 0;
+ let raf = 0;
+
+ host.addEventListener('pointerenter', () => { target = SPEED * HOVER; });
+ host.addEventListener('pointerleave', () => { target = SPEED; });
+
+ const frame = (now: number) => {
+ const dt = last ? Math.min(0.05, (now - last) / 1000) : 0;
+ last = now;
+ speed += (target - speed) * EASE;
+ offset += speed * dt;
+
+ // One run's width is the wrap point: the second run lands exactly
+ // where the first started, so resetting there is invisible. Measured
+ // every frame rather than cached — the band reflows on resize and on
+ // font load, and a stale width shows up as a visible jump.
+ const width = run.offsetWidth;
+ if (width > 0 && offset >= width) offset -= width;
+
+ track.style.transform = `translate3d(${-offset.toFixed(2)}px, 0, 0)`;
+ raf = requestAnimationFrame(frame);
+ };
+
+ raf = requestAnimationFrame(frame);
+
+ // Astro's view transitions swap the document without unloading it, so
+ // an un-cancelled rAF would keep running against a detached node and
+ // stack a second loop on the next page that has a band.
+ document.addEventListener('astro:before-swap', () => cancelAnimationFrame(raf), { once: true });
+ }
+
+ document.addEventListener('astro:page-load', initMarquee);
+ if (document.readyState !== 'loading') initMarquee();
+ else document.addEventListener('DOMContentLoaded', initMarquee);
+</script>
diff --git a/src/components/RecordRow.astro b/src/components/RecordRow.astro
@@ -0,0 +1,42 @@
+---
+import Icon from './Icon.astro';
+
+interface Props {
+ href: string;
+ /** Zero-padded index, supplied by the list so a row never has to know
+ * where it sits. */
+ n: string;
+ title: string;
+ /** The kind mark — the domain tag for a sheet, the topic for a payload. */
+ kind: string;
+ kindIcon?: string;
+ accent?: string;
+ /** The trailing slot: difficulty, date, page count. Dropped below md,
+ * where the row has no room for a fifth column. */
+ meta?: string;
+}
+const { href, n, title, kind, kindIcon = 'doc', accent = 'foam', meta } = Astro.props;
+
+/**
+ * One record in an index — the site's core object.
+ *
+ * One line, not a card. The title is deliberately NOT set as a display
+ * heading: at heading weight every entry carries the weight of a section
+ * and a list of fifty becomes unscannable. The point of an index is the
+ * shape of the whole list, and that only reads if a row is a row.
+ *
+ * Hovering wipes ink across the row (`.wipe` in chrome.css). On that ink
+ * the dawn accents sit at roughly 1:1, so every coloured cell takes a
+ * cream weight instead of keeping a hue nobody could read — see the
+ * `.record:hover` rules in global.css.
+ */
+---
+<a class="record wipe dmn-t" href={href} style={`--acc: var(--${accent});`}>
+ <span class="record__grid">
+ <span class="record__n">{n}</span>
+ <span class="record__kind"><Icon name={kindIcon} /> {kind}</span>
+ <span class="record__title">{title}</span>
+ {meta ? <span class="record__meta">{meta}</span> : <span class="record__meta" />}
+ <span class="record__arrow"><Icon name="chevron" /></span>
+ </span>
+</a>
diff --git a/src/components/SearchModal.astro b/src/components/SearchModal.astro
@@ -2,7 +2,7 @@
import Icon from './Icon.astro';
---
<div class="search-overlay" data-search-overlay hidden>
- <div class="search-box pane glow" role="dialog" aria-modal="true" aria-label="Search cheatsheets">
+ <div class="search-box" role="dialog" aria-modal="true" aria-label="Search cheatsheets">
<div class="search-inputrow">
<Icon name="search" class="search-ico" />
<input
@@ -20,31 +20,56 @@ import Icon from './Icon.astro';
</div>
<style>
+ /* An opaque scrim rather than a frosted one. The page under it is as
+ often a dark band as it is cream, and a translucent overlay reads as
+ two different dialogs depending on where you happened to be. */
.search-overlay {
position: fixed; inset: 0; z-index: 100;
display: grid; place-items: start center;
padding: clamp(3rem, 12vh, 8rem) 1rem 2rem;
- background: color-mix(in oklab, var(--base) 55%, transparent);
- -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
+ background: color-mix(in oklab, var(--base) 88%, transparent);
}
.search-overlay[hidden] { display: none; }
- .search-box { width: min(640px, 100%); --acc: var(--iris); }
- .search-inputrow { display: flex; align-items: center; gap: 0.6rem; padding: 0.7rem 0.8rem; border-bottom: 1px solid var(--glass-brd); }
- .search-ico { width: 20px; height: 20px; color: var(--fg-faint); flex: 0 0 auto; }
+ .search-box {
+ width: min(660px, 100%);
+ border: 1px solid var(--fg);
+ background: var(--base);
+ }
+ .search-inputrow { display: flex; align-items: center; gap: 0.6rem; padding: 0.7rem 0.8rem; border-bottom: 1px solid var(--rule); }
+ .search-ico { width: 18px; height: 18px; color: var(--fg-faint); flex: 0 0 auto; }
.search-input {
flex: 1; background: none; border: none; outline: none;
- color: var(--fg); font-size: 1.05rem; font-family: var(--font-body);
+ color: var(--fg); font-size: 1.02rem; font-family: var(--font-mono);
}
.search-input::placeholder { color: var(--fg-faint); }
- .search-results { max-height: min(56vh, 520px); overflow-y: auto; padding: 0.5rem; }
- .search-hint { color: var(--fg-faint); font-size: 0.82rem; padding: 0.8rem; }
- .search-hint kbd { border: 1px solid var(--glass-brd); border-radius: 5px; padding: 0.05rem 0.3rem; }
- .s-result { display: block; padding: 0.7rem 0.8rem; border-radius: 10px; border: 1px solid transparent; }
- .s-result:hover, .s-result:focus-visible { background: var(--glass-bg); border-color: var(--glass-brd); }
- .s-cat { font-family: var(--font-mono); font-size: 0.66rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); }
- .s-title { font-family: var(--font-display); font-weight: 600; font-size: 1.02rem; margin-top: 0.15rem; }
- .s-excerpt { color: var(--fg-dim); font-size: 0.85rem; margin-top: 0.2rem; line-height: 1.45; }
- .s-excerpt :global(mark) { background: color-mix(in oklab, var(--gold) 30%, transparent); color: var(--fg); border-radius: 3px; padding: 0 2px; }
+ .search-results { max-height: min(56vh, 520px); overflow-y: auto; }
+ .search-hint {
+ color: var(--fg-faint); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
+ padding: 1rem 0.9rem;
+ }
+ .search-hint kbd { border: 1px solid var(--rule); padding: 0.06rem 0.3rem; }
+ /* Results are index rows, like every other list on the site — the ink
+ wipe is the same object, at the same timing. */
+ .s-result {
+ display: block; padding: 0.75rem 0.9rem;
+ border-bottom: 1px solid var(--rule);
+ background-image: linear-gradient(90deg, var(--fg) 0 50%, transparent 50% 100%);
+ background-size: 200% 100%;
+ background-position: 100% 0;
+ background-repeat: no-repeat;
+ transition: background-position 380ms var(--ease-in), color 1ms;
+ }
+ .s-result:hover, .s-result:focus-visible { background-position: 0 0; color: var(--base); }
+ .s-cat {
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase; color: var(--accent);
+ }
+ .s-result:hover .s-cat, .s-result:focus-visible .s-cat { color: rgba(250, 244, 237, 0.78); }
+ .s-title { font-family: var(--font-display); font-weight: 600; font-size: 1.02rem; letter-spacing: -0.015em; margin-top: 0.2rem; }
+ .s-excerpt { color: var(--fg-dim); font-size: 0.85rem; margin-top: 0.25rem; line-height: 1.45; }
+ .s-result:hover .s-excerpt, .s-result:focus-visible .s-excerpt { color: rgba(250, 244, 237, 0.78); }
+ .s-excerpt :global(mark) { background: color-mix(in oklab, var(--gold) 34%, transparent); color: inherit; padding: 0 2px; }
</style>
<script>
diff --git a/src/components/SectionBanner.astro b/src/components/SectionBanner.astro
@@ -0,0 +1,272 @@
+---
+interface Stat {
+ label: string;
+ value: string | number;
+ /** Paint the value in the banner's accent — one per banner, the number
+ * worth reading first. */
+ accent?: boolean;
+}
+
+interface Props {
+ /** Mono lockup before the rule, e.g. `03 ^: ACTIVE DIRECTORY`. */
+ n: string;
+ label: string;
+ title: string;
+ blurb?: string;
+ stats?: Stat[];
+ accent?: 'iris' | 'foam' | 'love' | 'gold' | 'rose' | 'pine';
+}
+const { n, label, title, blurb, stats = [], accent = 'foam' } = Astro.props;
+
+/**
+ * The page header every route below the home page lands on: dark plate,
+ * live signal field, left scrim, and a ledger on the right.
+ *
+ * Legibility on the plate is carried by two layers, not one — the canvas
+ * erases its own top and bottom bands, AND the scrim gradient sits over
+ * the noise and under the content. Either alone leaves the title sitting
+ * on a bright filament some fraction of the loop.
+ */
+---
+<header class="banner plate bleed" style={`--acc: var(--${accent});`}>
+ <canvas class="banner__field" data-fuzz aria-hidden="true"></canvas>
+ <span class="banner__scrim" aria-hidden="true"></span>
+ <span class="grain grain--local" aria-hidden="true"></span>
+
+ <div class="wrap banner__body">
+ <div>
+ <p class="eyebrow" style={`--acc: var(--${accent});`}>
+ <span class="eyebrow__n">{n}</span>
+ <span class="eyebrow__mark">^:</span>
+ <span>{label}</span>
+ <span class="eyebrow__rule" aria-hidden="true"></span>
+ </p>
+ <h1 class="banner__title">{title}</h1>
+ {blurb && <p class="banner__blurb">{blurb}</p>}
+ </div>
+
+ {stats.length > 0 && (
+ <dl class="banner__ledger">
+ {stats.map((s) => (
+ <div class={`banner__row${s.accent ? ' banner__row--acc' : ''}`}>
+ <dt>{s.label}</dt>
+ <dd><b>{s.value}</b></dd>
+ </div>
+ ))}
+ </dl>
+ )}
+ </div>
+</header>
+
+<script>
+ /**
+ * The banners' signal field: six pixel-sorted streaks drifting on an
+ * additive blend, plus bright filaments, looping seamlessly every nine
+ * seconds.
+ *
+ * Ported from the main site's logic class. Four things are load-bearing
+ * and break the look if changed: the loop-global phase (so two plates on
+ * one page stay in step), the DPR backing (at 1× the streaks are 1–2px
+ * and upscaling blurs the pixel-sorted look), the erasure scrim, and the
+ * synchronous frame zero.
+ */
+
+ /** Band geometry — six streaks, three hues between them. `hue` is a
+ * *slot* (1 = iris, 2 = foam, 3 = love), not a colour: which colour a
+ * slot resolves to, and how it composites, is read from CSS at draw
+ * time so the field follows the mode along with everything else. */
+ const BANDS = [
+ { cy: 0.10, ch: 0.20, hue: 1, drift: 1, cyc: 1, dens: 12 },
+ { cy: 0.34, ch: 0.24, hue: 2, drift: -1, cyc: 2, dens: 15 },
+ { cy: 0.22, ch: 0.14, hue: 3, drift: 2, cyc: 1, dens: 9 },
+ { cy: 0.60, ch: 0.22, hue: 2, drift: 1, cyc: 1, dens: 13 },
+ { cy: 0.80, ch: 0.16, hue: 1, drift: -1, cyc: 2, dens: 10 },
+ { cy: 0.92, ch: 0.14, hue: 3, drift: 1, cyc: 2, dens: 9 },
+ ] as const;
+
+ interface Palette {
+ blend: GlobalCompositeOperation;
+ bands: [string, string, string];
+ filaments: [string, string, string];
+ gain: number;
+ }
+
+ const FALLBACK: Palette = {
+ blend: 'lighter',
+ bands: ['196,167,231', '156,207,216', '235,111,146'],
+ filaments: ['224,222,244', '156,207,216', '196,167,231'],
+ gain: 1,
+ };
+
+ /**
+ * Resolve the palette off the canvas itself, so a plate inside a scope
+ * that pins the night palette draws the dark field even while the page
+ * around it is on paper. Reading the computed style is what makes that
+ * work: the cascade has already answered the question, and the canvas
+ * only has to ask the right element.
+ *
+ * Channels come back space-separated from CSS and the 2D context wants
+ * them comma-separated, hence the normalise.
+ */
+ function readPalette(cv: HTMLCanvasElement): Palette {
+ const cs = getComputedStyle(cv);
+ const channels = (name: string, fallback: string) => {
+ const v = cs.getPropertyValue(name).trim();
+ return v ? v.replace(/\s+/g, ',') : fallback;
+ };
+ const blend = cs.getPropertyValue('--fuzz-blend').trim();
+ const gain = parseFloat(cs.getPropertyValue('--fuzz-gain'));
+ return {
+ blend: (blend || FALLBACK.blend) as GlobalCompositeOperation,
+ bands: [
+ channels('--fuzz-band-1', FALLBACK.bands[0]),
+ channels('--fuzz-band-2', FALLBACK.bands[1]),
+ channels('--fuzz-band-3', FALLBACK.bands[2]),
+ ],
+ filaments: [
+ channels('--fuzz-fil-1', FALLBACK.filaments[0]),
+ channels('--fuzz-fil-2', FALLBACK.filaments[1]),
+ channels('--fuzz-fil-3', FALLBACK.filaments[2]),
+ ],
+ gain: Number.isFinite(gain) ? gain : FALLBACK.gain,
+ };
+ }
+
+ /** One frame of the field at `phase` ∈ [0,1). Every band's drift and
+ * breathe is a whole number of cycles per loop, so the 9s loop seams
+ * cleanly and any two plates on the page stay in step. */
+ function frame(cv: HTMLCanvasElement, phase: number, amt: number, pal: Palette): void {
+ // clientWidth is 0 for the first frames after insert; fall back to the
+ // host box so the field is not blank until the second layout.
+ const host = cv.parentElement;
+ const w = (cv.clientWidth || host?.clientWidth || 0) | 0;
+ const h = (cv.clientHeight || host?.clientHeight || 0) | 0;
+ if (!w || !h) return;
+
+ const dpr = Math.min(2, devicePixelRatio || 1);
+ const bw = Math.round(w * dpr);
+ const bh = Math.round(h * dpr);
+ if (cv.width !== bw || cv.height !== bh) { cv.width = bw; cv.height = bh; }
+
+ const ctx = cv.getContext('2d');
+ if (!ctx) return;
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+
+ const frac = (x: number) => x - Math.floor(x);
+ const rnd = (s: number) => frac(Math.sin(s * 127.1) * 43758.5453);
+
+ ctx.clearRect(0, 0, w, h);
+ ctx.globalCompositeOperation = pal.blend;
+
+ for (let bi = 0; bi < BANDS.length; bi++) {
+ const b = BANDS[bi];
+ const cy = b.cy * h;
+ const ch = b.ch * h;
+ for (let y = Math.max(0, Math.round(cy - ch)); y < Math.min(h, cy + ch); y += 2) {
+ const fall = 1 - Math.abs(y - cy) / ch;
+ const breathe = 0.5 + 0.5 * Math.sin(2 * Math.PI * (b.cyc * phase + y * 0.004 + bi * 0.3));
+ const env = fall * (0.4 + 0.6 * breathe);
+ if (env <= 0.02) continue;
+ const n = Math.round(env * b.dens * amt);
+ for (let i = 0; i < n; i++) {
+ const s = y * 7.3 + i * 13.7 + bi * 101.3;
+ const x = frac(rnd(s) + phase * b.drift) * (w + 420) - 210;
+ const len = (18 + rnd(s + 1) * 340) * (0.4 + env);
+ const hx = 0.22 + 0.78 * Math.pow(Math.sin(Math.PI * ((x / w) * (1 + (bi % 3)) + phase + rnd(bi * 9))), 2);
+ const a = (0.06 + rnd(s + 2) * 0.34) * env * hx * 1.7 * pal.gain;
+ ctx.fillStyle = `rgba(${pal.bands[b.hue - 1]},${a.toFixed(3)})`;
+ ctx.fillRect(x, y, len, rnd(s + 3) > 0.84 ? 2 : 1);
+ }
+ }
+ }
+
+ for (let i = 0; i < 54; i++) {
+ const s = i * 37.1;
+ const y = Math.round((rnd(s) * h) / 2) * 2;
+ const cyc = 1 + (i % 2);
+ const x = frac(rnd(s + 5) + phase * cyc) * (w + 700) - 350;
+ const a = (0.1 + rnd(s + 6) * 0.28) * amt * pal.gain;
+ ctx.fillStyle = `rgba(${pal.filaments[i % 3]},${a.toFixed(3)})`;
+ ctx.fillRect(x, y, 120 + rnd(s + 7) * 520, 1);
+ }
+
+ // Scrim by erasure: destination-out bands at top and bottom so type
+ // near the plate edges always sits on clean ink. The percentage clamp
+ // matters — a fixed 40px erased most of the short plates.
+ ctx.globalCompositeOperation = 'destination-out';
+ const solid = Math.min(40, h * 0.14);
+ const ramp = Math.min(26, h * 0.1);
+ ctx.fillStyle = 'rgba(0,0,0,1)';
+ ctx.fillRect(0, 0, w, solid);
+ ctx.fillRect(0, h - solid, w, solid);
+ let g = ctx.createLinearGradient(0, solid, 0, solid + ramp);
+ g.addColorStop(0, 'rgba(0,0,0,1)');
+ g.addColorStop(1, 'rgba(0,0,0,0)');
+ ctx.fillStyle = g;
+ ctx.fillRect(0, solid, w, ramp);
+ g = ctx.createLinearGradient(0, h - solid - ramp, 0, h - solid);
+ g.addColorStop(0, 'rgba(0,0,0,0)');
+ g.addColorStop(1, 'rgba(0,0,0,1)');
+ ctx.fillStyle = g;
+ ctx.fillRect(0, h - solid - ramp, w, ramp);
+ ctx.globalCompositeOperation = 'source-over';
+ }
+
+ /** Density scale. The main site ships at 6; the constant exists for
+ * tuning, not for the UI. */
+ const PRESS = 6;
+ /** The frame reduced motion keeps as the permanent texture — mid-loop,
+ * where the bands are at their fullest rather than at a seam. */
+ const STILL = 0.3;
+
+ function initFuzz(): void {
+ const canvases = document.querySelectorAll<HTMLCanvasElement>('[data-fuzz]:not([data-bound])');
+ canvases.forEach((cv) => {
+ cv.dataset.bound = '1';
+ const amt = Math.max(0.2, PRESS / 5);
+ const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
+
+ // Resolved once per mode rather than once per frame: getComputedStyle
+ // flushes style on a canvas repainting at 60fps, and there is nothing
+ // to re-read between mode changes.
+ let pal = readPalette(cv);
+
+ // Draw frame zero synchronously. rAF never fires in a hidden tab,
+ // during print, or in a screenshot, and a schedule-only start leaves
+ // the canvas at its 300×150 default — a bare plate in every capture.
+ const still = () => frame(cv, STILL, amt, pal);
+ frame(cv, reduced ? STILL : (performance.now() / 9000) % 1, amt, pal);
+
+ // The animated loop re-measures every frame, so it absorbs resizes on
+ // its own; the static frame needs the observer to stay crisp.
+ const ro = new ResizeObserver(() => { if (reduced) still(); });
+ ro.observe(cv.parentElement ?? cv);
+
+ // A bitmap does not inherit. The variables have already changed by
+ // the time this fires, so re-reading them is all it takes — but a
+ // still frame has to be redrawn by hand, or the plate keeps the old
+ // palette until something else resizes it.
+ const onMode = () => { pal = readPalette(cv); if (reduced) still(); };
+ addEventListener('daemonmodechange', onMode);
+
+ let raf = 0;
+ if (!reduced) {
+ const loop = (now: number) => {
+ frame(cv, (now / 9000) % 1, amt, pal);
+ raf = requestAnimationFrame(loop);
+ };
+ raf = requestAnimationFrame(loop);
+ }
+
+ document.addEventListener('astro:before-swap', () => {
+ cancelAnimationFrame(raf);
+ ro.disconnect();
+ removeEventListener('daemonmodechange', onMode);
+ }, { once: true });
+ });
+ }
+
+ document.addEventListener('astro:page-load', initFuzz);
+ if (document.readyState !== 'loading') initFuzz();
+ else document.addEventListener('DOMContentLoaded', initFuzz);
+</script>
diff --git a/src/components/SheetCard.astro b/src/components/SheetCard.astro
@@ -1,25 +0,0 @@
----
-import type { Sheet } from '../lib/sheets';
-import { sheetHref } from '../lib/sheets';
-import { categoryOf } from '../lib/taxonomy';
-import { url } from '../lib/url';
-import Icon from './Icon.astro';
-interface Props { entry: Sheet }
-const { entry } = Astro.props;
-const cat = categoryOf(entry.data.category);
-const d = entry.data;
----
-<a class="card pane glow" href={url(sheetHref(entry))} style={`--acc: var(--${cat.accent});`}>
- <div class="pane__bar">
- <span class="pane__dots"><i></i><i></i><i></i></span>
- <span class="pane__label">{cat.tag} / {entry.id.split('/').pop()}</span>
- </div>
- <div class="pane__body">
- <h3 class="card-title">{d.title}</h3>
- <p class="card-desc">{d.description}</p>
- <div class="card-meta">
- <span class="badge badge--{d.difficulty}">{d.difficulty}</span>
- <span class="card-arrow"><Icon name="arrow" /></span>
- </div>
- </div>
-</a>
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
@@ -50,8 +50,10 @@ const canonical = new URL(Astro.url.pathname, Astro.site).href;
<ClientRouter />
</head>
<body>
- <div class="fx-glow" aria-hidden="true"></div>
- <div class="fx-grid" aria-hidden="true"></div>
+ <!-- The film over the whole page. It is what keeps a flat colour
+ reading as stock rather than as a swatch, and it stands in for the
+ texture the old glass build got from blur. -->
+ <span class="grain" aria-hidden="true"></span>
<Header />
<main id="main">
<slot />
diff --git a/src/pages/404.astro b/src/pages/404.astro
@@ -1,30 +1,37 @@
---
import Base from '../layouts/Base.astro';
+import SectionBanner from '../components/SectionBanner.astro';
import Icon from '../components/Icon.astro';
import { url } from '../lib/url';
---
<Base title="404 — signal lost" description="This path resolves to nothing.">
+ <SectionBanner
+ n="00"
+ label="stderr"
+ title="404 — signal lost"
+ blurb="That route resolves to nothing. The host may be down, or the path was never in scope."
+ accent="love"
+ stats={[{ label: 'Status', value: '404', accent: true }, { label: 'Scope', value: 'unknown' }]}
+ />
+
<div class="wrap">
- <section class="hero pane" style="--acc: var(--love);">
- <div class="scanlines" aria-hidden="true"></div>
- <div class="pane__bar">
- <span class="pane__dots"><i></i><i></i><i></i></span>
- <span class="pane__label">stderr — 404</span>
+ <!-- The shell readout, in the hero's register: not a box, just output
+ sitting on the page under the band that named the error. -->
+ <div class="whoami" style="margin-top:2rem;">
+ <div class="whoami__cmd">
+ <span class="u">daemon@sec</span><span>~</span><span>%</span>
+ <span class="c">curl -sI $PATH</span>
+ <span class="whoami__caret" aria-hidden="true"></span>
</div>
- <div class="pane__body">
- <p class="eyebrow" style="color:var(--love)">// signal lost</p>
- <h1 class="hero-title">4<span class="hl2">0</span>4</h1>
- <p class="hero-sub">That route resolves to nothing. The host may be down,
- or the path was never in scope.</p>
- <div class="term glass" style="padding:1rem 1.1rem; margin-top:1.4rem; max-width:520px;">
- <div class="line"><span class="p">daemon@sec</span><span class="o">:</span><span class="k">~</span><span class="o">$</span> <span class="c">curl -sI {'<path>'}</span></div>
- <div class="line" style="color:var(--love)">HTTP/1.1 404 Not Found</div>
- </div>
- <div class="hero-actions">
- <a class="btn" href={url('')}><Icon name="terminal" /> Return to vault</a>
- <button class="btn btn--ghost" data-search-open type="button"><Icon name="search" /> Search instead</button>
- </div>
- </div>
- </section>
+ <dl class="whoami__rows">
+ <dt>status</dt><dd style="color: var(--love);">HTTP/1.1 404 Not Found</dd>
+ <dt>hint</dt><dd>the vault index is one level up</dd>
+ </dl>
+ </div>
+
+ <div class="hero__actions" style="margin: 2rem 0 3.5rem;">
+ <a class="btn" href={url('')}><Icon name="terminal" /> Return to vault</a>
+ <button class="btn btn--ghost" data-search-open type="button"><Icon name="search" /> Search instead</button>
+ </div>
</div>
</Base>
diff --git a/src/pages/[category]/index.astro b/src/pages/[category]/index.astro
@@ -1,54 +1,74 @@
---
import Base from '../../layouts/Base.astro';
-import SheetCard from '../../components/SheetCard.astro';
+import SectionBanner from '../../components/SectionBanner.astro';
+import RecordRow from '../../components/RecordRow.astro';
import CategoryNav from '../../components/CategoryNav.astro';
-import Icon from '../../components/Icon.astro';
import { CATEGORIES } from '../../lib/taxonomy';
import type { CategoryDef } from '../../lib/taxonomy';
-import { sheetsByCategory, type Sheet } from '../../lib/sheets';
+import { sheetsByCategory, sheetHref, type Sheet } from '../../lib/sheets';
import { url } from '../../lib/url';
export async function getStaticPaths() {
const groups = await sheetsByCategory();
- return CATEGORIES.map((c) => ({
+ return CATEGORIES.map((c, i) => ({
params: { category: c.slug },
- props: { cat: c, items: groups.find((g) => g.slug === c.slug)?.items ?? [] },
+ props: {
+ cat: c,
+ items: groups.find((g) => g.slug === c.slug)?.items ?? [],
+ n: String(i + 1).padStart(2, '0'),
+ },
}));
}
-interface Props { cat: CategoryDef; items: Sheet[] }
-const { cat, items } = Astro.props;
+interface Props { cat: CategoryDef; items: Sheet[]; n: string }
+const { cat, items, n } = Astro.props;
+
+/* The ledger counts what the page can actually count. Difficulty spread is
+ the one figure a reader uses to decide where to start in a domain. */
+const byDifficulty = (level: string) => items.filter((s) => s.data.difficulty === level).length;
+const tools = new Set(items.flatMap((s) => s.data.tools));
---
<Base title={`${cat.title} — DÆMON//SEC`} description={cat.blurb}>
+ <SectionBanner
+ n={n}
+ label={cat.tag}
+ title={cat.title}
+ blurb={cat.blurb}
+ accent={cat.accent}
+ stats={[
+ { label: 'Sheets', value: String(items.length).padStart(2, '0'), accent: true },
+ { label: 'Tools covered', value: String(tools.size).padStart(2, '0') },
+ { label: 'Beginner / adv', value: `${byDifficulty('beginner')} / ${byDifficulty('advanced')}` },
+ ]}
+ />
+
<div class="wrap">
- <nav class="breadcrumb" style="margin-top:1.5rem;">
+ <nav class="breadcrumb" style="margin-top:1.6rem;">
<a href={url('')}>~</a><span class="sep">/</span>
<span style={`color: var(--${cat.accent})`}>{cat.tag.toLowerCase()}</span>
</nav>
- <header class="cat-header">
- <p class="eyebrow">// [{cat.tag}] · {items.length} {items.length === 1 ? 'sheet' : 'sheets'}</p>
- <h1 class="sheet-title" style={`color: var(--${cat.accent});`}>{cat.title}</h1>
- <p class="sheet-desc">{cat.blurb}</p>
- </header>
-
- <div class="cat-nav-wrap"><CategoryNav active={cat.slug} /></div>
+ <div style="margin:1.4rem 0 2rem;"><CategoryNav active={cat.slug} /></div>
{items.length > 0 ? (
- <div class="grid-cards">
- {items.map((entry) => <SheetCard entry={entry} />)}
+ <div class="index" style="margin-bottom:3rem;">
+ {items.map((entry, i) => (
+ <RecordRow
+ href={url(sheetHref(entry))}
+ n={String(i + 1).padStart(2, '0')}
+ title={entry.data.title}
+ kind={entry.data.difficulty}
+ accent={cat.accent}
+ meta={entry.data.updated ?? entry.id.split('/').pop()}
+ />
+ ))}
</div>
) : (
- <div class="pane" style="margin-top:1.5rem;">
- <div class="pane__body">
- <p class="muted mono">No sheets in this domain yet. More landing soon —
- <a class="accent-text" href={url('')}>back to the vault</a>.</p>
- </div>
+ <div class="slab" style="margin:1.5rem 0 3rem; padding:1.2rem 1.3rem;">
+ <p class="muted mono" style="font-size:0.85rem;">
+ No sheets in this domain yet. More landing soon —
+ <a class="accent-text" href={url('')}>back to the vault</a>.
+ </p>
</div>
)}
</div>
</Base>
-
-<style>
- .cat-header { margin-top: 1rem; }
- .cat-nav-wrap { margin: 1.6rem 0 2rem; }
-</style>
diff --git a/src/pages/credits.astro b/src/pages/credits.astro
@@ -59,13 +59,12 @@ SOFTWARE.`;
</p>
<h3>MIT License</h3>
- <div class="code-pane" style="margin:1rem 0;">
- <div class="code-pane__bar">
- <span class="code-pane__dots"><i></i><i></i><i></i></span>
+ <figure class="code-pane" style="margin:1.2rem 0;">
+ <figcaption class="code-pane__bar">
<span class="code-pane__lang">LICENSE · PayloadsAllTheThings</span>
- </div>
- <pre style="padding:1rem 1.1rem;overflow:auto;"><code>{LICENSE}</code></pre>
- </div>
+ </figcaption>
+ <pre style="overflow:auto;font-size:13px;line-height:1.7;color:#e0def4;"><code>{LICENSE}</code></pre>
+ </figure>
<h2>The DÆMON//SEC cheatsheets</h2>
<p>
@@ -82,7 +81,7 @@ SOFTWARE.`;
<a href="https://github.com/DAEMON-404/daemon-sec-cheatsheet" target="_blank" rel="noopener">GitHub</a>.
</p>
- <p class="muted" style="margin-top:2rem;border-top:1px solid var(--glass-brd);padding-top:1rem;">
+ <p class="muted" style="margin-top:2rem;border-top:1px solid var(--rule);padding-top:1rem;">
For authorized testing, CTFs, and education only. Know your scope.
</p>
</div>
diff --git a/src/pages/index.astro b/src/pages/index.astro
@@ -1,10 +1,12 @@
---
import Base from '../layouts/Base.astro';
-import CategoryCard from '../components/CategoryCard.astro';
-import SheetCard from '../components/SheetCard.astro';
+import Hero from '../components/Hero.astro';
+import Marquee from '../components/Marquee.astro';
+import DomainPlate from '../components/DomainPlate.astro';
+import RecordRow from '../components/RecordRow.astro';
import Icon from '../components/Icon.astro';
-import { CATEGORIES } from '../lib/taxonomy';
-import { allSheets, categoryCounts } from '../lib/sheets';
+import { CATEGORIES, categoryOf } from '../lib/taxonomy';
+import { allSheets, categoryCounts, sheetHref } from '../lib/sheets';
import { payloadsByTopic } from '../lib/payloads';
import { url } from '../lib/url';
@@ -16,117 +18,90 @@ const domains = CATEGORIES.filter((c) => (counts[c.slug] ?? 0) > 0).length;
const payloadTopics = await payloadsByTopic();
const payloadPages = payloadTopics.reduce((n, g) => n + g.count, 0);
+const marqueeItems = CATEGORIES.map((cat) => ({ cat, count: counts[cat.slug] ?? 0 }));
+
const featured = [...sheets]
.sort((a, b) => (b.data.updated ?? '').localeCompare(a.data.updated ?? ''))
- .slice(0, 6);
+ .slice(0, 8);
---
<Base>
- <div class="wrap">
- <!-- HERO -->
- <section class="hero pane plate">
- <div class="scanlines" aria-hidden="true"></div>
- <div class="pane__bar">
- <span class="pane__dots"><i></i><i></i><i></i></span>
- <span class="pane__label">daemon-sec — /root/vault</span>
- </div>
- <div class="pane__body">
- <div class="hero-grid">
- <div>
- <p class="eyebrow">// curated offensive-security reference</p>
- <h1 class="hero-title" data-glitch>
- The <span class="hl">cheatsheet</span><br />vault for
- <span class="hl2">operators</span>.
- </h1>
- <p class="hero-sub">
- {total} hand-picked, modernized cheatsheets across {domains} domains —
- Active Directory, enumeration, exploitation, priv-esc, and more.
- Fast search, copy-ready commands, zero fluff.
- </p>
- <div class="hero-actions">
- <button class="btn" data-search-open type="button">
- <Icon name="search" /> Search the vault <kbd style="opacity:.6">/</kbd>
- </button>
- <a class="btn btn--ghost" href={url('active-directory')}>
- <Icon name="terminal" /> Browse Active Directory
- </a>
- </div>
- <div class="hero-stats">
- <div class="hero-stat"><b>{total}</b><span>Cheatsheets</span></div>
- <div class="hero-stat"><b>{domains}</b><span>Domains</span></div>
- <div class="hero-stat"><b>∞</b><span>Copy-paste ready</span></div>
- </div>
- </div>
+ <!-- The page's rhythm: paper hero, band, paper index, band, paper index.
+ The bands break the measure and run edge to edge; their contents
+ return to it. -->
+ <Hero sheets={total} domains={domains} payloads={payloadPages} />
- <div class="term glass" style="padding:1.1rem 1.2rem; align-self:stretch;">
- <div class="line"><span class="p">daemon@sec</span><span class="o">:</span><span class="k">~</span><span class="o">$</span> <span class="c">whoami</span></div>
- <div class="line o">operator — authorized. scope loaded.</div>
- <div class="line"><span class="p">daemon@sec</span><span class="o">:</span><span class="k">~</span><span class="o">$</span> <span class="c">grep -ri "kerberoast" ./vault</span></div>
- <div class="line o">active-directory/kerberoasting.md</div>
- <div class="line o">tools/rubeus.md</div>
- <div class="line"><span class="p">daemon@sec</span><span class="o">:</span><span class="k">~</span><span class="o">$</span> <span class="c">cat ./vault/index | wc -l</span></div>
- <div class="line o">{total}</div>
- <div class="line"><span class="p">daemon@sec</span><span class="o">:</span><span class="k">~</span><span class="o">$</span> <span class="cursor"></span></div>
- </div>
- </div>
- </div>
- </section>
+ <Marquee items={marqueeItems} />
- <!-- DOMAINS -->
+ <div class="wrap">
<section class="section" id="domains">
<div class="section-head">
<div>
- <p class="eyebrow">// 12 domains</p>
+ <p class="eyebrow" style="--acc: var(--foam);">
+ <span class="eyebrow__n">01</span>
+ <span class="eyebrow__mark">^:</span>
+ <span>{CATEGORIES.length} domains</span>
+ <span class="eyebrow__rule" aria-hidden="true"></span>
+ </p>
<h2>Pick your vector</h2>
</div>
</div>
- <div class="grid-cards">
- {CATEGORIES.map((cat) => <CategoryCard cat={cat} count={counts[cat.slug] ?? 0} />)}
+ <div class="grid-plates">
+ {CATEGORIES.map((cat, i) => (
+ <DomainPlate cat={cat} count={counts[cat.slug] ?? 0} n={String(i + 1).padStart(2, '0')} />
+ ))}
</div>
</section>
+ </div>
- <!-- PAYLOADS -->
- <section class="section" id="payloads-cta">
- <a class="pane glow payloads-cta plate" href={url('payloads')} style="--acc: var(--iris);">
- <div class="pane__bar">
- <span class="pane__dots"><i></i><i></i><i></i></span>
- <span class="pane__label">[payloads_all_the_things]</span>
- <span class="patt-badge" style="margin-left:auto;">mirror</span>
- </div>
- <div class="pane__body payloads-cta__body">
- <div>
- <p class="eyebrow" style="color: var(--iris)">// full mirror · MIT</p>
- <h2 style="color: var(--iris); margin:0.2rem 0 0.4rem;">PayloadsAllTheThings</h2>
- <p class="card-desc" style="max-width:52ch;">
- The complete <strong>{payloadTopics.length}-topic</strong>, <strong>{payloadPages}-page</strong> payload &
- bypass reference by <strong>swisskyrepo</strong> — mirrored on-site, fully searchable, every page linked to source.
- </p>
- </div>
- <span class="btn btn--ghost" style="flex:none;">Browse payloads <Icon name="arrow" /></span>
- </div>
- </a>
- </section>
+ <!-- The ledger band. Far enough down the page from the marquee that the
+ two never share a sightline, which is why they are free to differ —
+ and being separate tokens, they do so on purpose. -->
+ <a class="cta-band bleed section" href={url('payloads')}>
+ <div class="wrap cta-band__body">
+ <div>
+ <p class="cta-band__eyebrow">02 ^: Full mirror · MIT</p>
+ <h2>PayloadsAllTheThings</h2>
+ <p>
+ The complete {payloadTopics.length}-topic, {payloadPages}-page payload and bypass
+ reference by <strong>swisskyrepo</strong> — mirrored on-site, fully searchable,
+ every page linked back to source.
+ </p>
+ </div>
+ <span class="btn">Browse payloads <Icon name="arrow" /></span>
+ </div>
+ </a>
- <!-- FEATURED -->
- {featured.length > 0 && (
+ {featured.length > 0 && (
+ <div class="wrap">
<section class="section" id="featured">
<div class="section-head">
<div>
- <p class="eyebrow">// freshly modernized</p>
+ <p class="eyebrow" style="--acc: var(--gold);">
+ <span class="eyebrow__n">03</span>
+ <span class="eyebrow__mark">^:</span>
+ <span>Freshly modernized</span>
+ <span class="eyebrow__rule" aria-hidden="true"></span>
+ </p>
<h2>Recently updated</h2>
</div>
<a class="btn btn--ghost" href={url('tags')}>All tags <Icon name="arrow" /></a>
</div>
- <div class="grid-cards">
- {featured.map((entry) => <SheetCard entry={entry} />)}
+ <div class="index">
+ {featured.map((entry, i) => {
+ const cat = categoryOf(entry.data.category);
+ return (
+ <RecordRow
+ href={url(sheetHref(entry))}
+ n={String(i + 1).padStart(2, '0')}
+ title={entry.data.title}
+ kind={cat.tag}
+ accent={cat.accent}
+ meta={entry.data.updated ?? entry.data.difficulty}
+ />
+ );
+ })}
</div>
</section>
- )}
- </div>
+ </div>
+ )}
</Base>
-
-<style>
- @media (prefers-reduced-motion: no-preference) {
- [data-glitch] { animation: rise 0.7s var(--ease) both; }
- @keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
- }
-</style>
diff --git a/src/pages/payloads/[...slug].astro b/src/pages/payloads/[...slug].astro
@@ -62,12 +62,12 @@ const leaf = entry.id.split('/').pop()!.replace(/^index$/, d.topic);
<nav class="prevnext" aria-label="Pagination">
{prev ? (
- <a class="pane glow" href={url(payloadHref(prev))} style="--acc: var(--iris)">
+ <a href={url(payloadHref(prev))} style="--acc: var(--iris)">
<span class="dir">← Prev</span><strong>{prev.data.title}</strong>
</a>
) : <span></span>}
{next ? (
- <a class="pane glow nx" href={url(payloadHref(next))} style="--acc: var(--iris)">
+ <a class="nx" href={url(payloadHref(next))} style="--acc: var(--iris)">
<span class="dir">Next →</span><strong>{next.data.title}</strong>
</a>
) : <span></span>}
diff --git a/src/pages/payloads/index.astro b/src/pages/payloads/index.astro
@@ -1,5 +1,7 @@
---
import Base from '../../layouts/Base.astro';
+import SectionBanner from '../../components/SectionBanner.astro';
+import RecordRow from '../../components/RecordRow.astro';
import Icon from '../../components/Icon.astro';
import { payloadsByTopic } from '../../lib/payloads';
import { url } from '../../lib/url';
@@ -7,51 +9,59 @@ import { url } from '../../lib/url';
const groups = await payloadsByTopic();
const totalPages = groups.reduce((n, g) => n + g.count, 0);
const sha = groups[0]?.readme?.data.sha ?? groups[0]?.items[0]?.data.sha ?? '';
-// Rotate accent colours so the grid reads varied, like the domain grid.
+/* Rotated so the index reads varied down its kind column rather than as
+ sixty-six identical rows. */
const ACCENTS = ['iris', 'foam', 'gold', 'love', 'rose', 'pine'];
---
-<Base title="Payloads — PayloadsAllTheThings mirror — DÆMON//SEC" description="A full, searchable mirror of PayloadsAllTheThings — 66 topics of web & infra exploitation payloads. Credit: swisskyrepo, MIT.">
+<Base
+ title="Payloads — PayloadsAllTheThings mirror — DÆMON//SEC"
+ description="A full, searchable mirror of PayloadsAllTheThings — 66 topics of web & infra exploitation payloads. Credit: swisskyrepo, MIT."
+>
+ <SectionBanner
+ n="02"
+ label="Full mirror · MIT"
+ title="PayloadsAllTheThings"
+ blurb="A complete on-site mirror of swisskyrepo's payload and bypass reference — searchable offline, every page linked back to source."
+ accent="iris"
+ stats={[
+ { label: 'Topics', value: String(groups.length).padStart(2, '0'), accent: true },
+ { label: 'Pages', value: totalPages },
+ { label: 'License', value: 'MIT' },
+ ]}
+ />
+
<div class="wrap">
- <header class="sheet-head" style="margin-top:2rem;">
- <p class="eyebrow" style="color: var(--iris)">// Payloads</p>
- <h1 class="sheet-title">PayloadsAllTheThings</h1>
- <p class="sheet-desc">
- A full, on-site mirror of <a href="https://github.com/swisskyrepo/PayloadsAllTheThings" target="_blank" rel="noopener" style="color:var(--iris)">PayloadsAllTheThings</a>
- — {groups.length} topics, {totalPages} pages of web & infrastructure exploitation payloads and bypasses.
- All credit to <strong>swisskyrepo</strong> and its contributors. Licensed <strong>MIT</strong>.
- </p>
- </header>
+ <nav class="breadcrumb" style="margin-top:1.6rem;">
+ <a href={url('')}>~</a><span class="sep">/</span><span class="muted">payloads</span>
+ </nav>
- <aside class="patt-credit" style="margin-bottom:1.6rem;">
+ <aside class="patt-credit" style="margin-top:1.4rem;">
<span class="patt-badge">mirror</span>
<span class="patt-credit__text">
Mirrored verbatim from the upstream repo. Images and raw payload files load from GitHub; see each page's <em>source</em> link.
<span class="patt-lic">MIT · <a href={url('credits')}>full credits & license</a>{sha && <> · <code>{sha.slice(0,7)}</code></>}</span>
</span>
<a class="patt-src" href="https://github.com/swisskyrepo/PayloadsAllTheThings" target="_blank" rel="noopener">
- <Icon name="github" style="width:15px;height:15px;" /> upstream
+ <Icon name="github" /> upstream
</a>
</aside>
- <div class="grid-cards" style="margin-bottom:3rem;">
- {groups.map((g, i) => {
- const acc = ACCENTS[i % ACCENTS.length];
- return (
- <a class="card pane glow" href={url(`payloads/${g.slug}`)} style={`--acc: var(--${acc});`}>
- <div class="pane__bar">
- <span class="pane__dots"><i></i><i></i><i></i></span>
- <span class="pane__label">[{g.slug.replace(/-/g, '_').slice(0, 22)}]</span>
- </div>
- <div class="pane__body">
- <h3 class="card-title" style={`color: var(--${acc});`}>{g.title}</h3>
- <div class="card-meta">
- <span class="card-count">{g.count} {g.count === 1 ? 'page' : 'pages'}</span>
- <span class="card-arrow"><Icon name="arrow" /></span>
- </div>
- </div>
- </a>
- );
- })}
+ <div class="index" style="margin-bottom:3rem;">
+ {groups.map((g, i) => (
+ <RecordRow
+ href={url(`payloads/${g.slug}`)}
+ n={String(i + 1).padStart(2, '0')}
+ title={g.title}
+ kind="payload"
+ kindIcon="layers"
+ accent={ACCENTS[i % ACCENTS.length]}
+ meta={`${g.count} ${g.count === 1 ? 'page' : 'pages'}`}
+ />
+ ))}
</div>
</div>
</Base>
+
+<style>
+ .patt-src svg { width: 14px; height: 14px; }
+</style>
diff --git a/src/pages/sheets/[...slug].astro b/src/pages/sheets/[...slug].astro
@@ -36,7 +36,12 @@ const pdfHref = d.pdf ? url(`pdfs/${d.pdf}`) : null;
</nav>
<header class="sheet-head" data-pagefind-meta={`category:${cat.title}`}>
- <p class="eyebrow" style={`color: var(--${cat.accent})`}>// {cat.title}</p>
+ <p class="eyebrow" style={`--acc: var(--${cat.accent});`}>
+ <span class="eyebrow__n">{cat.tag}</span>
+ <span class="eyebrow__mark">^:</span>
+ <span>{cat.title}</span>
+ <span class="eyebrow__rule" aria-hidden="true"></span>
+ </p>
<h1 class="sheet-title" data-pagefind-meta="title">{d.title}</h1>
{d.description && <p class="sheet-desc">{d.description}</p>}
<div class="sheet-meta">
@@ -54,14 +59,13 @@ const pdfHref = d.pdf ? url(`pdfs/${d.pdf}`) : null;
<div class="sheet-layout">
<article class="prose" data-pagefind-body>
{pdfHref && (
- <div class="pane" style="margin-bottom:1.5rem;">
- <div class="pane__bar">
- <span class="pane__dots"><i></i><i></i><i></i></span>
- <span class="pane__label">{d.pdf}</span>
- <a class="code-copy" href={pdfHref} download style="margin-left:auto;text-decoration:none;">download</a>
- </div>
+ <figure class="code-pane" style="margin-bottom:1.6rem;">
+ <figcaption class="code-pane__bar">
+ <span class="code-pane__lang">{d.pdf}</span>
+ <a class="code-copy" href={pdfHref} download>Download</a>
+ </figcaption>
<iframe class="pdf-frame" src={pdfHref} title={`${d.title} (PDF)`} loading="lazy"></iframe>
- </div>
+ </figure>
)}
<Content />
</article>
@@ -80,12 +84,12 @@ const pdfHref = d.pdf ? url(`pdfs/${d.pdf}`) : null;
<nav class="prevnext" aria-label="Pagination">
{prev ? (
- <a class="pane glow" href={url(sheetHref(prev))} style={`--acc: var(--${cat.accent})`}>
+ <a href={url(sheetHref(prev))} style={`--acc: var(--${cat.accent})`}>
<span class="dir">← Prev</span><strong>{prev.data.title}</strong>
</a>
) : <span></span>}
{next ? (
- <a class="pane glow nx" href={url(sheetHref(next))} style={`--acc: var(--${cat.accent})`}>
+ <a class="nx" href={url(sheetHref(next))} style={`--acc: var(--${cat.accent})`}>
<span class="dir">Next →</span><strong>{next.data.title}</strong>
</a>
) : <span></span>}
diff --git a/src/pages/tags/[tag].astro b/src/pages/tags/[tag].astro
@@ -1,7 +1,9 @@
---
import Base from '../../layouts/Base.astro';
-import SheetCard from '../../components/SheetCard.astro';
-import { allSheets, allTags, tagSlug, type Sheet } from '../../lib/sheets';
+import SectionBanner from '../../components/SectionBanner.astro';
+import RecordRow from '../../components/RecordRow.astro';
+import { categoryOf } from '../../lib/taxonomy';
+import { allSheets, allTags, tagSlug, sheetHref, type Sheet } from '../../lib/sheets';
import { url } from '../../lib/url';
export async function getStaticPaths() {
@@ -14,20 +16,44 @@ export async function getStaticPaths() {
}
interface Props { tag: string; items: Sheet[] }
const { tag, items } = Astro.props;
+
+/* How many domains the tag reaches across — the figure that says whether a
+ tag is a niche or a spine running through the vault. */
+const domains = new Set(items.map((s) => s.data.category)).size;
---
<Base title={`#${tag} — DÆMON//SEC`} description={`Cheatsheets tagged ${tag}.`}>
+ <SectionBanner
+ n="04"
+ label="Tag"
+ title={`#${tag}`}
+ accent="iris"
+ stats={[
+ { label: 'Sheets', value: String(items.length).padStart(2, '0'), accent: true },
+ { label: 'Domains', value: String(domains).padStart(2, '0') },
+ ]}
+ />
+
<div class="wrap">
- <nav class="breadcrumb" style="margin-top:1.5rem;">
+ <nav class="breadcrumb" style="margin-top:1.6rem;">
<a href={url('')}>~</a><span class="sep">/</span>
<a href={url('tags')}>tags</a><span class="sep">/</span>
<span class="accent-text">{tag}</span>
</nav>
- <header style="margin-top:1rem;">
- <p class="eyebrow">// {items.length} {items.length === 1 ? 'sheet' : 'sheets'}</p>
- <h1 class="sheet-title">#{tag}</h1>
- </header>
- <div class="grid-cards" style="margin-top:1.8rem;">
- {items.map((entry) => <SheetCard entry={entry} />)}
+
+ <div class="index" style="margin:1.6rem 0 3rem;">
+ {items.map((entry, i) => {
+ const cat = categoryOf(entry.data.category);
+ return (
+ <RecordRow
+ href={url(sheetHref(entry))}
+ n={String(i + 1).padStart(2, '0')}
+ title={entry.data.title}
+ kind={cat.tag}
+ accent={cat.accent}
+ meta={entry.data.updated ?? entry.data.difficulty}
+ />
+ );
+ })}
</div>
</div>
</Base>
diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro
@@ -1,32 +1,60 @@
---
import Base from '../../layouts/Base.astro';
+import SectionBanner from '../../components/SectionBanner.astro';
import { allTags, allTools, tagSlug } from '../../lib/sheets';
import { url } from '../../lib/url';
const tags = await allTags();
const tools = await allTools();
---
<Base title="Tags & Tools — DÆMON//SEC" description="Browse every cheatsheet by tag or tool.">
+ <SectionBanner
+ n="04"
+ label="Cross-reference"
+ title="Tags & Tools"
+ blurb="Jump into the vault by topic or by the tool you're reaching for."
+ accent="foam"
+ stats={[
+ { label: 'Tags', value: String(tags.length).padStart(2, '0'), accent: true },
+ { label: 'Tools', value: String(tools.length).padStart(2, '0') },
+ ]}
+ />
+
<div class="wrap">
- <nav class="breadcrumb" style="margin-top:1.5rem;">
+ <nav class="breadcrumb" style="margin-top:1.6rem;">
<a href={url('')}>~</a><span class="sep">/</span><span class="muted">index</span>
</nav>
- <header style="margin-top:1rem;">
- <p class="eyebrow">// cross-reference</p>
- <h1 class="sheet-title">Tags & Tools</h1>
- <p class="sheet-desc">Jump into the vault by topic or by the tool you're reaching for.</p>
- </header>
- <section class="section" style="margin-top:2rem;">
- <div class="section-head"><div><p class="eyebrow">// {tags.length} tags</p><h2>By tag</h2></div></div>
+ <section class="section">
+ <div class="section-head">
+ <div>
+ <p class="eyebrow" style="--acc: var(--iris);">
+ <span class="eyebrow__n">01</span>
+ <span class="eyebrow__mark">^:</span>
+ <span>{tags.length} tags</span>
+ <span class="eyebrow__rule" aria-hidden="true"></span>
+ </p>
+ <h2>By tag</h2>
+ </div>
+ </div>
<div class="cloud">
{tags.map((t) => (
- <a class="tag-pill cloud-item" href={url(`tags/${tagSlug(t.tag)}`)}>#{t.tag}<span class="cnt">{t.count}</span></a>
+ <a class="tag-pill cloud-item" href={url(`tags/${tagSlug(t.tag)}`)}>{t.tag}<span class="cnt">{t.count}</span></a>
))}
</div>
</section>
<section class="section">
- <div class="section-head"><div><p class="eyebrow">// {tools.length} tools</p><h2>By tool</h2></div></div>
+ <div class="section-head">
+ <div>
+ <p class="eyebrow" style="--acc: var(--gold);">
+ <span class="eyebrow__n">02</span>
+ <span class="eyebrow__mark">^:</span>
+ <span>{tools.length} tools</span>
+ <span class="eyebrow__rule" aria-hidden="true"></span>
+ </p>
+ <h2>By tool</h2>
+ </div>
+ </div>
<div class="cloud">
{tools.map((t) => (
<span class="tag-pill cloud-item">{t.tool}<span class="cnt">{t.count}</span></span>
@@ -37,7 +65,14 @@ const tools = await allTools();
</Base>
<style>
- .cloud { display: flex; flex-wrap: wrap; gap: 0.55rem; }
- .cloud-item { display: inline-flex; align-items: center; gap: 0.45rem; }
- .cloud-item .cnt { font-size: 0.66rem; color: var(--fg-faint); background: var(--glass-bg); padding: 0.05rem 0.35rem; border-radius: 999px; }
+ .cloud { display: flex; flex-wrap: wrap; gap: 0.4rem; }
+ .cloud-item { display: inline-flex; align-items: center; gap: 0.5rem; }
+ /* The count is a second register inside the chip, not a badge on it —
+ square, hairline-separated, same 10px mono. */
+ .cloud-item .cnt {
+ padding-left: 0.5rem;
+ border-left: 1px solid var(--rule);
+ color: var(--fg-faint);
+ }
+ .cloud-item:hover .cnt { color: inherit; border-left-color: currentColor; }
</style>
diff --git a/src/scripts/app.ts b/src/scripts/app.ts
@@ -4,6 +4,9 @@ function applyTheme(next: 'light' | 'dark'): void {
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('theme', next); } catch {}
document.documentElement.style.colorScheme = next;
+ // A canvas bitmap does not inherit the cascade, so the banners' signal
+ // field has no way to notice the mode changed. It listens for this.
+ dispatchEvent(new Event('daemonmodechange'));
}
function initTheme(): void {
@@ -40,29 +43,46 @@ function initTheme(): void {
});
}
+/**
+ * Every code block becomes the archive's terminal block: the language at
+ * the left of a caption bar, the copy control at the right, both in the
+ * 9.5px mono the rest of the site labels things in.
+ *
+ * No window chrome and no traffic-light dots — this is a printed listing
+ * of terminal output, not a simulated window. The plate under it stays
+ * dark in both modes (see prose.css).
+ */
+const COPY_ICON =
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="12" height="12"/><path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1"/></svg>';
+const CHECK_ICON =
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>';
+
function enhanceCode(): void {
const blocks = document.querySelectorAll<HTMLPreElement>('.prose pre.astro-code:not([data-enhanced])');
blocks.forEach((pre) => {
pre.dataset.enhanced = '1';
- const lang = pre.getAttribute('data-language') || 'sh';
+ const lang = pre.getAttribute('data-language') || 'text';
- const pane = document.createElement('div');
+ const pane = document.createElement('figure');
pane.className = 'code-pane';
- const bar = document.createElement('div');
+ const bar = document.createElement('figcaption');
bar.className = 'code-pane__bar';
- const dots = document.createElement('span');
- dots.className = 'code-pane__dots';
- dots.append(document.createElement('i'), document.createElement('i'), document.createElement('i'));
const langEl = document.createElement('span');
langEl.className = 'code-pane__lang';
- langEl.textContent = lang.replace(/[^a-z0-9+#-]/gi, '').slice(0, 16) || 'sh';
- bar.append(dots, langEl);
+ langEl.textContent = lang.replace(/[^a-z0-9+#-]/gi, '').slice(0, 16) || 'text';
const copy = document.createElement('button');
copy.className = 'code-copy';
copy.type = 'button';
- copy.textContent = 'copy';
+ const setLabel = (icon: string, word: string) => {
+ copy.innerHTML = icon;
+ copy.append(document.createTextNode(word));
+ };
+ setLabel(COPY_ICON, 'Copy');
+
+ // The feedback lives on the button for 1400ms rather than in a toast
+ // across the page — you copied this block, not the document.
copy.addEventListener('click', async () => {
const text = pre.innerText;
try {
@@ -73,14 +93,14 @@ function enhanceCode(): void {
try { document.execCommand('copy'); } catch {}
ta.remove();
}
- copy.textContent = 'copied'; copy.classList.add('copied');
- setTimeout(() => { copy.textContent = 'copy'; copy.classList.remove('copied'); }, 1400);
+ setLabel(CHECK_ICON, 'Copied');
+ copy.classList.add('copied');
+ setTimeout(() => { setLabel(COPY_ICON, 'Copy'); copy.classList.remove('copied'); }, 1400);
});
- bar.appendChild(copy);
+ bar.append(langEl, copy);
pre.parentNode?.insertBefore(pane, pre);
- pane.appendChild(bar);
- pane.appendChild(pre);
+ pane.append(bar, pre);
});
}
diff --git a/src/styles/app.css b/src/styles/app.css
@@ -1,4 +1,4 @@
@import './tokens.css';
-@import './glass.css';
+@import './chrome.css';
@import './global.css';
@import './prose.css';
diff --git a/src/styles/chrome.css b/src/styles/chrome.css
@@ -0,0 +1,254 @@
+/* ============================================================================
+ Chrome — the site's form language, ported from the main DÆMON build.
+
+ This file replaces the old glass.css. Where that file made a container
+ out of translucency (blur, tint, inner highlight, 14px radius), this one
+ makes one out of a hairline and a value change. The objects below are
+ the whole vocabulary; every page is assembled from them.
+
+ .slab flat panel — 1px rule, square, no shadow
+ .plate inverse (night) panel, defined in tokens.css
+ .grain the film over everything
+ .micro 10px mono label — the site's furniture type
+ .eyebrow `03 ^: LABEL` lockup with a keyed rule
+ .btn flat control
+ .wipe ink wipe for index rows
+ .clip / .stretch the lockup's scale rig
+ ========================================================================== */
+
+/* ---- The film ------------------------------------------------------------ */
+/* Fractal noise laid over the page so a flat colour reads as stock rather
+ than as a swatch. It is the one thing standing in for the texture the
+ glass build got from blur, and it is why removing the blur does not
+ leave the page feeling like unpainted CSS.
+
+ Multiply on paper, screen on ink (tokens.css) — noise has to be laid
+ *into* a light ground and *added* to a dark one; the same blend on both
+ is invisible on one of them. */
+.grain {
+ position: fixed;
+ inset: 0;
+ z-index: 1;
+ pointer-events: none;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.6'/%3E%3C/svg%3E");
+ opacity: var(--grain-opacity);
+ mix-blend-mode: var(--grain-blend);
+}
+
+/* A plate carries its own grain, because it pins the night palette for its
+ subtree and therefore needs the dark blend while the page around it is
+ still on the light one. */
+.grain--local {
+ position: absolute;
+ z-index: 2;
+}
+
+/* ---- Panels -------------------------------------------------------------- */
+.slab {
+ border: 1px solid var(--rule);
+ background: var(--surface);
+}
+/* The heavier variant, for the one panel on a screen that is the point of
+ it — the site draws these at full ink rather than at rule weight. */
+.slab--ink {
+ border-color: var(--fg);
+ background: var(--base);
+}
+
+/* ---- Type furniture ------------------------------------------------------ */
+/* 10px mono, uppercase, widely tracked. Every eyebrow, kind mark, stat
+ label, tab and caption on the site is this object. */
+.micro {
+ font-family: var(--font-mono);
+ font-size: var(--step-micro);
+ letter-spacing: var(--track-micro);
+ text-transform: uppercase;
+ line-height: 1.6;
+}
+.micro--wide { letter-spacing: var(--track-wide); }
+
+/* The section lockup: an index, the site's self-naming `^:` mark, a label,
+ then a rule that runs out to the end of the line in the section's own
+ hue. It is what tells you which of the twelve domains you are in
+ without a heading having to say it twice. */
+.eyebrow {
+ display: flex;
+ align-items: center;
+ gap: 0.9rem;
+ font-family: var(--font-mono);
+ font-size: var(--step-micro);
+ letter-spacing: var(--track-wide);
+ text-transform: uppercase;
+ color: var(--fg-faint);
+}
+.eyebrow__n { color: var(--acc, var(--accent)); }
+.eyebrow__mark { color: var(--fg-faint); opacity: 0.7; }
+.eyebrow__rule {
+ flex: 1;
+ height: 1px;
+ background: var(--acc, var(--accent));
+ opacity: 0.5;
+}
+
+/* Section heading — Archivo at its heaviest, condensed, tracked in tight.
+ Uppercase is load-bearing: at this weight and width the lowercase
+ letterforms lose their counters and the word turns into a block. */
+.display {
+ font-family: var(--font-display);
+ font-weight: 700;
+ font-stretch: 84%;
+ letter-spacing: -0.03em;
+ line-height: 0.94;
+ text-transform: uppercase;
+}
+
+/* ---- Controls ------------------------------------------------------------ */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.55rem;
+ padding: 0.72rem 1.1rem;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: var(--track-micro);
+ text-transform: uppercase;
+ /* The label is a single tracked line. Without this it wraps at the
+ button's intrinsic width and the control turns into a three-line
+ block, which at this letter-spacing is unreadable. */
+ white-space: nowrap;
+ border: 1px solid transparent;
+ background: var(--fg);
+ color: var(--base);
+ transition: background-color 240ms ease, color 200ms ease, border-color 220ms ease;
+}
+/* An inline SVG with no intrinsic size resolves to 300×150 and detonates
+ the control. Every icon in a button is a 14px mark beside the label. */
+.btn svg { width: 14px; height: 14px; flex: none; }
+.btn:hover { background: var(--accent-2); }
+.btn--ghost {
+ background: transparent;
+ color: var(--fg);
+ border-color: var(--rule-hi);
+}
+.btn--ghost:hover { background: var(--fg); color: var(--base); border-color: var(--fg); }
+
+/* A chip is a bordered label, never a filled pill. Square, hairline, hue
+ from whatever it qualifies. */
+.chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.36rem 0.7rem;
+ font-family: var(--font-mono);
+ font-size: var(--step-micro);
+ letter-spacing: var(--track-micro);
+ text-transform: uppercase;
+ border: 1px solid color-mix(in oklab, var(--acc, var(--accent)) 55%, transparent);
+ color: var(--acc, var(--accent));
+ transition: background-color 240ms ease, color 200ms ease;
+}
+a.chip:hover { background: var(--acc, var(--accent)); color: var(--base); }
+
+/* The live dot — a filled core with a ring expanding off it. Used for
+ anything the page wants to claim is current rather than static. */
+.dot {
+ position: relative;
+ display: block;
+ width: 6px;
+ height: 6px;
+ flex: none;
+}
+.dot i,
+.dot u {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ display: block;
+}
+.dot i { background: currentColor; }
+.dot u { border: 1px solid currentColor; }
+@media (prefers-reduced-motion: no-preference) {
+ .dot i { animation: dmn-pulse 2.4s ease-in-out infinite; }
+ .dot u { animation: dmn-ring 4.2s ease-out infinite; }
+}
+
+/* ---- The index row's ink wipe -------------------------------------------- */
+/* Hovering a row fills it with ink from the left. Driven by
+ `background-position` over a hard-stop gradient rather than by
+ `background-color`, and the transition list deliberately has no `color`
+ in it — dragging a pointer down a fifty-row index is the one interaction
+ here that can drop frames, and a colour transition on every row it
+ crosses is what does it. The text colour flips instantly instead. */
+.wipe {
+ background-image: linear-gradient(90deg, var(--fg) 0 50%, transparent 50% 100%);
+ background-size: 200% 100%;
+ background-position: 100% 0;
+ background-repeat: no-repeat;
+ transition: background-position 380ms var(--ease-in), color 1ms;
+}
+.wipe:hover { background-position: 0 0; }
+
+/* ---- Shared timing ------------------------------------------------------- */
+.dmn-t {
+ transition:
+ background-color 240ms ease,
+ color 200ms ease,
+ border-color 220ms ease,
+ transform 220ms var(--ease-in),
+ opacity 200ms ease;
+}
+
+/* ---- The lockup's scale rig ---------------------------------------------- */
+/* `scaleY` overflows its own line box, so a stretched display line has to
+ sit in a fixed-height wrapper that clips it — otherwise it collides with
+ whatever is under it and no amount of margin fixes it, because the
+ overflow is not in the layout. */
+.clip { overflow: hidden; }
+.stretch {
+ transform-origin: 0 0;
+ white-space: nowrap;
+ user-select: none;
+}
+
+/* ---- Keyframes ----------------------------------------------------------- */
+@keyframes dmn-up {
+ from { opacity: 0; transform: translateY(18px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+@keyframes dmn-track-in {
+ from { letter-spacing: 0.075em; transform: scaleX(0.93); }
+ to { letter-spacing: 0.012em; transform: scaleX(1); }
+}
+@keyframes dmn-draw {
+ from { transform: scaleX(0); }
+ to { transform: scaleX(1); }
+}
+@keyframes dmn-wipe-in {
+ from { clip-path: inset(0 100% 0 0); }
+ to { clip-path: inset(0 0 0 0); }
+}
+@keyframes dmn-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.25; }
+}
+@keyframes dmn-ring {
+ 0% { transform: scale(1); opacity: 0.7; }
+ 70%, 100% { transform: scale(2.6); opacity: 0; }
+}
+@keyframes dmn-blink {
+ 50% { opacity: 0; }
+}
+
+/* Under reduced motion every entrance collapses to a single 0.01ms pass,
+ which lands on the same rest state the animation would have reached —
+ so the markup needs no second code path. */
+@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;
+ }
+}
diff --git a/src/styles/glass.css b/src/styles/glass.css
@@ -1,121 +0,0 @@
-/* ============================================================================
- Glass + the terminal-pane signature.
- .glass -> frosted translucent panel
- .pane -> glass panel with a monospace status bar + status dots
- ========================================================================== */
-
-.glass {
- background: var(--glass-bg);
- -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%);
- backdrop-filter: blur(var(--glass-blur)) saturate(150%);
- border: 1px solid var(--glass-brd);
- border-radius: var(--radius);
- box-shadow: var(--glass-shadow), inset 0 1px 0 rgba(255, 255, 255, 0.04);
-}
-
-@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
- .glass { background: var(--glass-bg-strong); }
-}
-
-/* Terminal pane: the universal container language --------------------------- */
-.pane {
- position: relative;
- background: var(--glass-bg);
- -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(150%);
- backdrop-filter: blur(var(--glass-blur)) saturate(150%);
- border: 1px solid var(--glass-brd);
- border-radius: var(--radius);
- box-shadow: var(--glass-shadow), inset 0 1px 0 rgba(255, 255, 255, 0.04);
- overflow: hidden;
-}
-
-.pane__bar {
- display: flex;
- align-items: center;
- gap: 0.6rem;
- padding: 0.5rem 0.85rem;
- border-bottom: 1px solid var(--glass-brd);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent);
- font-family: var(--font-mono);
- font-size: 0.72rem;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: var(--fg-faint);
-}
-
-.pane__dots {
- display: inline-flex;
- gap: 0.34rem;
- flex: 0 0 auto;
-}
-.pane__dots i {
- width: 9px;
- height: 9px;
- border-radius: 50%;
- display: block;
- opacity: 0.9;
-}
-.pane__dots i:nth-child(1) { background: var(--love); }
-.pane__dots i:nth-child(2) { background: var(--gold); }
-.pane__dots i:nth-child(3) { background: var(--foam); }
-
-.pane__label { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.pane__body { padding: 1rem 1.1rem; }
-
-/* Accent edge-glow, driven by --acc (a palette token) ----------------------- */
-.glow {
- --acc: var(--foam);
- transition: transform 0.35s var(--ease), box-shadow 0.35s var(--ease),
- border-color 0.35s var(--ease);
-}
-.glow:hover,
-.glow:focus-within {
- transform: translateY(-4px);
- border-color: color-mix(in oklab, var(--acc) 55%, var(--glass-brd-hi));
- box-shadow:
- var(--glass-shadow),
- 0 0 0 1px color-mix(in oklab, var(--acc) 35%, transparent),
- 0 10px 40px color-mix(in oklab, var(--acc) 22%, transparent);
-}
-
-/* Ambient background layers: grid + scanlines + drift ----------------------- */
-.fx-grid {
- position: fixed;
- inset: 0;
- z-index: -2;
- pointer-events: none;
- background-image:
- linear-gradient(var(--grid-line) 1px, transparent 1px),
- linear-gradient(90deg, var(--grid-line) 1px, transparent 1px);
- background-size: 46px 46px;
- mask-image: radial-gradient(120% 90% at 50% -10%, #000 30%, transparent 78%);
- -webkit-mask-image: radial-gradient(120% 90% at 50% -10%, #000 30%, transparent 78%);
-}
-.fx-glow {
- position: fixed;
- inset: 0;
- z-index: -3;
- pointer-events: none;
- background:
- radial-gradient(60% 45% at 12% 8%, color-mix(in oklab, var(--iris) 20%, transparent), transparent 60%),
- radial-gradient(55% 45% at 88% 4%, color-mix(in oklab, var(--foam) 16%, transparent), transparent 60%);
-}
-
-.scanlines {
- position: absolute;
- inset: 0;
- pointer-events: none;
- background: repeating-linear-gradient(
- 0deg, transparent 0, transparent 2px, var(--scan) 3px, transparent 4px
- );
- mix-blend-mode: screen;
- opacity: 0.6;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .scanlines { animation: scan-drift 8s linear infinite; }
- @keyframes scan-drift {
- from { background-position-y: 0; }
- to { background-position-y: 100px; }
- }
-}
diff --git a/src/styles/global.css b/src/styles/global.css
@@ -1,5 +1,8 @@
/* ============================================================================
Base elements, layout, chrome, and components.
+ Built out of the objects in chrome.css. Nothing here rounds a corner,
+ blurs a background, or casts a shadow — see the form-language note at
+ the top of tokens.css for why.
========================================================================== */
*, *::before, *::after { box-sizing: border-box; }
@@ -10,7 +13,7 @@ html {
scroll-behavior: smooth;
-webkit-text-size-adjust: 100%;
}
-:root[data-theme='dark'] html, html:has(:root[data-theme='dark']) { color-scheme: dark; }
+:root[data-theme='dark'] { color-scheme: dark; }
body {
font-family: var(--font-body);
@@ -30,236 +33,826 @@ a { color: inherit; text-decoration: none; }
img, svg { display: block; max-width: 100%; }
button { font: inherit; color: inherit; cursor: pointer; background: none; border: none; }
-h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 700; line-height: 1.1; letter-spacing: -0.01em; }
+h1, h2, h3, h4 {
+ font-family: var(--font-display);
+ font-weight: 700;
+ line-height: 1.06;
+ letter-spacing: -0.02em;
+}
+/* Square, like everything else — a rounded focus ring on a square page
+ reads as a rendering artefact rather than as focus. */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
- border-radius: 4px;
}
+/* Page content sits above the grain film. */
+main, .site-header, .site-footer { position: relative; z-index: 2; }
+
/* ---- Layout -------------------------------------------------------------- */
-.wrap { width: 100%; max-width: var(--maxw); margin-inline: auto; padding-inline: clamp(1rem, 3vw, 2rem); }
-.stack > * + * { margin-top: var(--gap, 1rem); }
-.eyebrow {
- font-family: var(--font-mono);
- font-size: 0.74rem;
- letter-spacing: 0.22em;
- text-transform: uppercase;
- color: var(--fg-faint);
+.wrap {
+ width: 100%;
+ max-width: var(--maxw);
+ margin-inline: auto;
+ padding-inline: clamp(1.15rem, 3.2vw, 2.75rem);
}
+/* Full-bleed: a band breaks the measure and runs edge to edge, but its
+ *contents* return to it. This is the page's basic rhythm — paper on the
+ measure, bands across the whole width. */
+.bleed { width: 100%; }
+.stack > * + * { margin-top: var(--gap, 1rem); }
.accent-text { color: var(--accent); }
-/* ---- Header -------------------------------------------------------------- */
+/* ---- Masthead ------------------------------------------------------------ */
+/* Opaque, not frosted: the bar has to hold its own value against whatever
+ scrolls under it, and on this page that is as often a dark band as it is
+ cream. A translucent bar over a passing band inverts mid-scroll. */
.site-header {
position: sticky;
top: 0;
- z-index: 50;
- padding: 0.55rem 0;
- border-bottom: 1px solid var(--glass-brd);
- background: color-mix(in oklab, var(--bg) 62%, transparent);
- -webkit-backdrop-filter: blur(14px) saturate(140%);
- backdrop-filter: blur(14px) saturate(140%);
-}
-.site-header .wrap { display: flex; align-items: center; gap: 1rem; }
+ z-index: 60;
+ border-bottom: 1px solid var(--fg);
+ background: var(--base);
+}
+.site-header .wrap {
+ display: flex;
+ align-items: stretch;
+ gap: clamp(1rem, 3vw, 2.4rem);
+ min-height: 58px;
+}
+
.brand {
font-family: var(--font-lockup);
font-weight: 600;
- font-size: 1.32rem;
+ font-size: 1.34rem;
letter-spacing: 0.015em;
display: inline-flex;
- align-items: baseline;
- gap: 0.08rem;
+ align-items: center;
+ gap: 0.06rem;
white-space: nowrap;
+ color: var(--fg);
}
-.brand b { color: var(--accent); font-weight: 700; }
-.brand .slash { color: var(--accent-2); }
+.brand .slash { color: var(--accent); }
.brand .blink { color: var(--accent); }
@media (prefers-reduced-motion: no-preference) {
- .brand .blink { animation: blink 1.15s steps(1) infinite; }
- @keyframes blink { 50% { opacity: 0; } }
+ .brand .blink { animation: dmn-blink 1.15s steps(1) infinite; }
+}
+
+/* The numbered nav. Each entry owns a hue and its index wears it, so the
+ bar reads as the colour legend the rest of the site keys back to rather
+ than as six grey words. The index keeps its space at rest — revealing it
+ on hover would reflow the whole bar every time the pointer crossed an
+ item. */
+.mast-nav {
+ display: flex;
+ align-items: stretch;
+ margin-left: auto;
+}
+.mast-nav a {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0 clamp(0.6rem, 1.4vw, 1.05rem);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: var(--track-micro);
+ text-transform: uppercase;
+ color: var(--fg-dim);
+ transition: color 200ms ease;
+}
+.mast-nav a:hover,
+.mast-nav a[aria-current='page'] { color: var(--fg); }
+.mast-nav .n {
+ font-size: var(--step-micro);
+ color: var(--fg-faint);
+ transition: color 200ms ease;
+}
+.mast-nav a:hover .n,
+.mast-nav a[aria-current='page'] .n { color: var(--acc); }
+/* The rule wipes in from the left on hover and stays put when the entry is
+ the current page. */
+.mast-nav a::after {
+ content: '';
+ position: absolute;
+ left: 0; right: 0; bottom: -1px;
+ height: 2px;
+ background: var(--acc);
+ transform: scaleX(0);
+ transform-origin: 0 50%;
+ transition: transform 260ms var(--ease-in);
}
-.header-spacer { flex: 1; }
-.header-actions { display: flex; align-items: center; gap: 0.5rem; }
+.mast-nav a:hover::after,
+.mast-nav a[aria-current='page']::after { transform: scaleX(1); }
+@media (max-width: 900px) { .mast-nav .lbl { display: none; } }
+@media (max-width: 640px) { .mast-nav { display: none; } }
+.mast-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding-left: clamp(0.5rem, 1.5vw, 1rem);
+ border-left: 1px solid var(--rule);
+}
.icon-btn {
- width: 38px; height: 38px;
+ width: 34px; height: 34px;
display: grid; place-items: center;
- border-radius: 10px;
- border: 1px solid var(--glass-brd);
- background: var(--glass-bg);
+ border: 1px solid var(--rule);
color: var(--fg-dim);
- transition: color 0.2s, border-color 0.2s, background 0.2s;
+ transition: color 200ms ease, border-color 220ms ease, background-color 240ms ease;
}
-.icon-btn:hover { color: var(--accent); border-color: var(--glass-brd-hi); }
-.icon-btn svg { width: 18px; height: 18px; }
+.icon-btn:hover { color: var(--base); background: var(--fg); border-color: var(--fg); }
+.icon-btn svg { width: 17px; height: 17px; }
.search-trigger {
- display: inline-flex; align-items: center; gap: 0.6rem;
- height: 38px; padding: 0 0.7rem 0 0.8rem;
- border-radius: 10px;
- border: 1px solid var(--glass-brd);
- background: var(--glass-bg);
+ display: inline-flex; align-items: center; gap: 0.55rem;
+ height: 34px; padding: 0 0.6rem 0 0.7rem;
+ border: 1px solid var(--rule);
color: var(--fg-faint);
- font-size: 0.85rem;
- transition: color 0.2s, border-color 0.2s;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ transition: color 200ms ease, border-color 220ms ease;
}
-.search-trigger:hover { color: var(--fg-dim); border-color: var(--glass-brd-hi); }
+.search-trigger:hover { color: var(--fg); border-color: var(--rule-hi); }
.search-trigger kbd {
- font-family: var(--font-mono); font-size: 0.72rem;
- padding: 0.1rem 0.36rem; border-radius: 6px;
- border: 1px solid var(--glass-brd); color: var(--fg-faint);
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ padding: 0.08rem 0.32rem;
+ border: 1px solid var(--rule); color: var(--fg-faint);
}
.search-trigger .st-label { display: none; }
@media (min-width: 560px) { .search-trigger .st-label { display: inline; } }
/* ---- Nav rail (desktop) -------------------------------------------------- */
-.layout { display: grid; grid-template-columns: 1fr; gap: 2rem; }
+.layout { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
@media (min-width: 960px) {
.layout.has-rail { grid-template-columns: var(--rail) minmax(0, 1fr); }
}
-.rail { position: sticky; top: 70px; align-self: start; max-height: calc(100vh - 90px); overflow-y: auto; display: none; }
+.rail { position: sticky; top: 78px; align-self: start; max-height: calc(100vh - 100px); overflow-y: auto; display: none; }
@media (min-width: 960px) { .rail { display: block; } }
.rail h4 {
- font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.16em;
- text-transform: uppercase; color: var(--fg-faint); margin: 1.1rem 0 0.5rem;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-wide);
+ text-transform: uppercase; color: var(--fg-faint);
+ margin: 1.3rem 0 0.6rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--rule);
}
.rail a {
display: flex; align-items: center; gap: 0.5rem;
- padding: 0.34rem 0.6rem; border-radius: 8px;
- color: var(--fg-dim); font-size: 0.9rem;
- border-left: 2px solid transparent;
- transition: color 0.18s, background 0.18s, border-color 0.18s;
-}
-.rail a:hover { color: var(--fg); background: var(--glass-bg); }
-.rail a[aria-current='page'] { color: var(--accent); border-left-color: var(--accent); background: var(--glass-bg); }
-.rail .rail-tag { font-family: var(--font-mono); font-size: 0.64rem; color: var(--fg-faint); margin-left: auto; }
-
-/* ---- Buttons + badges + tags -------------------------------------------- */
-.btn {
- display: inline-flex; align-items: center; gap: 0.5rem;
- padding: 0.6rem 1.05rem; border-radius: 11px;
- font-family: var(--font-mono); font-size: 0.85rem; letter-spacing: 0.02em;
- border: 1px solid var(--glass-brd-hi);
- background: var(--glass-bg); color: var(--fg);
- transition: transform 0.2s var(--ease), border-color 0.2s, box-shadow 0.2s;
-}
-.btn:hover { transform: translateY(-2px); border-color: color-mix(in oklab, var(--accent) 50%, transparent); box-shadow: 0 6px 24px color-mix(in oklab, var(--accent) 18%, transparent); }
-.btn--ghost { background: transparent; }
+ padding: 0.34rem 0 0.34rem 0.7rem;
+ color: var(--fg-dim); font-size: 0.88rem;
+ border-left: 2px solid var(--rule);
+ transition: color 180ms ease, border-color 180ms ease, padding-left 260ms var(--ease-in);
+}
+.rail a:hover { color: var(--fg); border-left-color: var(--fg); padding-left: 1rem; }
+.rail a[aria-current='page'] { color: var(--accent); border-left-color: var(--accent); }
+.rail .rail-tag { font-family: var(--font-mono); font-size: var(--step-micro); color: var(--fg-faint); margin-left: auto; }
+/* ---- Badges + tags ------------------------------------------------------- */
.badge {
display: inline-flex; align-items: center; gap: 0.35rem;
- font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.06em;
- padding: 0.2rem 0.5rem; border-radius: 999px;
- border: 1px solid var(--glass-brd); color: var(--fg-dim);
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro);
+ padding: 0.22rem 0.5rem;
+ border: 1px solid var(--rule); color: var(--fg-dim);
text-transform: uppercase;
}
-.badge--beginner { color: var(--foam); border-color: color-mix(in oklab, var(--foam) 45%, transparent); }
-.badge--intermediate { color: var(--gold); border-color: color-mix(in oklab, var(--gold) 45%, transparent); }
-.badge--advanced { color: var(--love); border-color: color-mix(in oklab, var(--love) 45%, transparent); }
+.badge svg { width: 12px; height: 12px; }
+.badge--beginner { color: var(--foam); border-color: color-mix(in oklab, var(--foam) 50%, transparent); }
+.badge--intermediate { color: var(--gold); border-color: color-mix(in oklab, var(--gold) 50%, transparent); }
+.badge--advanced { color: var(--love); border-color: color-mix(in oklab, var(--love) 50%, transparent); }
.tag-pill {
- font-family: var(--font-mono); font-size: 0.72rem;
- padding: 0.18rem 0.55rem; border-radius: 999px;
- border: 1px solid var(--glass-brd); color: var(--fg-dim);
- transition: color 0.18s, border-color 0.18s, background 0.18s;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro);
+ text-transform: uppercase;
+ padding: 0.24rem 0.55rem;
+ border: 1px solid var(--rule); color: var(--fg-dim);
+ transition: color 180ms ease, border-color 180ms ease, background-color 220ms ease;
}
-.tag-pill:hover { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 50%, transparent); }
+.tag-pill:hover { color: var(--base); background: var(--fg); border-color: var(--fg); }
.tags-row { display: flex; flex-wrap: wrap; gap: 0.4rem; }
-/* ---- Category / sheet cards --------------------------------------------- */
-.grid-cards {
+/* ---- Domain plates ------------------------------------------------------- */
+/* The twelve domains, as tiles. A tile is a slab with its hue on the top
+ edge and nowhere else: twelve fully-coloured cards would be a paint
+ chart, and the point of the colour is to key the domain, not to fill
+ the grid. */
+.grid-plates {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(min(100%, 270px), 1fr));
- gap: 1.1rem;
-}
-.card { display: flex; flex-direction: column; min-height: 100%; }
-.card .pane__body { display: flex; flex-direction: column; gap: 0.55rem; flex: 1; }
-.card-title { font-family: var(--font-display); font-size: var(--step-1); font-weight: 700; }
-.card-desc { color: var(--fg-dim); font-size: 0.9rem; line-height: 1.5; }
-.card-meta { margin-top: auto; display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding-top: 0.4rem; }
-.card-count { font-family: var(--font-mono); font-size: 0.78rem; color: var(--fg-faint); }
-.card-arrow { color: var(--acc, var(--accent)); transition: transform 0.25s var(--ease); }
-.card:hover .card-arrow { transform: translateX(4px); }
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr));
+ gap: 1px;
+ background: var(--rule);
+ border: 1px solid var(--rule);
+}
+/* One-pixel gaps over an ink ground rather than real gutters: the grid
+ draws its own rules, so the twelve tiles read as one ruled table
+ instead of as twelve floating objects. */
+.plate-tile {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 1.15rem 1.2rem 1.25rem;
+ background: var(--base);
+ border-top: 2px solid var(--acc);
+ transition: background-color 240ms ease;
+}
+.plate-tile:hover { background: var(--wash-strong); }
+.plate-tile__head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.6rem;
+}
+.plate-tile__tag {
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-wide); text-transform: uppercase;
+ color: var(--acc);
+}
+.plate-tile__n {
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); color: var(--fg-faint);
+}
+.plate-tile__title {
+ font-family: var(--font-display);
+ font-size: 1.32rem;
+ font-weight: 700;
+ font-stretch: 84%;
+ letter-spacing: -0.03em;
+ line-height: 0.98;
+ text-transform: uppercase;
+ color: var(--fg);
+}
+.plate-tile__blurb { color: var(--fg-dim); font-size: 0.875rem; line-height: 1.5; }
+.plate-tile__meta {
+ margin-top: auto;
+ padding-top: 0.7rem;
+ display: flex; align-items: center; justify-content: space-between;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
+ color: var(--fg-faint);
+}
+.plate-tile__arrow { color: var(--acc); transition: transform 250ms var(--ease); }
+.plate-tile:hover .plate-tile__arrow { transform: translateX(4px); }
+
+/* ---- Record rows --------------------------------------------------------- */
+/* The archive's core object: number, kind mark, title, meta — one line,
+ not a card. A title set as a display heading gives every entry the
+ weight of a section and makes a list of fifty unscannable; the point of
+ an index is the shape of the whole list, and that only reads if a row
+ is a row. */
+.index { border-top: 1px solid var(--rule); }
+.record {
+ display: block;
+ border-bottom: 1px solid var(--rule);
+ padding: 1.05rem clamp(0.6rem, 1.6vw, 1.3rem);
+ color: var(--fg);
+}
+.record:hover { color: var(--base); }
+/* Every cell placed explicitly rather than left to auto-flow. Narrow, the
+ row is two lines — number, title and chevron across the top, kind
+ underneath — and at md it collapses to the single line the index is
+ really for. Auto-placement cannot express both: the title has to take
+ the full middle on mobile, and the moment it does the chevron is pushed
+ onto a line of its own. */
+.record__grid {
+ display: grid;
+ grid-template-columns: 30px minmax(0, 1fr) auto;
+ align-items: center;
+ column-gap: 1.05rem;
+ row-gap: 0.35rem;
+}
+@media (min-width: 768px) {
+ .record__grid { grid-template-columns: 42px 112px minmax(0, 1fr) 108px 18px; row-gap: 0; }
+}
+.record__n {
+ grid-column: 1; grid-row: 1;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
+ color: var(--accent);
+}
+.record:hover .record__n { color: rgba(250, 244, 237, 0.78); }
+.record__kind {
+ grid-column: 2; grid-row: 2;
+ display: flex; align-items: center; gap: 0.45rem;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
+ color: var(--acc, var(--fg-faint));
+}
+@media (min-width: 768px) { .record__kind { grid-row: 1; } }
+/* On the hover ink the dawn accents sit at roughly 1:1, so every one of
+ them takes a cream weight rather than keeping a hue nobody could read.
+ /78 is the floor for the meta layer — cream on ink clears AA at /74 and
+ these are 10px, so they get a little over the line rather than sitting
+ on it. */
+.record:hover .record__kind,
+.record:hover .record__meta { color: rgba(250, 244, 237, 0.78); }
+.record__kind svg { width: 12px; height: 12px; }
+.record__title {
+ grid-column: 2; grid-row: 1;
+ font-family: var(--font-display);
+ font-size: 1.02rem;
+ font-weight: 600;
+ letter-spacing: -0.015em;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+@media (min-width: 768px) { .record__title { grid-column: 3; } }
+.record__meta {
+ display: none;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
+ color: var(--fg-faint);
+}
+@media (min-width: 768px) { .record__meta { display: block; grid-column: 4; grid-row: 1; } }
+.record__arrow {
+ grid-column: 3; grid-row: 1;
+ color: var(--fg-faint);
+ transition: transform 250ms var(--ease);
+}
+@media (min-width: 768px) { .record__arrow { grid-column: 5; } }
+.record:hover .record__arrow { color: rgba(250, 244, 237, 0.78); transform: translateX(3px); }
+.record__arrow svg { width: 15px; height: 15px; }
/* ---- Hero ---------------------------------------------------------------- */
-.hero { position: relative; margin-top: clamp(1.5rem, 4vw, 3rem); }
-/* The hero is a plate: a solid Rosé Pine band on the cream page, with the
- main site's offset poster-slab shadow. */
-.hero.plate {
+/* The lockup over the ledger. Full-bleed cream with an ink rule under it,
+ which is the page's hardest value break and the reason the marquee
+ below needs no top edge of its own. */
+.hero {
+ position: relative;
+ overflow: hidden;
+ border-bottom: 1px solid var(--fg);
+ background: var(--surface);
+}
+.hero__inner { padding-top: clamp(1.6rem, 3.4vw, 2.6rem); }
+/* Sized in `cqw` against the text column rather than in `vw` against the
+ window, so the lockup fills the space it actually has. In `vw` it is
+ sized against a viewport the column stops tracking at `--maxw`, and past
+ that width the type and the rule under it drift out of line with the
+ rest of the page. */
+.hero__lockup { container-type: inline-size; }
+.hero__line { height: 18.1cqw; }
+.hero__word {
+ font-family: var(--font-lockup);
+ font-size: 21cqw;
+ line-height: 0.86;
+ font-weight: 600;
+ letter-spacing: 0.012em;
+ color: var(--fg);
+}
+@media (prefers-reduced-motion: no-preference) {
+ /* The line keeps settling after the letters have landed: the glyphs
+ finish at 780ms, the tracking at 1500ms. */
+ .hero__word { animation: dmn-track-in 1500ms var(--ease-in) both; }
+ .hero__glyph { animation: dmn-up 780ms var(--ease-in) both; }
+}
+.hero__glyph {
+ display: inline-block;
+ transition: color 220ms ease;
+}
+/* Six letters, six accents, taken under the pointer rather than worn at
+ rest — the lockup teaches the key the rest of the site is coloured by,
+ but only to someone who goes looking. */
+.hero__glyph:nth-child(1):hover { color: var(--pine); }
+.hero__glyph:nth-child(2):hover { color: var(--iris); }
+.hero__glyph:nth-child(3):hover { color: var(--love); }
+.hero__glyph:nth-child(4):hover { color: var(--rose); }
+.hero__glyph:nth-child(5):hover { color: var(--gold); }
+.hero__glyph:nth-child(6):hover { color: var(--foam); }
+
+/* Ink rule. Timed to land as the last glyph settles — six glyphs at 54ms
+ plus the tail of the 780ms keyframe. Width in `cqw` for the same reason
+ the lockup is: a viewport-relative rule outgrows the column. */
+.hero__rule {
+ display: block;
+ height: 3px;
+ width: 64cqw;
+ margin-top: 0.6cqw;
+ transform-origin: 0 50%;
+ background: var(--pine);
+}
+@media (prefers-reduced-motion: no-preference) {
+ .hero__rule { animation: dmn-draw 720ms var(--ease-in) 780ms both; }
+}
+.hero__subline { height: 7cqw; margin-top: 0.9cqw; }
+.hero__sub {
+ font-family: var(--font-display);
+ font-size: 5.5cqw;
+ line-height: 0.82;
+ font-weight: 700;
+ font-stretch: 62%;
+ letter-spacing: 0.045em;
+ text-transform: uppercase;
+ color: var(--fg);
+ transform: scaleY(1.5);
+}
+/* One gesture rather than letter by letter: it is a subtitle, and two
+ per-glyph runs in a row read as an effect rather than as an entrance. */
+.hero__sub > span { display: inline-block; }
+@media (prefers-reduced-motion: no-preference) {
+ .hero__sub > span { animation: dmn-wipe-in 860ms cubic-bezier(0.22, 0.61, 0.36, 1) 380ms both; }
+}
+
+.hero__ledger {
+ margin-top: clamp(1.4rem, 3vw, 2.4rem);
+ padding-bottom: clamp(2rem, 4vw, 3rem);
+}
+.hero__card {
+ display: grid;
+ gap: 1.6rem 2.6rem;
+ border: 1px solid var(--fg);
+ background: var(--base);
+ padding: clamp(1.3rem, 2.4vw, 1.9rem);
+}
+@media (min-width: 720px) { .hero__card { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } }
+.hero__eyebrow {
+ font-family: var(--font-mono); font-size: 10.5px;
+ letter-spacing: 0.2em; text-transform: uppercase; color: var(--pine);
+}
+.hero__h2 {
+ margin-top: 0.85rem;
+ font-family: var(--font-display);
+ font-size: clamp(1.6rem, 2.5vw, 2.4rem);
+ line-height: 0.94;
+ font-weight: 700;
+ font-stretch: 84%;
+ letter-spacing: -0.03em;
+ text-transform: uppercase;
+}
+.hero__blurb { margin-top: 0.9rem; max-width: 46ch; color: var(--fg); font-size: 0.95rem; line-height: 1.5; }
+.hero__actions { margin-top: 1.2rem; display: flex; flex-wrap: wrap; gap: 0.6rem; }
+
+.hero__stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(94px, 1fr));
+ gap: 1rem 1.3rem;
+ align-content: start;
+ border-top: 1px solid var(--fg);
+ padding-top: 1.1rem;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ line-height: 1.6;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+.hero__stat b { font-weight: 400; }
+.hero__stat--iris b { color: var(--iris); }
+.hero__stat--love b { color: var(--love); }
+.hero__stat--gold b { color: var(--gold); }
+
+/* The shell readout beside the card. Borderless on purpose: the card is
+ the printed spec, this is the terminal output next to it, so the two
+ read as different registers rather than as two boxes. */
+.whoami { font-family: var(--font-mono); font-size: 12.5px; line-height: 1.6; max-width: 58ch; }
+.whoami__cmd { display: flex; align-items: center; gap: 0.5rem; color: var(--fg-faint); flex-wrap: wrap; }
+.whoami__cmd .u { color: var(--pine); }
+.whoami__cmd .c { color: var(--fg); }
+.whoami__caret {
+ display: inline-block; width: 0.5em; height: 1.05em;
+ background: color-mix(in oklab, var(--fg) 80%, transparent);
+ transform: translateY(0.12em);
+}
+@media (prefers-reduced-motion: no-preference) {
+ .whoami__caret { animation: dmn-blink 1.1s steps(1, end) infinite; }
+}
+.whoami__rows {
+ margin-top: 0.9rem;
+ padding-top: 0.9rem;
+ border-top: 1px solid var(--rule-faint);
+ display: grid;
+ grid-template-columns: 4.2rem 1fr;
+ gap: 0.45rem 1.2rem;
+ color: var(--fg-dim);
+}
+.whoami__rows dt { color: var(--fg-faint); }
+.whoami__rows dd { display: flex; align-items: center; gap: 0.5rem; }
+
+/* ---- Marquee band -------------------------------------------------------- */
+/* The index grammar run sideways. The band is a ground, not an accent —
+ see the `--band` note in tokens.css.
+
+ The bottom edge is the one at risk: it meets the page's next section at
+ too close a value to carry itself, which is why the ink rule below is
+ load-bearing rather than decorative. Do not drop it. */
+.marquee {
+ position: relative;
+ overflow: hidden;
+ background: var(--band);
+ /* Ink, written out rather than taken from `--fg`: the band pins the
+ night palette for its contents, which would flip this rule to a light
+ one. The edge below meets the cream page and the black tabs meet the
+ section under them, and in both cases it is a *dark* rule that keeps
+ the seam from bleeding. */
+ border-bottom: 1px solid #575279;
+ --marquee-fg: #faf4ed;
+ /* /78, not /62. Cream on this band clears AA at /74, and these are the
+ smallest words on the page *and* the only ones in motion, so they get
+ a little over the line rather than sitting on it. At /62 the index
+ numbers and sheet counts were legible in a still frame and mush while
+ the band was moving. */
+ --marquee-dim: rgba(250, 244, 237, 0.78);
+
+ /* Pine, lifted for this band only.
+ Every other night accent clears its floor on `--band`: iris 4.75,
+ foam 5.83, gold 6.06, rose 5.88, love 3.42 — all at or above the 3:1
+ large-text floor the display titles need. Published night pine
+ (#3e8fb0) lands at 2.73 and is the one that fails, because it is the
+ accent nearest the ground in both hue and value.
+ #6fb3d0 is the same hue walked up until it clears with headroom
+ (4.28:1) while staying visibly pine rather than sliding into foam,
+ which sits beside it in the rotation and would collapse two domains
+ onto one colour. */
+ --pine: #6fb3d0;
+}
+.marquee__track {
+ display: flex;
+ width: max-content;
+ will-change: transform;
+}
+/* Two identical runs translated as one unit to -50%, which lands the
+ second exactly where the first started — the reset is invisible. */
+.marquee__run {
+ display: flex;
+ flex: none;
+ align-items: center;
+}
+.marquee__item {
+ display: flex;
+ align-items: baseline;
+ gap: 0.7rem;
+ padding: 0.95rem clamp(1.1rem, 2.4vw, 2rem);
+ white-space: nowrap;
+ font-family: var(--font-mono);
+ /* 11px, not the 10px `--step-micro` every other label on the site uses.
+ This is the one place the furniture type is in motion, and a word
+ sliding past at 70px/s needs the extra pixel that a static eyebrow
+ does not. */
+ font-size: 11px;
+ letter-spacing: var(--track-micro);
+ text-transform: uppercase;
+ color: var(--marquee-fg);
+}
+.marquee__n { color: var(--marquee-dim); }
+/* The kind *word* stays cream and only its icon takes the hue. Colouring
+ both put three competing colours in every entry, which is what made the
+ band read as noise rather than as an index. */
+.marquee__kind { color: var(--marquee-fg); display: inline-flex; align-items: center; gap: 0.4rem; }
+.marquee__kind svg { width: 11px; height: 11px; color: var(--acc); }
+/* The credential in display type, the qualifier trailing in the date slot
+ — record-row grammar, horizontally. */
+.marquee__title {
+ font-family: var(--font-display);
+ font-size: 1.06rem;
+ font-weight: 700;
+ font-stretch: 84%;
+ letter-spacing: -0.02em;
+ text-transform: uppercase;
+ color: var(--acc);
+}
+.marquee__qual { color: var(--marquee-dim); }
+@media (max-width: 720px) { .marquee__qual { display: none; } }
+
+/* The band's own tab — `^:` survives in exactly one place. It is the
+ banner's self-naming mark, and the band names itself once; repeated on
+ every record it was furniture. The tab counts its records, because every
+ index on this site counts itself. */
+.marquee__tab {
+ position: absolute;
+ top: 0; bottom: 0;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding-inline: clamp(0.9rem, 2.2vw, 1.5rem);
+ /* The tabs are their own ground: Rosé Pine's black, which is the page's
+ furniture ink rather than another colour competing with the band. The
+ band pins the night palette (`.plate`), so `--base` is that black in
+ both modes. */
background: var(--base);
- border-color: rgba(224, 222, 244, 0.12);
- box-shadow: 6px 6px 0 rgba(78, 57, 105, 0.16), var(--glass-shadow);
-}
-:root[data-theme='dark'] .hero.plate { box-shadow: 6px 6px 0 rgba(0, 0, 0, 0.4), var(--glass-shadow); }
-.hero .pane__body { padding: clamp(1.6rem, 4vw, 3.2rem); }
-.hero-grid { display: grid; gap: 2rem; }
-@media (min-width: 880px) { .hero-grid { grid-template-columns: 1.35fr 1fr; align-items: center; } }
-.hero-title {
- font-size: var(--step-4); line-height: 0.98; letter-spacing: -0.02em;
- font-weight: 700; text-wrap: balance;
-}
-.hero-title .hl { color: var(--accent); }
-.hero-title .hl2 { color: var(--accent-2); }
-.hero-sub { color: var(--fg-dim); font-size: var(--step-1); max-width: 46ch; margin-top: 1rem; }
-.hero-actions { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-top: 1.6rem; }
-.hero-stats { display: flex; gap: 1.6rem; margin-top: 1.8rem; flex-wrap: wrap; }
-.hero-stat b { font-family: var(--font-display); font-size: var(--step-2); color: var(--fg); display: block; line-height: 1; }
-.hero-stat span { font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--fg-faint); }
-
-/* the mock terminal on the hero side */
-.term { font-family: var(--font-mono); font-size: 0.84rem; line-height: 1.7; }
-.term .line { white-space: pre-wrap; word-break: break-word; }
-.term .p { color: var(--accent); }
-.term .c { color: var(--fg); }
-.term .o { color: var(--fg-faint); }
-.term .k { color: var(--accent-2); }
-.term .cursor { display: inline-block; width: 0.55em; height: 1.05em; background: var(--accent); vertical-align: -0.16em; }
-@media (prefers-reduced-motion: no-preference) { .term .cursor { animation: blink 1.15s steps(1) infinite; } }
+ color: #faf4ed;
+ font-family: var(--font-mono);
+ font-size: var(--step-micro);
+ letter-spacing: var(--track-wide);
+ text-transform: uppercase;
+}
+.marquee__tab--l { left: 0; }
+.marquee__tab--r { right: 0; }
+@media (max-width: 640px) { .marquee__tab--r { display: none; } }
+.marquee__tab .dot { color: #9ccfd8; }
+
+/* ---- Section banner ------------------------------------------------------ */
+/* Dark plate, live noise field, left scrim. Legibility on the plate is
+ carried by two layers, not one: the canvas erases its own top and bottom
+ bands, AND this gradient sits over the noise, under the content. */
+.banner {
+ position: relative;
+ overflow: hidden;
+ background: var(--plate);
+ border-bottom: 1px solid var(--fg);
+}
+.banner__field { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
+.banner__scrim {
+ position: absolute;
+ top: 0; bottom: 0; left: 0;
+ width: 68%;
+ pointer-events: none;
+ background: linear-gradient(
+ 90deg,
+ rgb(var(--plate-scrim) / 0.95) 0%,
+ rgb(var(--plate-scrim) / 0.86) 52%,
+ rgb(var(--plate-scrim) / 0) 100%
+ );
+}
+.banner__body {
+ position: relative;
+ z-index: 2;
+ display: grid;
+ gap: 1.8rem 2.6rem;
+ align-items: end;
+ padding-block: clamp(1.5rem, 3.2vw, 2.2rem);
+}
+@media (min-width: 900px) { .banner__body { grid-template-columns: minmax(0, 1fr) 280px; } }
+.banner__title {
+ margin-top: 0.85rem;
+ font-family: var(--font-display);
+ font-size: clamp(1.9rem, 4.2vw, 3.2rem);
+ line-height: 0.92;
+ font-weight: 700;
+ font-stretch: 82%;
+ letter-spacing: -0.035em;
+ text-transform: uppercase;
+ color: var(--fg);
+}
+.banner__blurb { margin-top: 0.85rem; max-width: 58ch; color: var(--fg-dim); font-size: 0.95rem; line-height: 1.55; }
+.banner__ledger {
+ display: grid;
+ gap: 0.55rem;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: var(--track-micro);
+ text-transform: uppercase;
+}
+.banner__row {
+ display: flex; align-items: baseline; justify-content: space-between;
+ gap: 1rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--rule);
+ color: var(--fg-faint);
+}
+.banner__row b { font-weight: 400; color: var(--fg); }
+.banner__row--acc b { color: var(--acc, var(--accent)); }
/* ---- Section headers ----------------------------------------------------- */
-.section { margin-top: clamp(2.6rem, 6vw, 4.5rem); }
-.section-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 1rem; margin-bottom: 1.3rem; }
-.section-head h2 { font-size: var(--step-2); }
-.section-head .eyebrow { margin-bottom: 0.4rem; }
+.section { margin-top: clamp(2.4rem, 5.5vw, 4rem); }
+.section:last-child { padding-bottom: clamp(2.5rem, 6vw, 4.5rem); }
+.section-head {
+ display: flex; align-items: flex-end; justify-content: space-between;
+ gap: 1rem; margin-bottom: 1.2rem;
+}
+.section-head h2 {
+ margin-top: 0.6rem;
+ font-family: var(--font-display);
+ font-size: clamp(1.5rem, 2.8vw, 2.2rem);
+ font-weight: 700;
+ font-stretch: 84%;
+ letter-spacing: -0.03em;
+ line-height: 0.94;
+ text-transform: uppercase;
+}
+
+/* ---- Payloads call-to-action band ---------------------------------------- */
+.cta-band {
+ display: block;
+ background: var(--band-ledger);
+ border-block: 1px solid var(--fg);
+ color: #faf4ed;
+}
+.cta-band__body {
+ display: flex; align-items: center; justify-content: space-between;
+ gap: 1.6rem; flex-wrap: wrap;
+ padding-block: clamp(1.5rem, 3.2vw, 2.3rem);
+}
+.cta-band__eyebrow {
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-wide); text-transform: uppercase;
+ color: rgba(250, 244, 237, 0.72);
+}
+.cta-band h2 {
+ margin-top: 0.6rem;
+ font-family: var(--font-display);
+ font-size: clamp(1.7rem, 3.4vw, 2.6rem);
+ font-weight: 700; font-stretch: 82%;
+ letter-spacing: -0.035em; line-height: 0.94;
+ text-transform: uppercase;
+ color: #faf4ed;
+}
+.cta-band p { margin-top: 0.7rem; max-width: 54ch; color: rgba(250, 244, 237, 0.85); font-size: 0.93rem; line-height: 1.55; }
+.cta-band .btn {
+ background: transparent; color: #faf4ed;
+ border-color: rgba(250, 244, 237, 0.5);
+ flex: none;
+}
+.cta-band:hover .btn { background: #faf4ed; color: var(--band-ledger); border-color: #faf4ed; }
/* ---- Sheet page ---------------------------------------------------------- */
-.sheet-head { margin-top: 1.5rem; }
-.breadcrumb { font-family: var(--font-mono); font-size: 0.78rem; color: var(--fg-faint); display: flex; gap: 0.4rem; flex-wrap: wrap; align-items: center; }
+/* Same measure as the body below it, so the title, the rule under the
+ eyebrow and the prose all start and end on the same two lines. */
+.sheet-head { margin-top: 1.6rem; max-width: 1080px; }
+.breadcrumb {
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
+ color: var(--fg-faint);
+ display: flex; gap: 0.45rem; flex-wrap: wrap; align-items: center;
+}
.breadcrumb a:hover { color: var(--accent); }
-.breadcrumb .sep { color: var(--fg-faint); opacity: 0.5; }
-.sheet-title { font-size: var(--step-3); margin-top: 0.8rem; }
-.sheet-meta { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-top: 0.9rem; }
-.sheet-desc { color: var(--fg-dim); font-size: var(--step-1); margin-top: 0.9rem; max-width: 70ch; }
+.breadcrumb .sep { opacity: 0.45; }
+.sheet-title {
+ margin-top: 0.9rem;
+ font-family: var(--font-display);
+ font-size: clamp(1.9rem, 4vw, 3rem);
+ font-weight: 700; font-stretch: 84%;
+ letter-spacing: -0.035em; line-height: 0.94;
+ text-transform: uppercase;
+}
+.sheet-meta { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-top: 1rem; }
+.sheet-desc { color: var(--fg-dim); font-size: var(--step-1); margin-top: 1rem; max-width: 70ch; }
-.sheet-layout { display: grid; grid-template-columns: 1fr; gap: 2rem; margin-top: 2rem; }
+/* The reading measure, not the archive measure. `--maxw` is 1440px because
+ the index pages are built out of full-bleed bands and ruled tables; a
+ reading column laid out on it leaves a dead third between the prose and
+ the contents rail. The sheet pages get their own, narrower one. */
+.sheet-layout {
+ display: grid; grid-template-columns: 1fr;
+ gap: 2.5rem; margin-top: 2.2rem;
+ max-width: 1080px;
+}
@media (min-width: 1040px) { .sheet-layout { grid-template-columns: minmax(0, 1fr) 220px; } }
-.toc { position: sticky; top: 70px; align-self: start; max-height: calc(100vh - 90px); overflow-y: auto; display: none; }
+.toc { position: sticky; top: 78px; align-self: start; max-height: calc(100vh - 100px); overflow-y: auto; display: none; }
@media (min-width: 1040px) { .toc { display: block; } }
-.toc h4 { font-family: var(--font-mono); font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-faint); margin-bottom: 0.6rem; }
-.toc a { display: block; padding: 0.24rem 0.6rem; border-left: 2px solid var(--glass-brd); color: var(--fg-faint); font-size: 0.82rem; line-height: 1.4; transition: color 0.18s, border-color 0.18s; }
+.toc h4 {
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-wide); text-transform: uppercase;
+ color: var(--fg-faint); margin-bottom: 0.7rem;
+ padding-bottom: 0.5rem; border-bottom: 1px solid var(--rule);
+}
+.toc a {
+ display: block; padding: 0.26rem 0 0.26rem 0.7rem;
+ border-left: 2px solid var(--rule);
+ color: var(--fg-faint); font-size: 0.82rem; line-height: 1.4;
+ transition: color 180ms ease, border-color 180ms ease;
+}
.toc a:hover, .toc a.active { color: var(--accent); border-left-color: var(--accent); }
-.toc a.lvl-3 { padding-left: 1.2rem; font-size: 0.78rem; }
+.toc a.lvl-3 { padding-left: 1.3rem; font-size: 0.78rem; }
-.prevnext { display: grid; gap: 1rem; margin-top: 2.5rem; }
+.prevnext { display: grid; gap: 1px; margin-top: 3rem; background: var(--rule); border: 1px solid var(--rule); }
@media (min-width: 620px) { .prevnext { grid-template-columns: 1fr 1fr; } }
-.prevnext a { padding: 1rem 1.1rem; }
-.prevnext .dir { font-family: var(--font-mono); font-size: 0.72rem; color: var(--fg-faint); letter-spacing: 0.1em; text-transform: uppercase; }
+.prevnext a { padding: 1.1rem 1.2rem; background: var(--base); transition: background-color 240ms ease; }
+.prevnext a:hover { background: var(--wash-strong); }
+.prevnext .dir {
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ color: var(--fg-faint); letter-spacing: var(--track-wide); text-transform: uppercase;
+}
.prevnext .nx { text-align: right; }
-.prevnext strong { display: block; margin-top: 0.3rem; color: var(--fg); font-family: var(--font-display); }
+.prevnext strong {
+ display: block; margin-top: 0.4rem; color: var(--fg);
+ font-family: var(--font-display); font-weight: 600; letter-spacing: -0.015em;
+}
/* ---- Footer -------------------------------------------------------------- */
-.site-footer { margin-top: clamp(3rem, 8vw, 6rem); border-top: 1px solid var(--glass-brd); padding: 2.4rem 0; color: var(--fg-faint); }
-/* Footer is a plate too — a dark band closing the cream page. */
-.site-footer.plate { background: var(--base); border-top-color: rgba(224, 222, 244, 0.1); }
-.site-footer .wrap { display: flex; flex-wrap: wrap; gap: 1rem 2rem; align-items: center; justify-content: space-between; }
+/* A dark band closing the cream page — the same object as the marquee and
+ the banners, in its last position. */
+.site-footer {
+ position: relative;
+ overflow: hidden;
+ margin-top: clamp(3rem, 7vw, 5.5rem);
+ border-top: 1px solid var(--fg);
+ padding-block: clamp(2rem, 4vw, 3rem);
+ background: var(--base);
+ color: var(--fg-dim);
+}
+.site-footer .wrap {
+ display: grid; gap: 1.6rem 2.5rem;
+ align-items: start;
+}
+@media (min-width: 760px) { .site-footer .wrap { grid-template-columns: minmax(0, 1fr) auto; } }
.site-footer a:hover { color: var(--accent); }
-.footer-note { font-size: 0.82rem; max-width: 60ch; line-height: 1.5; }
-.footer-mono { font-family: var(--font-mono); font-size: 0.76rem; letter-spacing: 0.04em; }
+.footer-note { font-size: 0.86rem; max-width: 56ch; line-height: 1.55; margin-top: 0.8rem; }
+.footer-mono {
+ display: block;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
+ line-height: 1.9;
+}
+@media (min-width: 760px) { .footer-col { text-align: right; } }
/* ---- PDF embed ----------------------------------------------------------- */
-.pdf-frame { width: 100%; height: min(80vh, 900px); border: 0; border-radius: var(--radius-sm); background: var(--code-bg); }
+.pdf-frame { width: 100%; height: min(80vh, 900px); border: 1px solid var(--rule); background: var(--code-bg); }
/* ---- Empty / utility ----------------------------------------------------- */
.muted { color: var(--fg-faint); }
@@ -267,78 +860,57 @@ h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 700; line-height
.mono { font-family: var(--font-mono); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
-/* ---- Header nav ---------------------------------------------------------- */
-.header-nav { display: flex; align-items: center; gap: 1rem; margin-left: 0.4rem; }
-.header-nav a {
- font-family: var(--font-mono);
- font-size: 0.86rem;
+/* ---- Category chips ------------------------------------------------------ */
+.cat-nav { display: flex; flex-wrap: wrap; gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
+.cat-chip {
+ display: inline-flex; align-items: center; gap: 0.45rem;
+ padding: 0.5rem 0.8rem;
+ background: var(--base);
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
color: var(--fg-dim);
- padding: 0.2rem 0;
- border-bottom: 1.5px solid transparent;
- transition: color 0.18s, border-color 0.18s;
+ transition: background-color 220ms ease, color 200ms ease;
}
-.header-nav a:hover { color: var(--accent); }
-.header-nav a[aria-current='page'] { color: var(--fg); border-color: var(--accent); }
-@media (max-width: 560px) { .header-nav { display: none; } }
+.cat-chip:hover { background: var(--wash-strong); color: var(--fg); }
+.cat-chip__tag { color: var(--acc); }
+.cat-chip[aria-current='page'] { background: var(--acc); color: var(--base); }
+.cat-chip[aria-current='page'] .cat-chip__tag { color: var(--base); }
-/* ---- Payloads attribution banner + mirror badge -------------------------- */
+/* ---- PayloadsAllTheThings attribution ------------------------------------ */
.patt-credit {
- display: flex;
- align-items: center;
- gap: 0.7rem;
- flex-wrap: wrap;
- margin: 0.2rem 0 1.6rem;
- padding: 0.6rem 0.85rem;
- border: 1px solid var(--glass-brd);
+ display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap;
+ margin: 0.2rem 0 1.8rem;
+ padding: 0.7rem 0.9rem;
+ border: 1px solid var(--rule);
border-left: 2px solid var(--iris);
- border-radius: var(--radius-sm, 8px);
- background: color-mix(in oklab, var(--iris) 8%, var(--glass-bg));
- -webkit-backdrop-filter: blur(8px);
- backdrop-filter: blur(8px);
- font-size: 0.84rem;
+ background: var(--wash);
+ font-size: 0.86rem;
}
.patt-badge {
- font-family: var(--font-mono);
- font-size: 0.66rem;
- letter-spacing: 0.14em;
- text-transform: uppercase;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
color: var(--iris);
border: 1px solid color-mix(in oklab, var(--iris) 50%, transparent);
- border-radius: 4px;
- padding: 0.12rem 0.4rem;
+ padding: 0.14rem 0.4rem;
flex: none;
}
.patt-credit__text { color: var(--fg-dim); line-height: 1.5; }
.patt-credit__text a { color: var(--iris); border-bottom: 1px solid color-mix(in oklab, var(--iris) 40%, transparent); }
.patt-credit__text a:hover { border-color: var(--iris); }
-.patt-lic { display: inline-block; margin-left: 0.5rem; color: var(--fg-faint); font-family: var(--font-mono); font-size: 0.76rem; }
+.patt-lic { display: inline-block; margin-left: 0.5rem; color: var(--fg-faint); font-family: var(--font-mono); font-size: 0.78rem; }
.patt-lic code { background: none; color: var(--fg-faint); }
.patt-src {
margin-left: auto;
- display: inline-flex;
- align-items: center;
- gap: 0.35rem;
- font-family: var(--font-mono);
- font-size: 0.8rem;
+ display: inline-flex; align-items: center; gap: 0.35rem;
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro); text-transform: uppercase;
color: var(--fg-dim);
- padding: 0.3rem 0.6rem;
- border: 1px solid var(--glass-brd);
- border-radius: 7px;
- transition: color 0.18s, border-color 0.18s;
+ padding: 0.32rem 0.6rem;
+ border: 1px solid var(--rule);
+ transition: color 180ms ease, border-color 180ms ease;
}
.patt-src:hover { color: var(--iris); border-color: color-mix(in oklab, var(--iris) 50%, transparent); }
-.payloads-cta { display: block; text-decoration: none; }
-.payloads-cta.plate { background: var(--base); border-color: rgba(224, 222, 244, 0.12); box-shadow: 5px 5px 0 rgba(78, 57, 105, 0.14), var(--glass-shadow); }
-:root[data-theme='dark'] .payloads-cta.plate { box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.35), var(--glass-shadow); }
-.payloads-cta__body {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 1.5rem;
- flex-wrap: wrap;
-}
-
/* ---- Theme toggle: circular-reveal wipe ---------------------------------- */
/* Scoped to .theme-vt so page navigations keep Astro's default crossfade.
The toggle sets --vt-x/--vt-y (origin) and --vt-r (end radius) in JS. */
diff --git a/src/styles/prose.css b/src/styles/prose.css
@@ -1,30 +1,69 @@
/* ============================================================================
Prose — how a rendered cheatsheet reads.
- Code blocks become terminal panes (enhanced by enhance-code.ts).
+
+ The body is paper. The code is not: every block is a dark plate, in both
+ modes, exactly as the main site sets them. That is the page's core
+ rhythm — terminal output pasted into a printed document — and it is why
+ the blocks do not follow the theme. A code block that turns cream in
+ light mode stops being a terminal and becomes a quotation.
========================================================================== */
-.prose { color: var(--fg); font-size: var(--step-0); line-height: 1.68; max-width: 74ch; }
-.prose > * + * { margin-top: 1.05rem; }
+.prose { color: var(--fg); font-size: 17.5px; line-height: 1.68; letter-spacing: -0.003em; max-width: 78ch; }
+.prose > * + * { margin-top: 1.15rem; }
-.prose h2, .prose h3, .prose h4 { font-family: var(--font-display); line-height: 1.15; scroll-margin-top: 84px; }
+.prose h2, .prose h3, .prose h4 { font-family: var(--font-display); scroll-margin-top: 92px; }
.prose h2 {
- font-size: var(--step-2); margin-top: 2.4rem; padding-bottom: 0.4rem;
- border-bottom: 1px solid var(--glass-brd);
+ font-size: clamp(1.35rem, 2.2vw, 1.75rem);
+ font-weight: 700;
+ font-stretch: 84%;
+ letter-spacing: -0.03em;
+ line-height: 1;
+ text-transform: uppercase;
+ margin-top: 2.6rem;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid var(--fg);
+}
+.prose h3 {
+ font-size: 1.18rem; font-weight: 700; letter-spacing: -0.02em;
+ line-height: 1.15; margin-top: 2rem; color: var(--fg);
+}
+.prose h4 {
+ font-family: var(--font-mono);
+ font-size: 11px; letter-spacing: var(--track-micro); text-transform: uppercase;
+ margin-top: 1.6rem; color: var(--fg-faint);
}
-.prose h2::before { content: '// '; color: var(--accent); font-family: var(--font-mono); font-size: 0.7em; opacity: 0.8; }
-.prose h3 { font-size: var(--step-1); margin-top: 1.8rem; color: var(--fg); }
-.prose h4 { font-size: 1.02rem; margin-top: 1.4rem; color: var(--fg-dim); }
-.prose p { color: var(--fg); }
+.prose p { color: var(--fg); text-wrap: pretty; }
.prose strong { color: var(--fg); font-weight: 600; }
.prose em { color: var(--fg-dim); }
+/* ---- Drop cap ------------------------------------------------------------ */
+/* The opening letter of a sheet, floated three lines deep in EB Garamond.
+ Set on a span rather than with `::first-letter` for two reasons: the
+ pseudo-element cannot be given a different family and weight reliably
+ across engines at this size, and — the real one — the letter stays real
+ selectable, searchable text this way, read as part of the word it opens
+ rather than as decoration sitting beside it.
+
+ The rehype pass that splits it out (see `rehypeDropCap` in
+ astro.config.mjs) skips short openers, so an image caption or a
+ one-line note is left alone. */
+.prose .cap {
+ float: left;
+ font-family: var(--font-lockup);
+ font-size: 72px;
+ font-weight: 600;
+ line-height: 0.74;
+ color: var(--love);
+ margin: 5px 0.85rem 0 0;
+}
+
.prose a {
color: var(--accent);
text-decoration: underline;
text-decoration-color: color-mix(in oklab, var(--accent) 40%, transparent);
text-underline-offset: 3px;
- transition: text-decoration-color 0.2s;
+ transition: text-decoration-color 200ms ease;
}
.prose a:hover { text-decoration-color: var(--accent); }
@@ -33,108 +72,100 @@
.prose ul li::marker { color: var(--accent); }
.prose ol li::marker { color: var(--fg-faint); font-family: var(--font-mono); }
-.prose hr { border: 0; border-top: 1px solid var(--glass-brd); margin: 2rem 0; }
+.prose hr { border: 0; border-top: 1px solid var(--rule); margin: 2.4rem 0; }
-/* Inline code */
+/* Inline code — a tinted slug on the page, square, hairline-bordered. This
+ is the one place the paper carries code, and it stays paper-coloured;
+ the dark treatment is reserved for blocks. */
.prose :not(pre) > code {
font-family: var(--font-mono);
font-size: 0.86em;
- padding: 0.12em 0.4em;
- border-radius: 6px;
+ padding: 0.14em 0.4em;
background: var(--code-bg);
- border: 1px solid var(--glass-brd);
+ border: 1px solid var(--rule);
color: var(--rose);
word-break: break-word;
}
/* Blockquotes / callouts */
.prose blockquote {
- border-left: 3px solid var(--accent-2);
- background: var(--glass-bg);
- padding: 0.8rem 1.1rem;
- border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
+ border-left: 2px solid var(--accent-2);
+ background: var(--wash);
+ padding: 0.9rem 1.1rem;
color: var(--fg-dim);
}
.prose blockquote p { margin-top: 0.4rem; }
.prose blockquote > :first-child { margin-top: 0; }
-/* Tables */
-.prose .table-scroll { overflow-x: auto; border-radius: var(--radius-sm); border: 1px solid var(--glass-brd); }
+/* Tables — a ruled table, not a card with a table in it. */
+.prose .table-scroll { overflow-x: auto; border: 1px solid var(--rule); }
.prose table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
.prose thead th {
- background: var(--glass-bg-strong);
- font-family: var(--font-mono); font-size: 0.76rem; letter-spacing: 0.04em;
+ background: var(--wash-strong);
+ font-family: var(--font-mono); font-size: var(--step-micro);
+ letter-spacing: var(--track-micro);
text-transform: uppercase; color: var(--fg-dim);
- text-align: left; padding: 0.6rem 0.8rem; white-space: nowrap;
+ text-align: left; padding: 0.62rem 0.8rem; white-space: nowrap;
+ border-bottom: 1px solid var(--rule);
}
-.prose tbody td { padding: 0.55rem 0.8rem; border-top: 1px solid var(--glass-brd); vertical-align: top; }
-.prose tbody tr:hover { background: var(--glass-bg); }
+.prose tbody td { padding: 0.58rem 0.8rem; border-top: 1px solid var(--rule); vertical-align: top; }
+.prose tbody tr:hover { background: var(--wash); }
-.prose img { border-radius: var(--radius-sm); border: 1px solid var(--glass-brd); margin-inline: auto; }
+.prose img { border: 1px solid var(--rule); margin-inline: auto; }
-/* ---- Code panes ---------------------------------------------------------- */
+/* ---- Code blocks --------------------------------------------------------- */
+/* A figure on the plate, with its caption above the code: the language at
+ the left, the copy control at the right, both in the 9.5px mono the rest
+ of the site labels things in. No window chrome, no traffic-light dots —
+ this is a printed listing of terminal output, not a simulated window. */
.code-pane {
position: relative;
- border: 1px solid var(--glass-brd);
- border-radius: var(--radius-sm);
- overflow: hidden;
- background: var(--code-bg);
- -webkit-backdrop-filter: blur(6px);
- backdrop-filter: blur(6px);
+ background: var(--plate);
+ padding: 1.25rem 1.4rem 1.4rem;
+ /* Runs past the prose measure at wide viewports, because a fixed-width
+ terminal listing is the one thing on the page that genuinely needs
+ more than 74ch and wraps badly under it. */
+ max-width: min(100%, 92ch);
}
.code-pane__bar {
- display: flex; align-items: center; gap: 0.55rem;
- padding: 0.4rem 0.7rem;
- border-bottom: 1px solid var(--glass-brd);
- background: linear-gradient(180deg, rgba(255,255,255,0.03), transparent);
+ display: flex; align-items: center; justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 0.9rem;
}
-.code-pane__dots { display: inline-flex; gap: 0.3rem; }
-.code-pane__dots i { width: 8px; height: 8px; border-radius: 50%; display: block; }
-.code-pane__dots i:nth-child(1) { background: var(--love); }
-.code-pane__dots i:nth-child(2) { background: var(--gold); }
-.code-pane__dots i:nth-child(3) { background: var(--foam); }
.code-pane__lang {
- font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.1em;
- text-transform: uppercase; color: var(--fg-faint); margin-left: 0.2rem;
+ font-family: var(--font-mono); font-size: 9.5px;
+ letter-spacing: 0.18em; text-transform: uppercase;
+ color: #c4a7e7;
}
.code-copy {
- margin-left: auto;
- font-family: var(--font-mono); font-size: 0.68rem; letter-spacing: 0.06em;
- text-transform: uppercase;
- color: var(--fg-faint);
- padding: 0.2rem 0.5rem; border-radius: 6px;
- border: 1px solid var(--glass-brd);
- transition: color 0.18s, border-color 0.18s;
+ display: inline-flex; align-items: center; gap: 0.45rem;
+ font-family: var(--font-mono); font-size: 9.5px;
+ letter-spacing: 0.18em; text-transform: uppercase;
+ color: #9ccfd8;
+ transition: color 200ms ease;
}
-.code-copy:hover { color: var(--accent); border-color: color-mix(in oklab, var(--accent) 50%, transparent); }
-.code-copy.copied { color: var(--foam); border-color: var(--foam); }
+.code-copy:hover { color: #e0def4; }
+.code-copy.copied { color: #f6c177; }
+.code-copy svg { width: 11px; height: 11px; }
.prose pre.astro-code {
margin: 0;
- padding: 0.95rem 1.1rem;
+ padding: 0;
overflow-x: auto;
- font-size: 0.84rem;
- line-height: 1.6;
+ font-size: 13px;
+ line-height: 1.7;
background: transparent !important;
+ color: #e0def4;
}
-.prose pre.astro-code code { font-family: var(--font-mono); display: block; }
-
-/* Shiki dual-theme: defaultColor:false -> flip on [data-theme] */
-:root .astro-code,
-:root .astro-code span,
-:root[data-theme='dark'] .astro-code,
-:root[data-theme='dark'] .astro-code span {
- color: var(--shiki-dark);
- font-style: var(--shiki-dark-font-style);
- font-weight: var(--shiki-dark-font-weight);
- text-decoration: var(--shiki-dark-text-decoration);
-}
-:root[data-theme='light'] .astro-code,
-:root[data-theme='light'] .astro-code span {
- color: var(--shiki-light);
- font-style: var(--shiki-light-font-style);
- font-weight: var(--shiki-light-font-weight);
- text-decoration: var(--shiki-light-text-decoration);
+.prose pre.astro-code code {
+ font-family: var(--font-mono);
+ display: block;
+ /* Wrapped rather than scrolled: a cheatsheet's value is the whole
+ command being visible and copyable at a glance, and a long
+ `--script-args` tail hidden behind a horizontal scrollbar is the one
+ thing most likely to be pasted wrong. */
+ white-space: pre-wrap;
+ word-break: break-word;
}
/* First heading of the doc is the page title (rendered separately) */
diff --git a/src/styles/tokens.css b/src/styles/tokens.css
@@ -4,6 +4,23 @@
(night). Type is Archivo + IBM Plex Mono, with an EB Garamond lockup for
the wordmark. Palette values are the contrast-tuned DÆMON dawn/night tokens.
The theme attribute is set on <html> by an inline head script before paint.
+
+ ── Form language ──
+ The site is an editorial-brutalist paper archive, not a glass UI. Three
+ rules follow from that and everything here serves them:
+
+ 1. Zero radius. Corners are square, everywhere, including images and
+ inputs. `--radius*` survive only so that consumers written against
+ them keep compiling; they all resolve to 0.
+ 2. No blur, no glow, no drop shadow. Structure is carried by 1px ink
+ rules (`--rule`), flat panels (`.slab`), and the grain film.
+ 3. Colour separates by *value*, not by translucency. A panel differs
+ from its page because it is a different ink, not because it is
+ frosted.
+
+ Alpha still belongs on borders and washes. It does NOT belong on text:
+ use the ink / subtle / muted ramp, which is tuned to stay legible at
+ every step in both modes.
========================================================================== */
/* ---- Fonts (self-hosted, base-aware via Vite) ---------------------------- */
@@ -35,6 +52,18 @@
--hl-med: #dfdad9;
--hl-high: #cecacd;
+ /* The published Rosé Pine Dawn pastels, kept for decoration — washes,
+ rules, noise bands, anything that is a shape rather than a word. On
+ cream they run from gold at 2.05:1 to love at 3.84:1, all under the
+ 4.5:1 floor, so never put text in them here. They are what the theme
+ actually looks like: use them as ground and put ink on top. */
+ --pine-soft: #286983;
+ --foam-soft: #56949f;
+ --gold-soft: #ea9d34;
+ --love-soft: #b4637a;
+ --iris-soft: #907aa9;
+ --rose-soft: #d7827e;
+
/* semantic */
--bg: var(--base);
--fg: var(--text);
@@ -43,17 +72,36 @@
--accent: var(--iris);
--accent-2: var(--pine);
- /* glass — light frosted paper */
- --glass-bg: rgba(255, 250, 243, 0.72);
- --glass-bg-strong: rgba(255, 250, 243, 0.9);
- --glass-brd: rgba(87, 82, 121, 0.14);
- --glass-brd-hi: rgba(87, 82, 121, 0.26);
- --glass-blur: 16px;
- --glass-shadow: 0 10px 34px rgba(87, 82, 121, 0.13);
- --grid-line: rgba(87, 82, 121, 0.05);
- --scan: rgba(121, 90, 162, 0.04);
- --code-bg: rgba(242, 233, 225, 0.72);
+ /* Structure. `--rule` is the hairline every edge on the site is drawn
+ with; `--rule-hi` is the same ink at the weight a hovered or active
+ edge takes. `--wash` is the one permitted fill that is not a flat
+ colour — a paper tint for table stripes and hover grounds. */
+ --rule: rgba(87, 82, 121, 0.22);
+ --rule-hi: rgba(87, 82, 121, 0.48);
+ --rule-faint: rgba(87, 82, 121, 0.12);
+ --wash: rgba(242, 233, 225, 0.6);
+ --wash-strong: #f2e9e1;
+
+ --code-bg: #f2e9e1;
--selection: rgba(121, 90, 162, 0.22);
+
+ /* The grain film's weight on paper. Multiply, because on a light ground
+ noise has to be laid *into* the page — added to it, it is invisible. */
+ --grain-blend: multiply;
+ --grain-opacity: 0.1;
+
+ /* The banners' signal field, resolved from CSS at draw time so the
+ canvas follows the mode. On paper there is no light to add, so the
+ bands are multiplied in at lower gain (multiply bites harder than
+ `lighter` at the same alpha) using the published dawn hues. */
+ --fuzz-blend: multiply;
+ --fuzz-gain: 0.55;
+ --fuzz-band-1: 144 122 169; /* iris-soft */
+ --fuzz-band-2: 86 148 159; /* foam-soft */
+ --fuzz-band-3: 180 99 122; /* love-soft */
+ --fuzz-fil-1: 87 82 121; /* ink */
+ --fuzz-fil-2: 86 148 159;
+ --fuzz-fil-3: 144 122 169;
}
/* ---- Dark = Rosé Pine (night) ------------------------------------------- */
@@ -74,6 +122,15 @@
--hl-med: #403d52;
--hl-high: #524f67;
+ /* On the dark side the published accents already clear their floors as
+ text, so `-soft` is the same value rather than a separate pastel. */
+ --pine-soft: #3e8fb0;
+ --foam-soft: #9ccfd8;
+ --gold-soft: #f6c177;
+ --love-soft: #eb6f92;
+ --iris-soft: #c4a7e7;
+ --rose-soft: #ebbcba;
+
--bg: var(--base);
--fg: var(--text);
--fg-dim: var(--subtle);
@@ -81,16 +138,54 @@
--accent: var(--iris);
--accent-2: var(--foam);
- --glass-bg: rgba(31, 29, 46, 0.55);
- --glass-bg-strong: rgba(38, 35, 58, 0.78);
- --glass-brd: rgba(224, 222, 244, 0.10);
- --glass-brd-hi: rgba(224, 222, 244, 0.18);
- --glass-blur: 18px;
- --glass-shadow: 0 8px 40px rgba(0, 0, 0, 0.45);
- --grid-line: rgba(224, 222, 244, 0.045);
- --scan: rgba(196, 167, 231, 0.05);
- --code-bg: rgba(25, 23, 36, 0.66);
+ --rule: rgba(224, 222, 244, 0.16);
+ --rule-hi: rgba(224, 222, 244, 0.38);
+ --rule-faint: rgba(224, 222, 244, 0.08);
+ --wash: rgba(224, 222, 244, 0.05);
+ --wash-strong: #26233a;
+
+ --code-bg: #131019;
--selection: rgba(196, 167, 231, 0.30);
+
+ /* Screen, not multiply: on ink, noise has to be added or it vanishes. */
+ --grain-blend: screen;
+ --grain-opacity: 0.07;
+
+ --fuzz-blend: lighter;
+ --fuzz-gain: 1;
+ --fuzz-band-1: 196 167 231;
+ --fuzz-band-2: 156 207 216;
+ --fuzz-band-3: 235 111 146;
+ --fuzz-fil-1: 224 222 244;
+ --fuzz-fil-2: 156 207 216;
+ --fuzz-fil-3: 196 167 231;
+}
+
+/* ---- Bands: grounds, not accents ---------------------------------------- */
+/* A full-bleed band has to carry text on top of it, and an accent at
+ reading weight cannot also be a ground: at the accent's lightness the
+ night palette stops reading on it. So a band is an accent walked down
+ its own hue until the night accents all clear their floors on it.
+
+ Two bands, two tokens — they shared one for a while on the main site
+ and retuning the marquee silently repainted the ledger halfway down
+ the page. These sit outside the theme blocks because a band is the
+ same ink in both modes: it is the page's constant. */
+:root {
+ /* The marquee. Iris, because iris is the signature accent — the hero
+ rule, the section eyebrows — so the one saturated stripe near the
+ top of the page belongs to the family around it. Published iris
+ (#795aa2, hue 266) walked down to ground weight at the same hue.
+ Cream on it: 9.10:1. */
+ --band: #4e3969;
+ /* The ledger band, far enough down the page that the two never share a
+ sightline. Cream on it: 7.04:1. */
+ --band-ledger: #015a77;
+ /* The plate ground the section banners and code blocks are drawn on,
+ and its scrim channels — the canvas reads these to erase its own
+ edges under the title. */
+ --plate: #171322;
+ --plate-scrim: 23 19 34;
}
/* ---- Fonts + scale ------------------------------------------------------ */
@@ -107,11 +202,28 @@
--step-3: clamp(1.85rem, 1.4rem + 2vw, 2.9rem);
--step-4: clamp(2.4rem, 1.6rem + 3.6vw, 4.4rem);
- --radius: 14px;
- --radius-sm: 9px;
- --maxw: 1180px;
+ /* The micro label — 10px mono, uppercase, widely tracked. This is the
+ site's signature type object: every eyebrow, kind mark, stat label,
+ tab and code-block caption is set in it. Fixed in px on purpose; it
+ is furniture, and furniture does not scale with the headline. */
+ --step-micro: 10px;
+ --track-micro: 0.18em;
+ --track-wide: 0.24em;
+
+ /* Square corners, everywhere. Kept as tokens so existing consumers keep
+ compiling — they all resolve to nothing. */
+ --radius: 0;
+ --radius-sm: 0;
+
+ /* The archive measure. Wider than the old glass build's 1180px because
+ the page is now built out of full-bleed bands and ruled tables rather
+ than out of floating cards, and those want the room. */
+ --maxw: 1440px;
--rail: 240px;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
+ /* The site's entrance curve — slower out, hard landing. Distinct from
+ `--ease`, which is for hover-scale transitions. */
+ --ease-in: cubic-bezier(0.2, 0.7, 0.2, 1);
}
/* ---- Plate: the DÆMON signature — an inverse (night) panel on the cream
@@ -127,6 +239,8 @@
--text: #e0def4;
--love: #eb6f92; --gold: #f6c177; --rose: #ebbcba;
--pine: #3e8fb0; --foam: #9ccfd8; --iris: #c4a7e7;
+ --pine-soft: #3e8fb0; --foam-soft: #9ccfd8; --gold-soft: #f6c177;
+ --love-soft: #eb6f92; --iris-soft: #c4a7e7; --rose-soft: #ebbcba;
--hl-low: #21202e; --hl-med: #403d52; --hl-high: #524f67;
--bg: var(--base);
--fg: var(--text);
@@ -134,12 +248,21 @@
--fg-faint: var(--muted);
--accent: var(--iris);
--accent-2: var(--foam);
- --glass-bg: rgba(31, 29, 46, 0.6);
- --glass-bg-strong: rgba(38, 35, 58, 0.82);
- --glass-brd: rgba(224, 222, 244, 0.12);
- --glass-brd-hi: rgba(224, 222, 244, 0.2);
- --glass-shadow: 0 12px 44px rgba(0, 0, 0, 0.4);
- --grid-line: rgba(224, 222, 244, 0.05);
- --code-bg: rgba(25, 23, 36, 0.72);
+ --rule: rgba(224, 222, 244, 0.16);
+ --rule-hi: rgba(224, 222, 244, 0.38);
+ --rule-faint: rgba(224, 222, 244, 0.08);
+ --wash: rgba(224, 222, 244, 0.05);
+ --wash-strong: #26233a;
+ --code-bg: #131019;
+ --grain-blend: screen;
+ --grain-opacity: 0.07;
+ --fuzz-blend: lighter;
+ --fuzz-gain: 1;
+ --fuzz-band-1: 196 167 231;
+ --fuzz-band-2: 156 207 216;
+ --fuzz-band-3: 235 111 146;
+ --fuzz-fil-1: 224 222 244;
+ --fuzz-fil-2: 156 207 216;
+ --fuzz-fil-3: 196 167 231;
color: var(--fg);
}