daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

commit 6813e11af34363dbd14065d5815797b09d1d3c80
parent 5b8ba9d7477d1bcc155873493cff96d361b277d0
Author: DAEMON <zer0sec.xp@icloud.com>
Date:   Thu,  3 Sep 2026 01:11:07 +0100

Redesign the vault homepage to match daemon-sec.xyz

Re-skin the cheatsheet site to the main site's design language (the dmn
system): DM Mono type, the fixed dawn/night/signal Rosé Pine palette, and a
1560px measure. The home route is recomposed from the main site's own
components, all bound to the real vault data — nothing is removed.

- Hero loop: a 1:1 vanilla port of the site's hero-loop (one 21.4s rAF clock,
  1920x1080 canvas scaled at >=1280px, fluid flow below, glyph decode,
  pointer/focus takeover, theme-aware plate inversion, reduced-motion holds
  T=12). Rows are real links to the category routes.
- Manifest band: dual-lane counter-scrolling marquee with an inverse cap.
- Operator band: dark plate, terminal readout, search rail that opens Pagefind.
- Index: SectionHeader + filter pills + paper-wash record rows (titles stay
  legible on hover) + show-more, bound to recent sheets across shelves.
- Shelves grid: the full taxonomy ("pick your vector").
- Mirrors band: the swisskyrepo PayloadsAllTheThings + InternalAllTheThings
  mirrors, credited with live tree/file counts.
- Masthead + footer re-plated in the main site's grammar; Callout and
  SectionHeader ported; RecordRow reused site-wide.
- Add self-hosted DM Mono (300/400/500 + italic); point the site mono at it.

Design and CSS only: content, frontmatter, taxonomy, search indexing and
deploy are untouched. Verified light/dark and 1560/1024/390 widths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E5eBM16vjWscUMJJTanpwk

Diffstat:
Asrc/components/Callout.astro | 26++++++++++++++++++++++++++
Msrc/components/Footer.astro | 69+++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/components/Header.astro | 84++++++++++++++++++++++++++++++++++++-------------------------------------------
Asrc/components/HeroLoop.astro | 384+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/Icon.astro | 3+++
Msrc/components/Marquee.astro | 181++++++++++++++++---------------------------------------------------------------
Asrc/components/Operator.astro | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/RecordRow.astro | 46++++++++++++++++++++++++++--------------------
Asrc/components/SectionHeader.astro | 26++++++++++++++++++++++++++
Asrc/fonts/dm-mono-300.woff2 | 0
Asrc/fonts/dm-mono-400-italic.woff2 | 0
Asrc/fonts/dm-mono-400.woff2 | 0
Asrc/fonts/dm-mono-500.woff2 | 0
Msrc/layouts/Base.astro | 6+++---
Msrc/pages/index.astro | 332++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Msrc/styles/app.css | 1+
Asrc/styles/daemon.css | 418+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/styles/tokens.css | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
18 files changed, 1369 insertions(+), 331 deletions(-)

diff --git a/src/components/Callout.astro b/src/components/Callout.astro @@ -0,0 +1,26 @@ +--- +import Icon from './Icon.astro'; + +interface Props { + tone?: 'note' | 'finding' | 'warning' | 'danger'; + /** Override the tone's default kind label. */ + label?: string; +} +const { tone = 'note', label } = Astro.props; + +/** + * Flat plate, a 2px accent rule, mono eyebrow — the archive's callout, ported + * from the main site's Callout. Four tones, each with its own kind mark. + */ +const TONES = { + note: { color: 'var(--pine)', icon: 'info', label: 'Note' }, + finding: { color: 'var(--foam)', icon: 'check', label: 'Finding' }, + warning: { color: 'var(--gold)', icon: 'warn', label: 'Warning' }, + danger: { color: 'var(--love)', icon: 'close', label: 'Danger' }, +} as const; +const t = TONES[tone]; +--- +<div class="dcallout" style={`--tone: ${t.color};`}> + <div class="dcallout__label"><Icon name={t.icon} /> {label ?? t.label}</div> + <div class="dcallout__body"><slot /></div> +</div> diff --git a/src/components/Footer.astro b/src/components/Footer.astro @@ -1,32 +1,57 @@ --- 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; + +/** + * The footer, re-plated on the signal field per the main site's Footer: a + * dark closing band with the wordmark in its reversed setting, the motto, + * the numbered nav (each index keeps its section hue), and the status block. + * `.dfooter` pins the signal palette, so it stays dark in both modes. + */ +const NAV = [ + { n: '01', label: 'Vault', href: url(''), acc: 'love' }, + { n: '02', label: 'Payloads', href: url('payloads'), acc: 'iris' }, + { n: '03', label: 'Internal', href: url('internal'), acc: 'pine' }, + { n: '04', label: 'Tags', href: url('tags'), acc: 'foam' }, + { n: '05', label: 'Credits', href: url('credits'), acc: 'gold' }, +]; --- -<!-- 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> +<footer class="dfooter site-footer"> + <span class="dfooter__scrim" aria-hidden="true"></span> <span class="scanlines" aria-hidden="true"></span> - <div class="wrap corners"> - <div> - <a class="brand" href={url('')}>DÆMON<span class="slash">//</span>SEC</a> - <p class="footer-note"> - A curated vault of IT &amp; security cheatsheets. For authorized testing, - CTFs, and education only. Know your scope; get permission first. - </p> - </div> - <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">Rosé Pine · Astro · Pagefind</span> + <div class="archive dfooter__inner"> + <div class="dfooter__grid"> + <div> + <a href={url('')} class="dfooter__wordmark">DΛΣMӨП<span class="hy">-</span>SΣC</a> + <p class="dfooter__motto"> + Terse, tested, kept current. A curated vault of IT &amp; security cheatsheets — for + authorised testing, CTFs and education only. Know your scope; get permission first. + </p> + </div> + + <nav class="dfooter__nav" aria-label="Footer"> + {NAV.map((item) => ( + <a href={item.href} style={`--acc: var(--color-night-${item.acc});`}> + <span class="n">{item.n}</span>{item.label} + </a> + ))} + <a href={SITE} rel="noopener" style="--acc: var(--color-night-foam);"><span class="n">06</span>Main site ↗</a> + <a href={GITHUB} target="_blank" rel="noopener" style="--acc: var(--color-night-gold);"><span class="n">07</span>GitHub ↗</a> + </nav> + + <div class="dfooter__meta"> + <div class="live"> + <span class="dmn-dot" style="--dot: var(--color-night-love);" aria-hidden="true"><i class="dmn-anim-pulse"></i><u class="dmn-anim-ring"></u></span> + All systems operational + </div> + <div class="row">Payloads &amp; Internal mirrored from + <a href="https://github.com/swisskyrepo/PayloadsAllTheThings" target="_blank" rel="noopener">swisskyrepo</a> · MIT</div> + <div class="row">Rosé Pine · Astro · Pagefind</div> + <div class="row">© {year} DÆMON//SEC · <a href={url('credits')}>credits</a> · MIT</div> + </div> </div> </div> </footer> diff --git a/src/components/Header.astro b/src/components/Header.astro @@ -6,17 +6,14 @@ 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. + * The masthead, in the main site's grammar (daemon-sec.xyz `Masthead`): a + * wordmark chip on the left, numbered mono nav in the centre, and the action + * cluster on the right. Each nav 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. * - * 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. + * The nav entries and the search / theme / mobile wiring are the cheat-sheet + * site's own — `[data-search-open]`, `[data-theme-toggle]` and + * `[data-mobile-nav-toggle]` are bound in scripts/app.ts and SearchModal. */ const NAV = [ { n: '01', label: 'Vault', href: url(''), acc: 'love', exact: true }, @@ -33,78 +30,73 @@ const isActive = (href: string, exact?: boolean) => { const target = href.replace(/\/+$/, '') || '/'; return exact ? here === target : here === target || here.startsWith(target + '/'); }; + +/* The Orthodox cross, drawn rather than typed — none of the site's faces + carry U+2626, so it would fall back per-character. Ported from the main + site's OrthodoxCross. */ +const CROSS = `<line x1="9" y1="1.3" x2="9" y2="22.7"/><line x1="6.1" y1="6.2" x2="11.9" y2="6.2"/><line x1="1.3" y1="10.7" x2="16.7" y2="10.7"/><line x1="3.7" y1="15.9" x2="14.3" y2="18.7"/>`; --- -<!-- The masthead as a tmux-style segbar: the brand is the accent-filled - left segment, the numbered nav entries and the action cluster are - hairline-divided segments running the full height of the bar. The - `.segbar` / `.seg` / `.seg--accent` objects are defined once in - chrome.css; the scoped block below carries the header-specific half. --> -<header class="site-header segbar"> - <div class="wrap"> - <a class="brand seg seg--accent glitch" href={url('')} aria-label="DÆMON//SEC home" data-text="DÆMON//SEC"> - DÆMON<span class="slash">//</span>SEC<span class="blink">_</span> +<header class="mast site-header"> + <div class="archive mast__row"> + <a class="mast__brand" href={url('')} aria-label="DÆMON//SEC — home"> + <span class="mast__chip">DΛΣMӨП<svg viewBox="0 0 18 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="butt" aria-hidden="true" set:html={CROSS} /></span> </a> - <nav class="mast-nav" aria-label="Primary"> + <nav class="mast__nav" aria-label="Sections"> {NAV.map((item) => ( <a - class="seg" + class="mast__navitem" 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> + <span class="rule" aria-hidden="true"></span> </a> ))} </nav> - <div class="mast-actions"> - <button class="search-trigger seg" data-search-open type="button" aria-label="Search cheatsheets"> - <Icon name="search" /> - <span class="st-label">Search</span> - <kbd>/</kbd> + <div class="mast__actions"> + <span class="mast__status" aria-hidden="true"> + <span class="dmn-dot" style="--dot: var(--love);"><i class="dmn-anim-pulse"></i><u class="dmn-anim-ring"></u></span> + <span>Offline · Pagefind</span> + </span> + <button class="mast__icon" data-search-open type="button" aria-label="Search the vault"> + <Icon name="search" /><kbd>/</kbd> </button> - <button class="icon-btn seg" data-theme-toggle type="button" aria-label="Toggle theme"> + <button class="mast__icon mast__mode" data-theme-toggle type="button" aria-label="Dark mode"> <span class="icon-sun"><Icon name="sun" /></span> <span class="icon-moon"><Icon name="moon" /></span> </button> - <a class="icon-btn seg" href={SITE} rel="noopener" aria-label="DÆMON main site"> - <Icon name="external" /> - </a> - <a class="icon-btn seg" href={GITHUB} target="_blank" rel="noopener" aria-label="GitHub repository"> - <Icon name="github" /> - </a> + <a class="mast__icon" href={SITE} rel="noopener" aria-label="DÆMON main site"><Icon name="external" /></a> + <a class="mast__icon" href={GITHUB} target="_blank" rel="noopener" aria-label="GitHub repository"><Icon name="github" /></a> <button - class="icon-btn seg mobile-nav-trigger" + class="mast__burger" data-mobile-nav-toggle type="button" aria-label="Open navigation" aria-controls="mobile-primary-nav" aria-expanded="false" > - <span class="mobile-nav-glyph" aria-hidden="true"><i></i><i></i><i></i></span> + <i></i><i></i><i></i> </button> </div> </div> - <nav id="mobile-primary-nav" class="mobile-nav" data-mobile-nav aria-label="Primary mobile" hidden> + <nav id="mobile-primary-nav" class="mast__mobile" data-mobile-nav aria-label="Primary mobile" hidden> {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> + <span class="n">{item.n}</span>{item.label} + <span class="arrow" aria-hidden="true">→</span> </a> ))} + <a href={SITE} rel="noopener"> + <span class="n" style="--acc: var(--foam);">↗</span>Main site + <span class="arrow" aria-hidden="true" style="--acc: var(--foam);">↗</span> + </a> </nav> </header> - -<style> - /* Show the correct theme glyph without a flash */ - .icon-sun { display: inline-flex; } - .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/HeroLoop.astro b/src/components/HeroLoop.astro @@ -0,0 +1,384 @@ +--- +interface Shelf { n: string; label: string; path: string; accent: string; desc: string } +interface Props { + shelves: Shelf[]; + name?: string; + tagline?: string; +} +const { shelves, name = 'DΛΣMӨП-RΣF', tagline = 'DÆMON-REF · Cheatsheet vault' } = Astro.props; +const data = JSON.stringify({ shelves, name, tagline }); +--- +<section class="hero-loop bleed" data-hero-loop data-hero={data} aria-label="Shelves"> + <noscript> + <div style="padding:56px 44px;max-width:1560px;margin:0 auto;"> + <p style="font-family:var(--font-wordmark);font-weight:800;font-size:clamp(40px,9vw,120px);font-stretch:88%;letter-spacing:-0.02em;line-height:1;color:var(--text);text-transform:uppercase;">{name}</p> + <p style="margin-top:14px;font-family:var(--font-plex);font-size:13px;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);">Six shelves. One vault.</p> + <ul style="margin-top:28px;list-style:none;padding:0;border-top:1px solid var(--line);"> + {shelves.map((s) => ( + <li><a href={s.path} style={`display:flex;justify-content:space-between;gap:16px;padding:16px 0;border-bottom:1px solid var(--line);font-family:var(--font-archivo);font-weight:600;font-size:24px;text-transform:uppercase;color:var(--${s.accent});`}>{s.label}<span style="font-family:var(--font-plex);font-size:13px;color:var(--subtle);">{s.path}</span></a></li> + ))} + </ul> + </div> + </noscript> +</section> + +<script> + // Hero loop — a 1:1 vanilla port of the handoff's hero-loop.js (itself the + // main site's hero-loop.tsx): one 21.4s rAF clock, an authored 1920×1080 + // canvas scaled to fit at >=1280px, a fluid flow shape below, pointer/focus + // takeover with an eased `mix`, glyph decode until +0.55s, and the plate as + // the inverse of the page (theme-aware P/Q). Pauses when hidden or scrolled + // out; prefers-reduced-motion holds T=12.0 with no scramble. + + const W = 1920, H = 1080, PLATE_W = 784, PLATE_H = 780, MIN_HEIGHT = 560, MARQUEE = 92; + const CANVAS_MIN = 1280, FLOW_PLATE_MIN = 860; + const BOOT = 0, CLOSE = 18.8, AUTHORED = 21.4, HELD_FRAME = 12.0; + const STARTS = [3.2, 5.8, 8.4, 11.0, 13.6, 16.2]; + const ENDS = STARTS.map((s, i) => (i < 5 ? STARTS[i + 1] : CLOSE)); + const GLYPH_POOL = 'ΛΣӨПΔΞΨΩ01#/\\_'; + + const DAWN = { base: 'var(--color-dawn-base)', surface: 'var(--color-dawn-surface)', ink: 'var(--color-dawn-ink)', subtle: 'var(--color-dawn-subtle)', muted: 'var(--color-dawn-muted)', line: 'color-mix(in srgb, var(--color-dawn-ink) 16%, transparent)', pine: 'var(--color-dawn-pine)', foam: 'var(--color-dawn-foam)', gold: 'var(--color-dawn-gold)', love: 'var(--color-dawn-love)', iris: 'var(--color-dawn-iris)', rose: 'var(--color-dawn-rose)' }; + const NIGHT = { base: 'var(--color-night-base)', surface: 'var(--color-night-surface)', ink: 'var(--color-night-text)', subtle: 'var(--color-night-subtle)', muted: 'var(--color-night-muted)', line: 'color-mix(in srgb, var(--color-night-text) 16%, transparent)', pine: 'var(--color-night-pine)', foam: 'var(--color-night-foam)', gold: 'var(--color-night-gold)', love: 'var(--color-night-love)', iris: 'var(--color-night-iris)', rose: 'var(--color-night-rose)' }; + const MONO = 'var(--font-plex)', ARCHIVO = 'var(--font-archivo)', WORDMARK_FACE = 'var(--font-wordmark)'; + + const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v)); + const easeOutCubic = (p: number) => 1 - Math.pow(1 - p, 3); + const easeInOutCubic = (p: number) => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2); + const enter = (T: number, start: number, dur = 0.6) => { const o = easeOutCubic(clamp((T - start) / dur, 0, 1)); return { o, y: (1 - o) * 24 }; }; + const draw = (T: number, start: number, dur = 0.8) => easeInOutCubic(clamp((T - start) / dur, 0, 1)); + const hold = (T: number, a: number, b: number, ramp = 0.35) => Math.min(easeOutCubic(clamp((T - a) / ramp, 0, 1)), easeOutCubic(clamp((b - T) / ramp, 0, 1))); + const hash = (i: number, k: number) => { const s = Math.sin(i * 12.9898 + k * 78.233) * 43758.5453; return s - Math.floor(s); }; + const activeAt = (T: number) => { for (let i = 0; i < 6; i++) if (T >= STARTS[i] && T < ENDS[i]) return i; return -1; }; + const timecode = (T: number) => `T+${String(Math.floor(T / 60)).padStart(2, '0')}:${String(Math.floor(T % 60)).padStart(2, '0')}.${String(Math.floor((T % 1) * 24)).padStart(2, '0')}`; + + type Shelf = { n: string; label: string; path: string; accent: string; desc: string }; + + function shelfState(T: number, mix: number, held: number | null, shelves: Shelf[]) { + const act: number[] = [], plateAct: number[] = [], plateY: number[] = [], plateLine: number[] = []; + for (let i = 0; i < shelves.length; i++) { + const hv = i === held ? 1 : 0; + act.push(hold(T, STARTS[i], ENDS[i]) * (1 - mix) + hv * mix); + plateAct.push(hold(T, STARTS[i], ENDS[i], 0.4) * (1 - mix) + hv * mix); + plateY.push((1 - enter(T, STARTS[i], 0.5).o) * 40 * (1 - mix)); + const line = draw(T, STARTS[i] + 0.15, 0.7) * (1 - draw(T, ENDS[i] - 0.4, 0.4)); + plateLine.push(line * (1 - mix) + hv * mix); + } + const fromClock = activeAt(T); + const active = mix > 0.5 ? (held ?? fromClock) : fromClock; + const standing = Math.max(hold(T, BOOT + 1.6, STARTS[0]), hold(T, CLOSE + 0.35, AUTHORED + 1)) * (1 - mix); + return { act, plateAct, plateY, plateLine, standing, active, accent: active >= 0 ? shelves[active].accent : 'love' }; + } + + // Tiny DOM helper. Numeric style values need no unit here — every length is + // passed as an explicit px string. + function el(tag: string, style?: Record<string, string>, kids?: any): HTMLElement { + const n = document.createElement(tag); + if (style) Object.assign(n.style, style); + if (kids != null) (Array.isArray(kids) ? kids : [kids]).forEach((k) => { if (k == null || k === false) return; n.append(k.nodeType ? k : document.createTextNode(String(k))); }); + return n; + } + + function build(hostW: number, mode: 'canvas' | 'flow', withPlate: boolean, shelves: Shelf[], name: string, tagline: string, P: any, Q: any, onHold: (i: number) => void, onRelease: () => void) { + // Row metrics differ by shape. + const M = mode === 'canvas' + ? { height: '76px', columns: '64px 1fr auto', padLeft: '14px', chipSize: '14px', chipPad: '3px 7px', labelSize: '30px', pathSize: '15px', pathPad: '16px', shift: 10, arrow: true } + : { height: '56px', columns: '44px 1fr auto', padLeft: '8px', chipSize: '11px', chipPad: '3px 6px', labelSize: 'clamp(17px, 2.2vw, 26px)', pathSize: 'clamp(10px, 1.1vw, 13px)', pathPad: '0px', shift: 8, arrow: false }; + + // ── Shelf row ── + const rows = shelves.map((s) => { + const acc = P[s.accent]; + const wash = el('span', { position: 'absolute', inset: '0', background: P.surface, opacity: '0', borderLeft: `3px solid ${acc}`, transformOrigin: 'left', transform: 'scaleX(0)' }); + const chip = el('span', { fontFamily: MONO, fontSize: M.chipSize, letterSpacing: '0.06em', padding: M.chipPad, color: P.muted, background: 'transparent', boxShadow: `inset 0 0 0 1px ${P.line}` }, s.n); + const chipWrap = el('span', { position: 'relative', display: 'flex', alignItems: 'center', paddingLeft: M.padLeft }, chip); + const label = el('span', { position: 'relative', fontFamily: ARCHIVO, fontWeight: '600', fontSize: M.labelSize, letterSpacing: '0.04em', textTransform: 'uppercase', color: P.ink, opacity: '1', transform: 'translateX(0)' }, s.label); + const arrow = M.arrow ? el('span', { display: 'inline-block', marginLeft: '12px', opacity: '0', transform: 'translateX(-8px)' }, '→') : null; + const path = el('span', { position: 'relative', fontFamily: MONO, fontSize: M.pathSize, letterSpacing: '0.06em', color: P.subtle, paddingRight: M.pathPad, opacity: '0.6' }, [s.path, arrow]); + const a = el('a', { position: 'relative', display: 'grid', gridTemplateColumns: M.columns, alignItems: 'center', height: M.height, borderTop: `1px solid ${P.line}`, opacity: '1', transform: 'translateY(0)', textDecoration: 'none', color: P.ink }, [wash, chipWrap, label, path]) as HTMLAnchorElement; + a.href = s.path; + const idx = shelves.indexOf(s); + a.addEventListener('mouseenter', () => onHold(idx)); + a.addEventListener('mouseleave', onRelease); + a.addEventListener('focus', () => onHold(idx)); + a.addEventListener('blur', onRelease); + return { a, wash, chip, label, path, arrow }; + }); + const nav = el('nav', mode === 'canvas' + ? { position: 'absolute', left: '72px', top: '424px', width: '916px' } + : { marginTop: '32px' }, [...rows.map((r) => r.a), el('div', { borderTop: `1px solid ${P.line}` })]); + nav.setAttribute('aria-label', 'Shelves'); + + // ── Plate ── + const dots: HTMLElement[] = []; + const cols = 24, rowsN = 24, cw = PLATE_W / cols, chh = PLATE_H / rowsN; + const dotField = el('div', { position: 'absolute', left: '0', top: '0', width: PLATE_W + 'px', height: PLATE_H + 'px' }); + for (let r = 0; r < rowsN; r++) for (let c = 0; c < cols; c++) { + const d = el('div', { position: 'absolute', left: (c * cw + cw / 2 - 2) + 'px', top: (r * chh + chh / 2 - 2) + 'px', width: '4px', height: '4px', borderRadius: '2px', background: Q.ink, opacity: '0.08' }); + dots.push(d); dotField.append(d); + } + const plateMeta = { fontFamily: MONO, fontSize: '14px', letterSpacing: '0.10em', textTransform: 'uppercase', color: Q.subtle }; + const chromeRight = el('span', { color: P.love }, '— / 06'); + const chrome = el('div', { position: 'absolute', left: '40px', right: '40px', top: '36px', display: 'flex', justifyContent: 'space-between', whiteSpace: 'nowrap', ...plateMeta }, [el('span', undefined, 'Plate · index'), chromeRight]); + const standing = el('div', { position: 'absolute', left: '40px', right: '40px', bottom: '48px', opacity: '0', transform: 'translateY(16px)' }, [ + el('div', { ...plateMeta, marginBottom: '14px' }, 'Standing by'), + el('div', { fontFamily: ARCHIVO, fontWeight: '700', fontSize: '64px', lineHeight: '0.98', letterSpacing: '-0.02em', color: Q.ink }, ['Security lives', el('br'), 'in the details.']), + el('div', { marginTop: '22px', fontFamily: MONO, fontSize: '15px', letterSpacing: '0.04em', color: Q.muted }, '06 shelves · select an entry'), + ]); + const sections = shelves.map((s) => { + const acc = Q[s.accent]; + const rule = el('div', { height: '2px', background: acc, marginTop: '26px', transformOrigin: 'left', transform: 'scaleX(0)' }); + const block = el('div', { position: 'absolute', left: '40px', right: '40px', bottom: '48px', opacity: '0', transform: 'translateY(40px)' }, [ + el('div', { display: 'flex', alignItems: 'center', gap: '14px', marginBottom: '20px' }, [ + el('span', { fontFamily: MONO, fontSize: '14px', letterSpacing: '0.08em', padding: '4px 8px', background: acc, color: Q.base }, s.n), + el('span', { fontFamily: MONO, fontSize: '15px', letterSpacing: '0.08em', color: acc }, s.path), + ]), + el('div', { fontFamily: ARCHIVO, fontWeight: '800', fontSize: (s.label.length > 5 ? 118 : 148) + 'px', lineHeight: '0.92', letterSpacing: '-0.03em', textTransform: 'uppercase', color: Q.ink }, s.label), + rule, + el('div', { marginTop: '22px', fontFamily: ARCHIVO, fontSize: '27px', lineHeight: '1.3', color: Q.subtle, maxWidth: '620px' }, s.desc), + el('div', { marginTop: '28px', fontFamily: MONO, fontSize: '15px', letterSpacing: '0.10em', textTransform: 'uppercase', color: Q.muted, display: 'flex', gap: '12px', alignItems: 'center' }, [el('span', { color: acc }, '▸'), ' Enter shelf']), + ]); + return { block, rule }; + }); + const plateSurface = el('div', { position: 'absolute', left: '0', top: '0', width: PLATE_W + 'px', height: PLATE_H + 'px', background: Q.base, color: Q.ink, overflow: 'hidden', clipPath: 'inset(0 100% 0 0)' }, [ + el('div', { position: 'absolute', inset: '0', background: `radial-gradient(120% 80% at 90% 100%, ${Q.surface} 0%, ${Q.base} 60%)` }), + dotField, chrome, + el('div', { position: 'absolute', left: '40px', right: '40px', top: '66px', height: '1px', background: Q.line }), + standing, ...sections.map((s) => s.block), + ]); + plateSurface.setAttribute('aria-hidden', 'true'); + + // Plate host — absolute box on canvas, aspect-ratio fitted box on flow. + let plateHost: HTMLElement, plateScaleK = 1; + if (mode === 'canvas') { + plateHost = el('div', { position: 'absolute', left: '1064px', top: '150px', width: PLATE_W + 'px', height: PLATE_H + 'px' }, plateSurface); + } else { + const inner = el('div', { position: 'absolute', left: '0', top: '0', width: PLATE_W + 'px', height: PLATE_H + 'px', transformOrigin: '0 0' }, plateSurface); + plateHost = el('div', { position: 'relative', width: '100%', aspectRatio: `${PLATE_W} / ${PLATE_H}`, overflow: 'hidden' }, inner); + plateHost.setAttribute('aria-hidden', 'true'); + (plateHost as any)._inner = inner; + } + + // ── Wordmark ── + const wm = name.split('').map((g) => el('span', { display: 'inline-block', opacity: '0', transform: 'translateY(24px)', color: g === '-' ? P.love : P.muted, fontFamily: MONO, fontWeight: '500' }, g)); + const wordmark = el('div', { fontFamily: WORDMARK_FACE, fontWeight: '700', lineHeight: '1', letterSpacing: '-0.02em', color: P.ink, display: 'flex', whiteSpace: 'pre' }, wm); + + // ── Meta rows + subline ── + const metaBase = (x?: Record<string, string>) => ({ fontFamily: MONO, fontSize: '15px', letterSpacing: '0.10em', textTransform: 'uppercase', color: P.subtle, whiteSpace: 'nowrap', ...(x || {}) }); + const recDot = el('div', { width: '8px', height: '8px', borderRadius: '4px', background: P.love, opacity: '1' }); + const timecodeEl = el('div', metaBase({ fontVariantNumeric: 'tabular-nums' }), timecode(0)); + const sublinePath = el('span', { color: P[shelves[0]?.accent] || P.love }, '/'); + let topMeta: HTMLElement, subline: HTMLElement, hairline: HTMLElement, bottomMeta: HTMLElement, root: HTMLElement; + + if (mode === 'canvas') { + hairline = el('div', { position: 'absolute', left: '72px', right: '72px', top: '96px', height: '1px', background: P.ink, transformOrigin: 'left', transform: 'scaleX(0)', opacity: '0.9' }); + topMeta = el('div', { position: 'absolute', left: '72px', right: '72px', top: '56px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', opacity: '0' }, [ + el('div', metaBase(), tagline), + el('div', { display: 'flex', alignItems: 'center', gap: '10px' }, [recDot, el('div', metaBase({ color: P.love }), 'REC')]), + timecodeEl, + ]); + wordmark.style.fontSize = '138px'; + subline = el('div', metaBase({ marginTop: '18px', marginLeft: '4px', color: P.muted, opacity: '0', transform: 'translateY(24px)' }), ['Six shelves. One vault. ', sublinePath]); + const wmBlock = el('div', { position: 'absolute', left: '68px', top: '178px' }, [wordmark, subline]); + bottomMeta = el('div', { position: 'absolute', left: '72px', right: '72px', bottom: '56px', display: 'flex', justifyContent: 'space-between', opacity: '0' }, [ + el('div', metaBase({ color: P.muted }), 'Rosé Pine · Dawn · plates inverted'), + el('div', metaBase({ color: P.muted }), 'Scroll ↓'), + ]); + root = el('div', { position: 'absolute', inset: '0', background: P.base, color: P.ink, opacity: '0', overflow: 'hidden', fontFamily: ARCHIVO }, [hairline, topMeta, wmBlock, nav, plateHost, bottomMeta]); + } else { + wordmark.style.fontSize = withPlate ? 'clamp(36px, 6.4vw, 88px)' : 'clamp(28px, 10vw, 80px)'; + hairline = el('div', { height: '1px', marginTop: '14px', background: P.ink, transformOrigin: 'left', transform: 'scaleX(0)', opacity: '0.9' }); + topMeta = el('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', opacity: '0' }, [ + el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)' }), tagline), + el('div', { display: 'flex', alignItems: 'center', gap: '8px' }, [recDot, timecodeEl]), + ]); + subline = el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)', marginTop: '14px', color: P.muted, whiteSpace: 'normal', opacity: '0', transform: 'translateY(24px)' }), ['Six shelves. One vault. ', sublinePath]); + const left = el('div', undefined, [wordmark, subline, nav]); + const center = el('div', { display: 'grid', gridTemplateColumns: withPlate ? 'minmax(0,1fr) clamp(300px, 44%, 560px)' : 'minmax(0,1fr)', gap: 'clamp(28px, 3.4vw, 56px)', alignItems: 'center' }, withPlate ? [left, plateHost] : [left]); + const centerWrap = el('div', { flex: '1', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: 'clamp(28px, 4vw, 56px) 0' }, center); + bottomMeta = el('div', { paddingTop: '32px', display: 'flex', justifyContent: 'space-between', gap: '12px', opacity: '0' }, [ + el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)', color: P.muted }), 'Rosé Pine · Dawn'), + el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)', color: P.muted }), 'Scroll ↓'), + ]); + root = el('div', { display: 'flex', flexDirection: 'column', minHeight: `min(calc(100svh - ${MARQUEE}px), 620px)`, padding: 'clamp(28px, 4vw, 56px) clamp(20px, 3.8vw, 72px)', background: P.base, color: P.ink, opacity: '0', fontFamily: ARCHIVO }, [topMeta, hairline, centerWrap, bottomMeta]); + } + + let lastStep = -1, lastAccent = ''; + function paintPlate(T: number, st: any, reveal: number) { + plateSurface.style.clipPath = `inset(0 ${(1 - reveal) * 100}% 0 0)`; + const accentName = st.active >= 0 ? shelves[st.active].accent : 'love'; + const accentCol = Q[accentName]; + chromeRight.textContent = st.active >= 0 ? `${shelves[st.active].n} / 06` : '— / 06'; + chromeRight.style.color = accentCol; + const step = Math.floor(T * 6); + if (step !== lastStep || accentName !== lastAccent) { + lastStep = step; lastAccent = accentName; + for (let i = 0; i < dots.length; i++) { + const v = hash(i, step), v2 = hash(i + 991, step), on = v > 0.72; + const d = dots[i]; + d.style.background = on && v2 > 0.6 ? accentCol : Q.ink; + d.style.opacity = String(on ? 0.35 + v2 * 0.55 : 0.08); + } + } + standing.style.opacity = String(st.standing); + standing.style.transform = `translateY(${(1 - st.standing) * 16}px)`; + for (let i = 0; i < sections.length; i++) { + sections[i].block.style.opacity = String(st.plateAct[i]); + sections[i].block.style.transform = `translateY(${st.plateY[i]}px)`; + sections[i].rule.style.transform = `scaleX(${st.plateLine[i]})`; + } + } + + function paint(T: number, st: any) { + const master = Math.min(enter(T, BOOT, 0.5).o, easeOutCubic(clamp((AUTHORED - T) / 0.5, 0, 1))); + const reveal = draw(T, BOOT + 0.9, 1.0) * (1 - draw(T, CLOSE + 1.2, 0.8)); + const rule = draw(T, BOOT + 0.25, 0.9), pulse = 0.55 + 0.45 * Math.sin(T * 5), sub = enter(T, BOOT + 2.1); + root.style.opacity = String(master); + hairline.style.transform = `scaleX(${rule})`; + topMeta.style.opacity = String(enter(T, BOOT + 0.2).o); + recDot.style.opacity = String(pulse); + timecodeEl.textContent = timecode(T); + bottomMeta.style.opacity = String(enter(T, BOOT + 2.4).o); + // Wordmark decode + const roll = Math.floor(T * 18); + for (let i = 0; i < wm.length; i++) { + const g = name[i], at = BOOT + 0.35 + i * 0.16, revealed = T >= at + 0.55, e = enter(T, at, 0.5); + const ch = revealed ? g : GLYPH_POOL[Math.floor(hash(i, roll) * GLYPH_POOL.length)]; + const node = wm[i]; + if (node.textContent !== ch) node.textContent = ch; + node.style.opacity = String(e.o); + node.style.transform = `translateY(${e.y}px)`; + node.style.color = g === '-' ? P.love : revealed ? P.ink : P.muted; + node.style.fontFamily = revealed ? WORDMARK_FACE : MONO; + node.style.fontWeight = revealed ? '700' : '500'; + } + // Subline + subline.style.opacity = String(sub.o); + subline.style.transform = `translateY(${sub.y}px)`; + sublinePath.textContent = st.active >= 0 ? shelves[st.active].path : '/'; + sublinePath.style.color = P[st.accent]; + // Rows + for (let i = 0; i < rows.length; i++) { + const a = st.act[i], s = shelves[i], acc = P[s.accent], lit = a > 0.5; + const en = enter(T, BOOT + 1.3 + i * 0.13, 0.55); + const R = rows[i]; + R.a.style.opacity = String(en.o); + R.a.style.transform = `translateY(${en.y}px)`; + R.wash.style.opacity = String(a); + R.wash.style.transform = `scaleX(${a})`; + R.chip.style.color = lit ? P.base : P.muted; + R.chip.style.background = lit ? acc : 'transparent'; + R.chip.style.boxShadow = `inset 0 0 0 1px ${lit ? acc : P.line}`; + R.label.style.color = a > 0.01 ? acc : P.ink; + R.label.style.opacity = String(0.55 + 0.45 * Math.max(a, st.active >= 0 ? 0 : 1)); + R.label.style.transform = `translateX(${a * M.shift}px)`; + R.path.style.opacity = String(0.6 + 0.4 * a); + if (R.arrow) { R.arrow.style.opacity = String(a); R.arrow.style.transform = `translateX(${(1 - a) * -8}px)`; } + } + paintPlate(T, st, reveal); + if (mode === 'flow' && withPlate) { + const k = plateHost.clientWidth / PLATE_W; + if (k > 0 && Math.abs(k - plateScaleK) > 0.001) { plateScaleK = k; (plateHost as any)._inner.style.transform = `scale(${k})`; } + } + } + return { root, paint }; + } + + function initHero() { + const host = document.querySelector('[data-hero-loop]') as HTMLElement | null; + if (!host || host.dataset.bound) return; + host.dataset.bound = '1'; + let cfg: { shelves: Shelf[]; name: string; tagline: string }; + try { cfg = JSON.parse(host.dataset.hero || '{}'); } catch { return; } + const shelves = cfg.shelves || []; + if (!shelves.length) return; + + const reduced = matchMedia('(prefers-reduced-motion: reduce)'); + let hover: { idx: number | null; last: number | null } = { idx: null, last: null }; + const onHold = (i: number) => { hover = { idx: i, last: i }; }; + const onRelease = () => { hover = { idx: null, last: hover.last }; }; + + let current: { root: HTMLElement; paint: (T: number, st: any) => void } | null = null; + let mode: 'canvas' | 'flow' | null = null, withPlate = false, dark = document.documentElement.getAttribute('data-theme') === 'dark'; + let carriedT = 0, carriedMix = 0, raf = 0, prev = 0, visible = !document.hidden, inView = true; + + function rebuild() { + const w = host.clientWidth || window.innerWidth; + const canvas = window.innerWidth >= CANVAS_MIN; + mode = canvas ? 'canvas' : 'flow'; + withPlate = !canvas && window.innerWidth >= FLOW_PLATE_MIN; + dark = document.documentElement.getAttribute('data-theme') === 'dark'; + const P = dark ? NIGHT : DAWN, Q = dark ? DAWN : NIGHT; + host.textContent = ''; + if (canvas) { + const stage = el('div', { position: 'absolute', left: '0', top: '0', transformOrigin: '0 0' }); + host.style.height = `max(${MIN_HEIGHT}px, min(calc(100vh - ${MARQUEE}px), ${(H / W) * 100}vw))`; + const built = build(w, 'canvas', false, shelves, cfg.name, cfg.tagline, P, Q, onHold, onRelease); + stage.append(built.root); + host.append(stage); + const fit = () => { + const bw = host.clientWidth, bh = host.clientHeight, scale = bw > 0 && bh > 0 ? Math.min(bw / W, bh / H) : 0; + stage.style.width = W + 'px'; stage.style.height = H + 'px'; + stage.style.left = ((bw - W * scale) / 2) + 'px'; stage.style.top = ((bh - H * scale) / 2) + 'px'; + stage.style.transform = `scale(${scale})`; + }; + fit(); + current = built; + (host as any)._fit = fit; + } else { + host.style.height = ''; + const built = build(w, 'flow', withPlate, shelves, cfg.name, cfg.tagline, P, Q, onHold, onRelease); + host.append(built.root); + current = built; + (host as any)._fit = null; + } + // Paint one frame immediately so a paused/first render is correct. + paintFrame(); + } + + function paintFrame() { + if (!current) return; + const T = reduced.matches ? HELD_FRAME : carriedT; + const st = shelfState(T, carriedMix, hover.last, shelves); + current.paint(T, st); + } + + const running = () => !reduced.matches && visible && inView; + function tick(now: number) { + if (!running()) { raf = 0; return; } + const dt = Math.min(0.05, (now - prev) / 1000); prev = now; + carriedT = (carriedT + dt) % AUTHORED; + const target = hover.idx !== null ? 1 : 0; + carriedMix += (target - carriedMix) * Math.min(1, dt * 14); + paintFrame(); + raf = requestAnimationFrame(tick); + } + function start() { if (raf || !running()) { if (!running()) paintFrame(); return; } prev = performance.now(); raf = requestAnimationFrame(tick); } + function stop() { if (raf) cancelAnimationFrame(raf); raf = 0; } + + rebuild(); + start(); + + // Reactivity: breakpoint + theme rebuild, visibility + in-view pause. + const canvasMq = matchMedia(`(min-width: ${CANVAS_MIN}px)`), plateMq = matchMedia(`(min-width: ${FLOW_PLATE_MIN}px)`); + const onBp = () => { stop(); rebuild(); start(); }; + canvasMq.addEventListener('change', onBp); + plateMq.addEventListener('change', onBp); + reduced.addEventListener('change', () => { stop(); paintFrame(); start(); }); + window.addEventListener('daemonmodechange', () => { stop(); rebuild(); start(); }); + const ro = new ResizeObserver(() => { const f = (host as any)._fit; if (f) f(); }); + ro.observe(host); + const onVis = () => { visible = !document.hidden; visible ? start() : stop(); }; + document.addEventListener('visibilitychange', onVis); + const io = new IntersectionObserver(([e]) => { inView = e.isIntersecting; inView ? start() : stop(); }); + io.observe(host); + + document.addEventListener('astro:before-swap', () => { + stop(); ro.disconnect(); io.disconnect(); + document.removeEventListener('visibilitychange', onVis); + canvasMq.removeEventListener('change', onBp); plateMq.removeEventListener('change', onBp); + }, { once: true }); + } + + document.addEventListener('astro:page-load', initHero); + if (document.readyState !== 'loading') initHero(); + else document.addEventListener('DOMContentLoaded', initHero); +</script> diff --git a/src/components/Icon.astro b/src/components/Icon.astro @@ -19,6 +19,9 @@ const paths: Record<string, string> = { 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"/>', + warn: '<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>', + info: '<circle cx="12" cy="12" r="9"/><path d="M12 16v-4M12 8h.01"/>', + lock: '<rect x="5" y="11" width="14" height="10"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>', }; const d = paths[name] ?? ''; --- diff --git a/src/components/Marquee.astro b/src/components/Marquee.astro @@ -1,157 +1,46 @@ --- -import Icon from './Icon.astro'; -import type { CategoryDef } from '../lib/taxonomy'; - +interface LaneTitle { n: string; title: string; accent: string } +interface LaneMeta { tag: string; text: string; accent: string } interface Props { - items: { cat: CategoryDef; count: number }[]; + titles: LaneTitle[]; + meta: LaneMeta[]; + /** The manifest count shown in the fixed cap. */ + manifest: string; } -const { items } = Astro.props; +const { titles, meta, manifest } = 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. + * The manifest band — two counter-scrolling lanes with a fixed inverse-plate + * cap, ported from the main site's dual-lane marquee. The title lane scrolls + * left, the meta lane scrolls right; each list is duplicated so the −50% loop + * is seamless. Both pause on hover; static under reduced motion. */ - -/* 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; +const titleRun = [...titles, ...titles]; +const metaRun = [...meta, ...meta]; --- -<!-- `.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> +<section class="vault-lanes bleed" aria-label="Manifest" data-marquee> + <div class="vault-lanes__body"> + <div class="vault-lane-wrap vault-lane-wrap--top"> + <div class="vault-lane"> + {titleRun.map((t, i) => ( + <div class="vault-title" style={`--acc: var(--${t.accent});`} aria-hidden={i >= titles.length ? 'true' : undefined}> + <span class="n">{t.n}</span><span class="t">{t.title}</span> + </div> ))} </div> - ))} + </div> + <div class="vault-lane-wrap"> + <div class="vault-lane-rev"> + {metaRun.map((m, i) => ( + <div class="vault-meta" style={`--acc: var(--${m.accent});`} aria-hidden={i >= meta.length ? 'true' : undefined}> + <span class="tag">{m.tag}</span><span class="tx">{m.text}</span><span class="sep">/</span> + </div> + ))} + </div> + </div> + </div> + <div class="vault-cap" aria-hidden="true"> + <span>Manifest ^: {manifest}</span> + <span class="verified"><i></i>Verified · MMXXVI</span> </div> - - <span class="marquee__tab marquee__tab--r">{String(items.length).padStart(2, '0')} domains</span> - - <!-- CRT film over the band. z-index 1 puts it over the track and under - the tabs (z-index 2); events pass straight through. --> - <span class="scanlines" aria-hidden="true"></span> </section> - -<style> - /* ---- Cyberdeck chrome for the band ------------------------------------- - Terminal refinements over the global `.marquee` block: JetBrains Mono - for the furniture type, hairline dividers where the tabs meet the band, - and a subdued scanline film. Scoped selectors out-rank global.css; only - the theme/palette reads stay global (they come from `.plate`). */ - .marquee__item, - .marquee__tab { - font-family: var(--font-term, 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace); - } - /* The tabs sit on their own black ground; a hairline on the seam keeps - the join from reading as a value smear against the band. */ - .marquee__tab--l { border-inline-end: 1px solid var(--rule-hi); } - .marquee__tab--r { border-inline-start: 1px solid var(--rule-hi); } - /* Subtler than the code panes' film — this band is in motion already, so - the CRT texture stays well under the type. */ - .marquee > .scanlines { opacity: 0.5; } -</style> - -<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/Operator.astro b/src/components/Operator.astro @@ -0,0 +1,71 @@ +--- +interface Props { + sheets: number; + domains: number; + mirrors: number; + updated: string; +} +const { sheets, domains, mirrors, updated } = Astro.props; + +/** + * The operator band — a dark plate with a grid texture, capability chips, a + * terminal `--status` readout and an action rail, ported from the main site's + * operator section. The rail's search field opens the Pagefind modal + * (`[data-search-open]`, bound in SearchModal); the stats and CTAs bind the + * real vault counts. + */ +const pad2 = (n: number) => String(n).padStart(2, '0'); +--- +<section class="operator bleed" aria-labelledby="op-title"> + <div class="operator__grid" aria-hidden="true"></div> + <div class="archive operator__inner"> + <div class="sec-head"> + <span class="sec-head__eyebrow" style="--tone: var(--color-night-iris);">Index / operator</span> + <h2 id="op-title" class="sec-head__title" style="--tone: var(--color-night-iris);">Start here.</h2> + <p class="sec-head__intro" style="--intro: var(--color-signal-meta);"> + {sheets} sheets across {domains} shelves, plus two full upstream mirrors. The shelves are + below; the index itself runs under them. + </p> + </div> + + <div class="operator__cols"> + <div> + <div class="op-chips"> + <span class="op-chip" style="--chip: var(--color-night-iris);"><span class="pip dmn-anim-pulse"></span>Offline search</span> + <span class="op-chip" style="--chip: var(--color-night-foam);">Copy-ready</span> + <span class="op-chip" style="--chip: var(--color-night-love);">Zero fluff</span> + </div> + <div class="op-readout"> + <div class="op-readout__cmd"> + <span class="u">daemon@ref</span><span>~</span><span>%</span><span class="c">vault --status</span><span class="dmn-blink"></span> + </div> + <dl> + <dt>sheets</dt><dd>{sheets} · curated</dd> + <dt>shelves</dt><dd>{domains} · domains</dd> + <dt>search</dt><dd><span class="on">offline</span> · fuzzy · no tracking</dd> + <dt>updated</dt><dd>{updated}</dd> + </dl> + </div> + </div> + + <div class="op-rail"> + <div class="op-rail__eyebrow">Quick search / 2026</div> + <button class="op-search" data-search-open type="button" aria-label="Search the vault"> + <span class="slash">/</span> + <span class="ph">search the vault</span> + <kbd>⌘K</kbd> + </button> + <dl class="op-stats"> + <dd><span style="color: var(--color-night-iris);">{pad2(domains)}</span> shelves</dd> + <dd><span style="color: var(--color-night-love);">{sheets}</span> sheets</dd> + <dd><span style="color: var(--color-night-gold);">{pad2(mirrors)}</span> mirrors</dd> + </dl> + <div class="op-cta-list"> + <a class="op-cta op-cta--pine" href="#index"><span>Open the index</span><span class="count">{sheets} sheets</span><span>↓</span></a> + <a class="op-cta op-cta--text" href="#shelves"><span>Pick a shelf</span><span class="count">{pad2(domains)} shelves</span><span>↓</span></a> + <a class="op-cta op-cta--love" href="#mirrors"><span>Mirrored trees</span><span class="count">{pad2(mirrors)} upstream</span><span>↓</span></a> + </div> + </div> + </div> + </div> +</section> diff --git a/src/components/RecordRow.astro b/src/components/RecordRow.astro @@ -11,32 +11,38 @@ interface Props { 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. */ + /** Trailing slot: date / page count. Dropped below md. */ meta?: string; + /** Disclosure detail, revealed on hover/focus. */ + excerpt?: string; + tagline?: string; + /** Client-side filter bucket (index page). */ + bucket?: string; } -const { href, n, title, kind, kindIcon = 'doc', accent = 'foam', meta } = Astro.props; +const { href, n, title, kind, kindIcon = 'doc', accent = 'foam', meta, excerpt, tagline, bucket } = Astro.props; /** - * One record in an index — the site's core object. + * One record in an index — the site's core object, ported from the main + * site's RecordRow. One line, not a card: the title is NOT a display heading, + * so a list of fifty stays scannable. * - * 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. + * Rule #6 of the redesign handoff: the hover is a PAPER wash (`--overlay`), + * not the solid ink wipe — the title must stay ≥4.5:1 on hover — so nothing + * here re-colours the text on hover. See `.rec` in daemon.css. */ --- -<a class="record wipe dmn-t corners" 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> +<a class="rec" href={href} style={`--acc: var(--${accent});`} data-bucket={bucket}> + <span class="rec__grid"> + <span class="rec__n">{n}</span> + <span class="rec__kind"><Icon name={kindIcon} /> {kind}</span> + <span class="rec__title">{title}</span> + <span class="rec__date">{meta}</span> + <span class="rec__arrow"><Icon name="chevron" /></span> + {(excerpt || tagline) && ( + <span class="rec__reveal"><span> + {excerpt && <span class="rec__excerpt">{excerpt}</span>} + {tagline && <span class="rec__tagline">{tagline}</span>} + </span></span> + )} </span> </a> diff --git a/src/components/SectionHeader.astro b/src/components/SectionHeader.astro @@ -0,0 +1,26 @@ +--- +interface Props { + eyebrow: string; + title: string; + intro?: string; + /** Heading + eyebrow colour — each section is keyed to a different accent. */ + tone?: 'ink' | 'pine' | 'iris' | 'love' | 'gold' | 'foam' | 'rose'; + class?: string; +} +const { eyebrow, title, intro, tone = 'ink', class: cls } = Astro.props; + +/** + * The three-column header every archive section opens with — mono eyebrow, + * oversized display heading, short intro — ported from the main site's + * SectionHeader. The eyebrow takes the section's own accent rather than a + * neutral; the heading tone is keyed to the same. Stacks below the lg break. + */ +// The eyebrow always uses an accent (never bare ink); the title follows tone. +const eyebrowVar = tone === 'ink' ? 'var(--pine)' : `var(--${tone})`; +const titleVar = tone === 'ink' ? 'var(--text)' : `var(--${tone})`; +--- +<div class:list={['sec-head', cls]}> + <span class="sec-head__eyebrow" style={`--tone: ${eyebrowVar};`}>{eyebrow}</span> + <h2 class="sec-head__title" style={`--tone: ${titleVar};`}>{title}</h2> + {intro && <p class="sec-head__intro">{intro}</p>} +</div> diff --git a/src/fonts/dm-mono-300.woff2 b/src/fonts/dm-mono-300.woff2 Binary files differ. diff --git a/src/fonts/dm-mono-400-italic.woff2 b/src/fonts/dm-mono-400-italic.woff2 Binary files differ. diff --git a/src/fonts/dm-mono-400.woff2 b/src/fonts/dm-mono-400.woff2 Binary files differ. diff --git a/src/fonts/dm-mono-500.woff2 b/src/fonts/dm-mono-500.woff2 Binary files differ. diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro @@ -6,10 +6,10 @@ import SearchModal from '../components/SearchModal.astro'; import { url } from '../lib/url'; import '../styles/app.css'; // Preload the two faces that paint above the fold — the wordmark in the -// masthead and the mono the segbar/labels use — so the header does not flash +// masthead and the DM Mono the masthead/labels use — so the header does not flash // its fallback on first paint. The rest load on demand via @font-face. import notoWordmark from '../fonts/noto-sans-display-var-latin.woff2'; -import jetbrainsMono from '../fonts/jetbrains-mono-400-latin.woff2'; +import dmMono from '../fonts/dm-mono-400.woff2'; interface Props { title?: string; @@ -32,7 +32,7 @@ const canonical = new URL(Astro.url.pathname, Astro.site).href; <link rel="canonical" href={canonical} /> <link rel="icon" href={url('favicon.svg')} type="image/svg+xml" /> <link rel="preload" href={notoWordmark} as="font" type="font/woff2" crossorigin /> - <link rel="preload" href={jetbrainsMono} as="font" type="font/woff2" crossorigin /> + <link rel="preload" href={dmMono} as="font" type="font/woff2" crossorigin /> <meta name="theme-color" content="#191724" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#faf4ed" media="(prefers-color-scheme: light)" /> <meta property="og:type" content="website" /> diff --git a/src/pages/index.astro b/src/pages/index.astro @@ -1,10 +1,11 @@ --- import Base from '../layouts/Base.astro'; -import Hero from '../components/Hero.astro'; +import HeroLoop from '../components/HeroLoop.astro'; import Marquee from '../components/Marquee.astro'; -import DomainPlate from '../components/DomainPlate.astro'; +import Operator from '../components/Operator.astro'; +import SectionHeader from '../components/SectionHeader.astro'; import RecordRow from '../components/RecordRow.astro'; -import Icon from '../components/Icon.astro'; +import Callout from '../components/Callout.astro'; import { CATEGORIES, categoryOf } from '../lib/taxonomy'; import { allSheets, categoryCounts, sheetHref } from '../lib/sheets'; import { payloadsByTopic } from '../lib/payloads'; @@ -23,105 +24,248 @@ const internalSections = await internalBySection(); const internalRoot = await internalRootPages(); const internalPages = internalSections.reduce((n, g) => n + g.count, 0) + internalRoot.length; -const marqueeItems = CATEGORIES.map((cat) => ({ cat, count: counts[cat.slug] ?? 0 })); +// Latest sheet date, for the operator readout. +const latest = sheets + .map((s) => s.data.updated) + .filter((d): d is string => !!d) + .sort() + .at(-1); +const updatedLabel = latest ? `${latest.slice(0, 7).replace('-', '.')} · latest` : '2026 · current'; -const featured = [...sheets] - .sort((a, b) => (b.data.updated ?? '').localeCompare(a.data.updated ?? '')) - .slice(0, 8); +// ── Hero: six flagship shelves, one accent each (a designed showcase; the +// full taxonomy is in the shelves grid below). Real routes + counts. ── +const HERO = [ + { slug: 'active-directory', accent: 'pine' }, + { slug: 'enumeration', accent: 'love' }, + { slug: 'exploitation', accent: 'gold' }, + { slug: 'privilege-escalation', accent: 'foam' }, + { slug: 'web', accent: 'iris' }, + { slug: 'dfir', accent: 'rose' }, +]; +const heroShelves = HERO.map((h, i) => { + const c = categoryOf(h.slug); + return { n: String(i + 1).padStart(2, '0'), label: c.tag, path: url(h.slug), accent: h.accent, desc: c.blurb }; +}); + +// ── Manifest band lanes ── +const laneTitles = [ + { n: '01', title: 'DΛΣMӨП', accent: 'iris' }, + { n: '02', title: 'Cheatsheets', accent: 'foam' }, + { n: '03', title: 'Offline search', accent: 'gold' }, + { n: '04', title: 'Copy-ready', accent: 'pine' }, + { n: '05', title: `${CATEGORIES.length} shelves`, accent: 'love' }, + { n: '06', title: 'Zero fluff', accent: 'rose' }, +]; +const laneMeta = [ + { tag: 'Sys', text: 'Open notebook', accent: 'iris' }, + { tag: 'Search', text: 'Pagefind · offline', accent: 'love' }, + { tag: 'Sheets', text: `${total} · curated`, accent: 'foam' }, + { tag: 'Theme', text: 'Rosé Pine Dawn', accent: 'gold' }, + { tag: 'Field', text: 'AD · web · privesc', accent: 'pine' }, + { tag: 'Mirror', text: 'Two upstream trees', accent: 'love' }, + { tag: 'Doctrine', text: 'Terse, not thin', accent: 'rose' }, +]; + +// ── Index rows: recent sheets, bucketed for the client-side filter ── +const byDate = [...sheets].sort((a, b) => (b.data.updated ?? '').localeCompare(a.data.updated ?? '')); +// Recent, but spread across shelves: cap 3 per category so one busy shelf +// (the CPTS workflow, freshest by date) does not fill the whole index and the +// Sheets / Workflow filters both have rows. +const perCat: Record<string, number> = {}; +const recentRaw: typeof byDate = []; +for (const entry of byDate) { + const c = entry.data.category; + perCat[c] = (perCat[c] ?? 0) + 1; + if (perCat[c] > 3) continue; + recentRaw.push(entry); + if (recentRaw.length >= 24) break; +} +const recent = recentRaw + .map((entry, i) => { + const cat = categoryOf(entry.data.category); + const bucket = entry.data.category === 'pentest-workflow' ? 'workflow' : 'sheets'; + const metaVal = entry.data.updated ? entry.data.updated.replace(/-/g, '.') : entry.data.difficulty.toUpperCase(); + const tags = entry.data.tags.slice(0, 2).join(' · '); + return { + href: url(sheetHref(entry)), + n: String(i + 1).padStart(2, '0'), + title: entry.data.title, + kind: cat.tag, + accent: cat.accent, + meta: metaVal, + excerpt: entry.data.description || `${cat.title} — terse, tested, kept current.`, + tagline: [cat.title, tags].filter(Boolean).join(' · '), + bucket, + }; + }); + +// ── Shelves grid: the full taxonomy ── +const shelfCards = CATEGORIES.map((cat, i) => ({ + n: String(i + 1).padStart(2, '0'), + slug: cat.slug, + label: cat.title, + accent: cat.accent, + count: counts[cat.slug] ?? 0, +})); + +// ── Mirrors manifest: the two swisskyrepo trees + their largest sub-trees ── +const topPayloads = [...payloadTopics].sort((a, b) => b.count - a.count).slice(0, 2); +const topInternal = [...internalSections].sort((a, b) => b.count - a.count).slice(0, 2); +const mirrorRows = [ + { n: '01', path: 'payloads/', count: payloadPages }, + { n: '02', path: 'internal/', count: internalPages }, + { n: '03', path: `payloads/${topPayloads[0]?.slug ?? ''}/`, count: topPayloads[0]?.count ?? 0 }, + { n: '04', path: `internal/${topInternal[0]?.slug ?? ''}/`, count: topInternal[0]?.count ?? 0 }, + { n: '05', path: `payloads/${topPayloads[1]?.slug ?? ''}/`, count: topPayloads[1]?.count ?? 0 }, + { n: '06', path: `internal/${topInternal[1]?.slug ?? ''}/`, count: topInternal[1]?.count ?? 0 }, +]; --- <Base> - <!-- 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} /> - - <Marquee items={marqueeItems} /> - - <div class="wrap"> - <section class="section" id="domains"> - <div class="section-head"> - <div> - <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-plates"> - {CATEGORIES.map((cat, i) => ( - <DomainPlate cat={cat} count={counts[cat.slug] ?? 0} n={String(i + 1).padStart(2, '0')} /> - ))} - </div> - </section> - </div> + <!-- 1 · Hero loop --> + <HeroLoop shelves={heroShelves} name="DΛΣMӨП-RΣF" tagline="DÆMON//SEC · Cheatsheet vault" /> - <!-- 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 corners"> - <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> + <!-- 2 · Manifest band --> + <Marquee titles={laneTitles} meta={laneMeta} manifest={String(CATEGORIES.length).padStart(2, '0')} /> + + <!-- 3 · Operator band --> + <Operator sheets={total} domains={domains} mirrors={2} updated={updatedLabel} /> + + <!-- 4 · Index --> + <section id="index" class="dsection" data-index> + <SectionHeader + eyebrow="Reference transmissions" + title="Terse, tested, kept current." + intro={`${total} sheets across ${domains} shelves, each stripped to the commands that matter.`} + tone="iris" + /> + <div class="filters"> + <button type="button" class="pill" data-filter="all" aria-pressed="true">All</button> + <button type="button" class="pill" data-filter="sheets" aria-pressed="false">Sheets</button> + <button type="button" class="pill" data-filter="workflow" aria-pressed="false">Workflow</button> + <a class="pill" href={url('payloads')}>Payloads ↗</a> + <a class="pill" href={url('internal')}>Internal ↗</a> + <span class="filters__count" data-shown>10 shown</span> </div> - </a> + <div class="index-list"> + {recent.map((r) => ( + <RecordRow + href={r.href} + n={r.n} + title={r.title} + kind={r.kind} + accent={r.accent} + meta={r.meta} + excerpt={r.excerpt} + tagline={r.tagline} + bucket={r.bucket} + /> + ))} + </div> + <button type="button" class="show-more" data-more hidden> + <span data-more-label>Show more</span><span>↓</span> + </button> + <div class="index-foot"> + <span data-foot>Showing 10 of {total} sheets</span> + <a href={url('tags')}>All {CATEGORIES.length} shelves ↗</a> + </div> + </section> + + <!-- 5 · Shelves grid (the full taxonomy — "pick your vector") --> + <section id="shelves" class="dsection"> + <SectionHeader + eyebrow="Shelves / taxonomy" + title="Pick your vector." + intro={`${CATEGORIES.length} shelves, grouped by how you actually reach for them.`} + tone="pine" + /> + <div class="shelves-grid"> + {shelfCards.map((s) => ( + <a class="shelf" href={url(s.slug)} style={`--acc: var(--${s.accent});`}> + <span class="shelf__top"><span>{s.n}</span><span>{s.count} {s.count === 1 ? 'sheet' : 'sheets'}</span></span> + <span class="shelf__title">{s.label}</span> + </a> + ))} + </div> + </section> - <a class="cta-band cta-band--internal bleed section" href={url('internal')}> - <div class="wrap cta-band__body corners"> + <!-- 6 · Mirrors band (the upstreamed repos — swisskyrepo) --> + <section id="mirrors" class="mirrors bleed" aria-labelledby="mirrors-title"> + <span class="mirrors__scrim" aria-hidden="true"></span> + <div class="archive mirrors__inner"> <div> - <p class="cta-band__eyebrow">03 ^: Full mirror · credit upstream</p> - <h2>InternalAllTheThings</h2> - <p> - The complete {internalSections.length}-section, {internalPages}-page internal-network - reference by <strong>swisskyrepo</strong> — Active Directory, cloud, containers and - red-team tradecraft, mirrored on-site and searchable offline. + <span class="mirrors__eyebrow">Third party / mirrored</span> + <h2 id="mirrors-title" class="mirrors__title">Some trees are borrowed.</h2> + <p class="mirrors__body"> + Two directories are generated mirrors of <strong>swisskyrepo</strong>'s + PayloadsAllTheThings and InternalAllTheThings — regenerated on a schedule and never + hand-edited, searchable offline, every page linked back to source. Attribution and + licences live on the credits page. </p> + <dl class="mirrors__dl"> + <div><dt>Trees</dt><dd>02</dd></div> + <div><dt>Licence</dt><dd><span class="mirrors__chip">Upstream</span></dd></div> + <div><dt>Sync</dt><dd>Scheduled</dd></div> + <div><dt>Edits</dt><dd>None</dd></div> + </dl> + <a class="mirrors__cta" href={url('credits')}>Read the credits →</a> + </div> + <div class="mirror-panel"> + <div class="mirror-panel__head"><span>Mirror manifest // upstream</span><span class="lic">swisskyrepo</span></div> + {mirrorRows.map((m) => ( + <div class="mirror-row"><span class="n">{m.n}</span><span class="path">{m.path}</span><span class="count">{m.count} files</span></div> + ))} </div> - <span class="btn">Browse internal <Icon name="arrow" /></span> - </div> - </a> - - {featured.length > 0 && ( - <div class="wrap"> - <section class="section" id="featured"> - <div class="section-head"> - <div> - <p class="eyebrow" style="--acc: var(--gold);"> - <span class="eyebrow__n">04</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="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> - )} + </section> + + <!-- 7 · Scope callout --> + <div class="archive" style="padding-top:80px;"> + <Callout tone="warning" label="Scope"> + Authorised testing, CTFs and education only. Know your scope, and get permission first, + before you touch a system. + </Callout> + </div> + <div style="height:96px"></div> </Base> + +<script> + // Index filter pills — client-side by bucket, with a show-more cap. The + // Payloads / Internal pills are real links (they navigate); All / Sheets / + // Workflow filter the visible rows. Progressive: rows are all rendered, so + // the index is complete with JS off. + function initIndexFilters() { + const root = document.querySelector('[data-index]') as HTMLElement | null; + if (!root || root.dataset.bound) return; + root.dataset.bound = '1'; + const CAP = 10; + const rows = [...root.querySelectorAll<HTMLElement>('.rec')]; + const pills = [...root.querySelectorAll<HTMLButtonElement>('button[data-filter]')]; + const shown = root.querySelector<HTMLElement>('[data-shown]'); + const foot = root.querySelector<HTMLElement>('[data-foot]'); + const more = root.querySelector<HTMLElement>('[data-more]'); + const moreLabel = root.querySelector<HTMLElement>('[data-more-label]'); + let filter = 'all', expanded = false; + + const apply = () => { + const matching = rows.filter((r) => filter === 'all' || r.dataset.bucket === filter); + rows.forEach((r) => { r.hidden = true; }); + const visible = expanded ? matching : matching.slice(0, CAP); + visible.forEach((r) => { r.hidden = false; }); + if (shown) shown.textContent = `${visible.length} shown`; + if (foot) foot.textContent = `Showing ${visible.length} of ${matching.length} ${filter === 'all' ? 'entries' : filter}`; + if (more && moreLabel) { + const rem = matching.length - visible.length; + more.hidden = rem <= 0; + moreLabel.textContent = `Show ${Math.min(CAP, rem)} more — ${rem} remaining`; + } + pills.forEach((p) => p.setAttribute('aria-pressed', String(p.dataset.filter === filter))); + }; + + pills.forEach((p) => p.addEventListener('click', () => { filter = p.dataset.filter || 'all'; expanded = false; apply(); })); + more?.addEventListener('click', () => { expanded = true; apply(); }); + apply(); + } + document.addEventListener('astro:page-load', initIndexFilters); + if (document.readyState !== 'loading') initIndexFilters(); + else document.addEventListener('DOMContentLoaded', initIndexFilters); +</script> diff --git a/src/styles/app.css b/src/styles/app.css @@ -2,3 +2,4 @@ @import './chrome.css'; @import './global.css'; @import './prose.css'; +@import './daemon.css'; diff --git a/src/styles/daemon.css b/src/styles/daemon.css @@ -0,0 +1,418 @@ +/* ============================================================================ + daemon.css — the daemon-sec.xyz form language, ported to the cheatsheet. + + The redesigned home route and the shared chrome (masthead, footer) are + re-compositions of the main site's own components: Masthead, SectionHeader, + RecordRow, Callout, Footer, the dual-lane marquee, the operator band, the + shelves grid and the mirrors band. Every value here is the main site's, + translated from its Tailwind classes into plain CSS against the tokens in + tokens.css. Nothing hardcodes a colour. + + Measure is 1560px / 44px gutter (`--container-archive`). Paper sections use + the theme-relative tokens (--base/--text/--pine…) so they invert with the + mode; the dark bands (operator, mirrors, footer, the marquee cap) pin the + fixed --color-night-* / --color-signal-* palette and stay dark in both. + ========================================================================== */ + +/* ---- Measure ------------------------------------------------------------- */ +.archive { + width: 100%; + max-width: var(--container-archive); + margin-inline: auto; + padding-inline: 44px; + box-sizing: border-box; +} +@media (max-width: 700px) { + .archive { padding-inline: clamp(18px, 5.4vw, 44px); } +} + +/* ---- Shared timing + status animations ----------------------------------- */ +.dmn-tt { + transition: + background-color 240ms ease, color 200ms ease, border-color 220ms ease, + transform 220ms var(--ease-in), padding-left 280ms var(--ease-in), opacity 200ms ease; +} +.dmn-anim-pulse { animation: dmn-pulse 2.4s ease-in-out infinite; } +.dmn-anim-ring { animation: dmn-ring-status 4.2s ease-out infinite; } +@keyframes dmn-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.25; } } +@keyframes dmn-ring-status { + 0% { transform: scale(1); opacity: 0.7; } + 70%,100% { transform: scale(2.6); opacity: 0; } +} +/* A hard terminal-cursor blink — a steps() square wave, not a fade. */ +.dmn-blink { + display: inline-block; width: 0.5em; height: 1.05em; + transform: translateY(0.12em); background: currentColor; + animation: dmn-cursor 1.1s steps(1, end) infinite; +} +@keyframes dmn-cursor { 0%,49% { opacity: 1; } 50%,100% { opacity: 0; } } +@media (prefers-reduced-motion: reduce) { + .dmn-anim-pulse, .dmn-anim-ring, .dmn-blink { animation: none; } +} + +/* A 7px status dot with a pulse and an expanding ring pushing out of it. */ +.dmn-dot { position: relative; display: block; width: 7px; height: 7px; } +.dmn-dot > i, .dmn-dot > u { + position: absolute; inset: 0; border-radius: 50%; display: block; +} +.dmn-dot > i { background: var(--dot, var(--love)); } +.dmn-dot > u { border: 1px solid var(--dot, var(--love)); } + +/* ============================================================================ + 1 · MASTHEAD — sticky, wordmark chip, numbered mono nav, status + actions. + ========================================================================== */ +.mast { + position: sticky; top: 0; z-index: 60; + border-bottom: 1px solid color-mix(in srgb, var(--text) 22%, transparent); + background: color-mix(in srgb, var(--base) 92%, transparent); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} +.mast__row { + display: flex; align-items: center; justify-content: space-between; + gap: 24px; height: 62px; +} +.mast__brand { display: inline-flex; align-items: center; } +.mast__chip { + display: inline-flex; align-items: center; gap: 7px; + background: var(--text); color: var(--base); + padding: 6px 9px 7px; + font-family: var(--font-wordmark); font-weight: 800; font-size: 15px; + line-height: 1; letter-spacing: -0.02em; font-stretch: 88%; +} +.mast__chip svg { height: 0.72em; width: auto; flex: none; transform: translateY(-0.045em); } + +.mast__nav { + display: none; align-items: center; gap: 22px; + font-family: var(--font-plex); font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase; +} +@media (min-width: 1024px) { .mast__nav { display: flex; } } +.mast__navitem { position: relative; display: flex; align-items: baseline; gap: 8px; padding: 6px 0; } +.mast__navitem .n { font-size: 10px; letter-spacing: 0.1em; color: var(--acc, var(--pine)); } +.mast__navitem .lbl { display: inline-block; color: var(--subtle); transition: color 200ms ease, transform 200ms var(--ease-in); } +.mast__navitem:hover .lbl { color: var(--text); transform: translateY(-1px); } +.mast__navitem[aria-current='page'] .lbl { color: var(--text); } +.mast__navitem .rule { + position: absolute; left: 0; bottom: -2px; height: 1px; width: 100%; + background: var(--acc, var(--pine)); transform: scaleX(0); transform-origin: left; + transition: transform 260ms var(--ease-in); +} +.mast__navitem:hover .rule, .mast__navitem[aria-current='page'] .rule { transform: scaleX(1); } + +.mast__actions { display: flex; align-items: center; gap: 14px; } +.mast__status { + display: none; align-items: center; gap: 9px; + font-family: var(--font-plex); font-size: 10.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--subtle); +} +@media (min-width: 1180px) { .mast__status { display: flex; } } +.mast__icon { + display: inline-flex; align-items: center; gap: 7px; color: var(--subtle); + transition: color 200ms ease; +} +.mast__icon:hover { color: var(--text); } +.mast__icon svg { width: 15px; height: 15px; } +.mast__icon kbd { + font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.06em; + border: 1px solid color-mix(in srgb, var(--text) 30%, transparent); padding: 1px 5px; color: var(--muted); +} +.mast__mode .icon-sun { display: inline-flex; } +.mast__mode .icon-moon { display: none; } +:root[data-theme='dark'] .mast__mode .icon-sun { display: none; } +:root[data-theme='dark'] .mast__mode .icon-moon { display: inline-flex; } + +.mast__burger { + display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px; + width: 34px; height: 34px; margin-right: -8px; +} +@media (min-width: 1024px) { .mast__burger { display: none; } } +.mast__burger i { + display: block; width: 20px; height: 1.5px; background: var(--text); + transition: opacity 200ms ease, transform 300ms var(--ease-in); +} +.mast__burger[aria-expanded='true'] i:nth-child(1) { transform: translateY(6.5px) rotate(45deg); } +.mast__burger[aria-expanded='true'] i:nth-child(2) { opacity: 0; } +.mast__burger[aria-expanded='true'] i:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); } + +.mast__mobile { border-top: 1px solid color-mix(in srgb, var(--text) 14%, transparent); background: var(--base); } +@media (min-width: 1024px) { .mast__mobile { display: none; } } +.mast__mobile a { + display: flex; align-items: center; gap: 14px; + padding: 14px 44px; border-bottom: 1px solid color-mix(in srgb, var(--text) 14%, transparent); + font-family: var(--font-plex); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--subtle); + transition: padding-left 260ms var(--ease-in), color 200ms ease; +} +.mast__mobile a:hover { padding-left: 50px; color: var(--text); } +.mast__mobile a .n { font-size: 10px; color: var(--acc, var(--pine)); } +.mast__mobile a .arrow { margin-left: auto; color: var(--acc, var(--pine)); } + +/* ============================================================================ + 2 · SECTION HEADER — three columns: mono eyebrow, oversized head, intro. + ========================================================================== */ +.sec-head { + display: grid; gap: 40px; align-items: start; + grid-template-columns: minmax(0, 0.6fr) minmax(0, 1.6fr) minmax(0, 1fr); +} +@media (max-width: 1023px) { .sec-head { grid-template-columns: 1fr; gap: 16px; } } +.sec-head__eyebrow { + padding-top: 10px; font-family: var(--font-plex); font-size: 10.5px; + letter-spacing: 0.18em; text-transform: uppercase; color: var(--tone, var(--pine)); +} +.sec-head__title { + margin: 0; font-family: var(--font-archivo); font-weight: 800; + font-size: clamp(46px, 6vw, 92px); line-height: 0.86; letter-spacing: -0.04em; + font-stretch: 88%; text-transform: uppercase; color: var(--tone, var(--text)); text-wrap: balance; +} +.sec-head__intro { + margin: 2px 0 0; max-width: 36ch; font-family: var(--font-archivo); + font-size: 15px; line-height: 1.55; color: var(--intro, var(--subtle)); text-wrap: pretty; +} + +/* Section wrapper — centred measure, top rhythm. */ +.dsection { margin-inline: auto; width: 100%; max-width: var(--container-archive); padding: 96px 44px 0; box-sizing: border-box; } +@media (max-width: 700px) { .dsection { padding: 72px clamp(18px, 5.4vw, 44px) 0; } } + +/* ============================================================================ + 3 · RECORD ROW — one line, paper wash on hover (titles stay ink, ≥4.5:1), + excerpt disclosure. Rule #6: paper wash, never the solid ink wipe. + ========================================================================== */ +.rec { + position: relative; display: block; color: var(--text); + border-bottom: 1px solid color-mix(in srgb, var(--text) 22%, transparent); + padding: 22px 28px; + background-image: linear-gradient(to right, var(--overlay) 50%, transparent 50%); + background-size: 200% 100%; background-position: 100% 0; background-repeat: no-repeat; + transition: background-position 260ms var(--ease-in); +} +.rec:hover, .rec:focus-visible { background-position: 0 0; } +.rec__grid { + display: grid; align-items: center; + grid-template-columns: 34px minmax(0, 1fr) auto; gap: 6px 18px; +} +@media (min-width: 768px) { + .rec__grid { grid-template-columns: 46px 118px minmax(0, 1fr) 112px 20px; gap: 8px 18px; } +} +.rec__n { grid-column: 1; grid-row: 1; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--iris); } +.rec__kind { + grid-column: 2; grid-row: 2; display: flex; align-items: center; gap: 8px; + font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--acc, var(--foam)); +} +.rec__kind svg { width: 11px; height: 11px; } +@media (min-width: 768px) { .rec__kind { grid-column: 2; grid-row: 1; } } +.rec__title { grid-column: 2; grid-row: 1; min-width: 0; font-family: var(--font-archivo); font-size: 17px; line-height: 1.3; font-weight: 600; } +@media (min-width: 768px) { .rec__title { grid-column: 3; } } +.rec__date { display: none; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.12em; color: var(--subtle); } +@media (min-width: 768px) { .rec__date { display: block; grid-column: 4; grid-row: 1; } } +.rec__arrow { grid-column: 3; grid-row: 1; justify-self: end; font-size: 11px; line-height: 1; color: var(--subtle); } +.rec__arrow svg { width: 13px; height: 13px; } +@media (min-width: 768px) { .rec__arrow { grid-column: 5; } } +.rec__reveal { + grid-column: 1 / -1; grid-row: 3; display: grid; grid-template-rows: 0fr; opacity: 0; + transition: grid-template-rows 380ms var(--ease-in), opacity 240ms ease; +} +@media (min-width: 768px) { .rec__reveal { grid-column: 3 / 4; grid-row: 2; } } +.rec__reveal > * { min-height: 0; overflow: hidden; } +.rec:hover .rec__reveal, .rec:focus-within .rec__reveal { grid-template-rows: 1fr; opacity: 1; } +.rec__reveal > * > * { display: block; } +.rec__excerpt { display: block; max-width: 62ch; padding-top: 10px; font-family: var(--font-archivo); font-size: 13.5px; line-height: 1.5; color: var(--subtle); text-wrap: pretty; } +.rec__tagline { display: block; padding-top: 8px; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--muted); } +@media (hover: none) { .rec__reveal { grid-template-rows: 1fr; opacity: 1; } } +@media (prefers-reduced-motion: reduce) { .rec, .rec__reveal { transition: none; } } + +/* ============================================================================ + 4 · MANIFEST BAND — dual-lane counter-scrolling marquee + inverse cap. + ========================================================================== */ +@keyframes vaultLane { from { transform: translateX(0); } to { transform: translateX(-50%); } } +@keyframes vaultLaneRev { from { transform: translateX(-50%); } to { transform: translateX(0); } } +.vault-lanes { + display: grid; grid-template-columns: 1fr auto; overflow: hidden; + background: var(--base); + border-top: 1px solid var(--text); border-bottom: 1px solid var(--text); +} +.vault-lanes__body { display: grid; grid-template-rows: 56px 34px; overflow: hidden; } +.vault-lane-wrap { position: relative; overflow: hidden; } +.vault-lane-wrap--top { border-bottom: 1px solid color-mix(in srgb, var(--text) 16%, transparent); } +.vault-lane { display: flex; align-items: center; height: 56px; width: max-content; animation: vaultLane 50s linear infinite; } +.vault-lane-rev { display: flex; align-items: center; height: 34px; width: max-content; animation: vaultLaneRev 80s linear infinite; } +.vault-lanes:hover .vault-lane, .vault-lanes:hover .vault-lane-rev { animation-play-state: paused; } +.vault-title { display: flex; align-items: center; gap: 16px; height: 56px; padding: 0 34px; white-space: nowrap; } +.vault-title .n { font-family: var(--font-plex); font-size: 12px; letter-spacing: 0.12em; padding: 3px 7px; background: var(--acc, var(--iris)); color: var(--base); } +.vault-title .t { font-family: var(--font-archivo); font-weight: 800; font-size: 30px; letter-spacing: -0.02em; line-height: 1; text-transform: uppercase; color: var(--text); } +.vault-meta { display: flex; align-items: center; gap: 12px; height: 34px; padding: 0 26px; white-space: nowrap; font-family: var(--font-plex); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; } +.vault-meta .tag { color: var(--acc, var(--iris)); letter-spacing: 0.14em; } +.vault-meta .tx { color: var(--muted); } +.vault-meta .sep { color: var(--muted); margin-left: 10px; } +.vault-cap { + display: flex; flex-direction: column; justify-content: center; gap: 6px; padding: 0 26px; white-space: nowrap; + background: var(--color-night-base); color: var(--color-signal-meta); + font-family: var(--font-plex); font-size: 12px; letter-spacing: 0.14em; text-transform: uppercase; +} +.vault-cap .verified { display: flex; align-items: center; gap: 8px; color: var(--color-night-subtle); } +.vault-cap .verified i { width: 7px; height: 7px; border-radius: 4px; background: var(--color-night-pine); display: inline-block; } +@media (prefers-reduced-motion: reduce) { .vault-lane, .vault-lane-rev { animation: none; } } +@media (max-width: 640px) { .vault-cap { display: none; } .vault-lanes { grid-template-columns: 1fr; } } + +/* ============================================================================ + 5 · OPERATOR BAND — dark plate: grid texture, chips, terminal readout, rail. + ========================================================================== */ +.operator { + position: relative; isolation: isolate; overflow: hidden; + background: var(--color-night-base); color: var(--color-night-text); padding: 92px 0 84px; +} +.operator__grid { + position: absolute; inset: 0; pointer-events: none; + background-image: + linear-gradient(color-mix(in srgb, var(--color-night-text) 5%, transparent) 1px, transparent 1px), + linear-gradient(90deg, color-mix(in srgb, var(--color-night-text) 5%, transparent) 1px, transparent 1px); + background-size: 64px 64px; +} +.operator__inner { position: relative; z-index: 2; } +.operator__cols { margin-top: 56px; display: grid; grid-template-columns: minmax(0, 1fr) minmax(320px, 420px); gap: 48px; align-items: center; } +@media (max-width: 900px) { .operator__cols { grid-template-columns: 1fr; gap: 36px; } } +.op-chips { display: flex; flex-wrap: wrap; gap: 10px; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; } +.op-chip { display: flex; align-items: center; gap: 8px; padding: 6px 11px; border: 1px solid color-mix(in srgb, var(--chip, var(--color-night-iris)) 55%, transparent); color: var(--chip, var(--color-night-iris)); } +.op-chip .pip { width: 6px; height: 6px; border-radius: 3px; background: var(--chip, var(--color-night-iris)); } +.op-readout { margin-top: 36px; max-width: 56ch; font-family: var(--font-plex); font-size: 12.5px; line-height: 1.6; } +.op-readout__cmd { display: flex; align-items: center; gap: 8px; color: var(--color-night-muted); } +.op-readout__cmd .u { color: var(--color-night-pine); } +.op-readout__cmd .c { color: var(--color-night-text); } +.op-readout dl { margin: 14px 0 0; display: grid; grid-template-columns: 5rem 1fr; gap: 7px 20px; border-top: 1px solid color-mix(in srgb, var(--color-night-text) 20%, transparent); padding-top: 14px; color: var(--color-night-subtle); } +.op-readout dt { color: var(--color-night-muted); } +.op-readout dd { margin: 0; } +.op-readout .on { color: var(--color-night-gold); } +.op-rail { border: 1px solid color-mix(in srgb, var(--color-night-text) 30%, transparent); background: var(--color-night-surface); padding: 22px 26px 26px; } +.op-rail__eyebrow { font-family: var(--font-plex); font-size: 10.5px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--color-night-pine); } +.op-search { margin-top: 14px; display: flex; align-items: center; gap: 12px; width: 100%; border: 1px solid color-mix(in srgb, var(--color-night-text) 30%, transparent); padding: 12px 14px; font-family: var(--font-plex); text-align: left; } +.op-search:hover { border-color: color-mix(in srgb, var(--color-night-text) 55%, transparent); } +.op-search .slash { color: var(--color-night-foam); font-size: 13px; } +.op-search .ph { flex: 1; color: var(--color-night-subtle); font-size: 13px; } +.op-search kbd { font-size: 10px; letter-spacing: 0.1em; color: var(--color-night-muted); border: 1px solid color-mix(in srgb, var(--color-night-text) 30%, transparent); padding: 2px 6px; } +.op-stats { margin: 18px 0 0; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px 20px; border-top: 1px solid color-mix(in srgb, var(--color-night-text) 30%, transparent); padding-top: 16px; font-family: var(--font-plex); font-size: 11px; line-height: 1.6; letter-spacing: 0.14em; text-transform: uppercase; color: var(--color-night-text); } +.op-stats dd { margin: 0; } +.op-cta-list { margin-top: 18px; display: flex; flex-direction: column; gap: 10px; font-family: var(--font-plex); font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; } +.op-cta { display: flex; align-items: center; gap: 16px; padding: 15px 20px; color: var(--color-night-base); background: var(--cta, var(--color-night-pine)); transition: background-color 220ms ease; } +.op-cta .count { margin-left: auto; opacity: 0.7; letter-spacing: 0.14em; } +.op-cta--pine:hover { background: var(--color-night-text); } +.op-cta--text { background: var(--color-night-text); } +.op-cta--text:hover { background: var(--color-night-pine); } +.op-cta--love { background: var(--color-night-love); } +.op-cta--love:hover { background: var(--color-night-text); } + +/* ============================================================================ + 6 · INDEX — filter pills + record rows + show-more + footer meta. + ========================================================================== */ +.filters { margin-top: 62px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; } +.pill { + cursor: pointer; border: 1px solid var(--text); padding: 13px 20px; + font-family: var(--font-plex); font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase; + background: transparent; color: var(--text); + transition: transform 200ms var(--ease-in), background-color 200ms ease, color 200ms ease; +} +.pill:hover { transform: translateY(-3px); background: var(--text); color: var(--base); } +.pill[aria-pressed='true'] { background: var(--text); color: var(--base); } +.filters__count { margin-left: auto; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--iris); } +.index-list { margin-top: 26px; border-top: 1px solid var(--text); } +.rec[hidden] { display: none; } +.show-more { + display: flex; width: 100%; align-items: center; justify-content: space-between; + border: 0; border-bottom: 1px solid color-mix(in srgb, var(--text) 22%, transparent); background: transparent; + padding: 24px 28px; font-family: var(--font-plex); font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--text); + transition: background-color 220ms ease, color 200ms ease; +} +.show-more:hover { background: var(--text); color: var(--base); } +.index-foot { display: flex; justify-content: space-between; padding: 26px 28px 0; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--iris); } +.index-foot a { color: var(--iris); } + +/* ============================================================================ + 7 · SHELVES GRID — 4-col hairline grid of the taxonomy. + ========================================================================== */ +.shelves-grid { + margin-top: 62px; display: grid; grid-template-columns: repeat(4, 1fr); + border-top: 1px solid var(--text); border-left: 1px solid var(--text); +} +@media (max-width: 900px) { .shelves-grid { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 520px) { .shelves-grid { grid-template-columns: 1fr; } } +.shelf { + display: flex; flex-direction: column; justify-content: space-between; gap: 36px; + min-height: 150px; padding: 18px 20px; box-sizing: border-box; color: var(--text); + border-right: 1px solid var(--text); border-bottom: 1px solid var(--text); + transition: background-color 200ms ease; +} +.shelf:hover { background: var(--panel); } +.shelf__top { display: flex; justify-content: space-between; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--acc, var(--pine)); } +.shelf__title { font-family: var(--font-archivo); font-weight: 900; font-size: 21px; line-height: 1.15; letter-spacing: -0.02em; font-stretch: 86%; text-transform: uppercase; text-wrap: pretty; } + +/* ============================================================================ + 8 · MIRRORS BAND — signal plate: borrowed trees + upstream manifest panel. + ========================================================================== */ +.mirrors { + position: relative; margin-top: 100px; overflow: hidden; + background: var(--color-signal-plate); color: var(--color-night-text); padding: 88px 0; +} +.mirrors__scrim { + position: absolute; inset: 0; pointer-events: none; + background: linear-gradient(180deg, + color-mix(in srgb, var(--color-signal-plate) 93%, transparent) 0%, + color-mix(in srgb, var(--color-signal-plate) 62%, transparent) 34%, + color-mix(in srgb, var(--color-signal-plate) 58%, transparent) 66%, + color-mix(in srgb, var(--color-signal-plate) 90%, transparent) 100%); +} +.mirrors__inner { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 56px 64px; } +@media (max-width: 900px) { .mirrors__inner { grid-template-columns: 1fr; } } +.mirrors__eyebrow { font-family: var(--font-plex); font-size: 10.5px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-signal-meta); } +.mirrors__title { margin: 14px 0 0; font-family: var(--font-archivo); font-weight: 800; font-size: clamp(46px, 6vw, 92px); line-height: 0.86; letter-spacing: -0.04em; font-stretch: 88%; text-transform: uppercase; } +.mirrors__body { margin: 20px 0 0; max-width: 46ch; font-family: var(--font-archivo); font-size: 15px; line-height: 1.6; color: var(--color-night-text); text-wrap: pretty; } +.mirrors__dl { margin: 32px 0 0; display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px 40px; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; } +.mirrors__dl dt { color: var(--color-signal-meta); } +.mirrors__dl dd { margin: 8px 0 0; color: var(--color-plate-ink); } +.mirrors__chip { display: inline-flex; border: 1px solid color-mix(in srgb, var(--color-night-love) 55%, transparent); padding: 3px 8px; color: var(--color-night-love-lift); } +.mirrors__cta { margin-top: 36px; display: inline-flex; align-items: center; gap: 12px; border: 1px solid var(--color-night-text); padding: 16px 24px; font-family: var(--font-plex); font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--color-night-text); transition: background-color 220ms ease, color 200ms ease; } +.mirrors__cta:hover { background: var(--color-night-text); color: var(--color-signal-plate); } +.mirror-panel { border: 1px solid color-mix(in srgb, var(--color-night-love) 34%, transparent); background: color-mix(in srgb, var(--color-signal-plate) 42%, transparent); } +.mirror-panel__head { display: flex; justify-content: space-between; border-bottom: 1px solid color-mix(in srgb, var(--color-night-love) 28%, transparent); padding: 14px 20px; font-family: var(--font-plex); font-size: 10.5px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--color-night-love); } +.mirror-panel__head .lic { color: var(--color-signal-dim); } +.mirror-row { display: grid; grid-template-columns: 34px 1fr 90px; align-items: center; gap: 12px; border-bottom: 1px solid color-mix(in srgb, var(--color-night-love) 14%, transparent); padding: 14px 20px; font-family: var(--font-plex); font-size: 11.5px; } +.mirror-row .n { color: var(--color-signal-meta); } +.mirror-row .path { color: var(--color-night-text); } +.mirror-row .count { color: var(--color-signal-dim); text-align: right; } + +/* ============================================================================ + 9 · CALLOUT — flat plate, 2px accent rule, mono eyebrow. + ========================================================================== */ +.dcallout { border-left: 2px solid var(--tone, var(--pine)); background: var(--surface); padding: 18px 20px; } +.dcallout__label { margin-bottom: 9px; display: flex; align-items: center; gap: 10px; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--tone, var(--pine)); } +.dcallout__label svg { width: 13px; height: 13px; } +.dcallout__body { font-family: var(--font-archivo); font-size: 15px; line-height: 1.6; color: var(--text); text-wrap: pretty; } + +/* ============================================================================ + 10 · HERO — the host the loop mounts into. The loop sets its own inner + styles inline (canvas ≥1280px, fluid flow below); this is the frame. + ========================================================================== */ +.hero-loop { position: relative; width: 100%; overflow: hidden; background: var(--base); min-height: 560px; } +.hero-loop a { color: inherit; text-decoration: none; } + +/* ============================================================================ + 11 · FOOTER — the page's closing dark band, on the signal plate. + ========================================================================== */ +.dfooter { position: relative; overflow: hidden; background: var(--color-signal-plate); color: var(--color-night-text); } +.dfooter__scrim { + position: absolute; inset: 0; pointer-events: none; + background: linear-gradient(180deg, + color-mix(in srgb, var(--color-signal-plate) 92%, transparent) 0%, + color-mix(in srgb, var(--color-signal-plate) 55%, transparent) 30%, + color-mix(in srgb, var(--color-signal-plate) 50%, transparent) 62%, + color-mix(in srgb, var(--color-signal-plate) 86%, transparent) 100%); +} +.dfooter__inner { position: relative; padding-top: 56px; padding-bottom: 64px; } +.dfooter__grid { display: grid; gap: 48px 44px; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); } +.dfooter__wordmark { font-family: var(--font-wordmark); font-weight: 800; letter-spacing: -0.025em; font-stretch: 80%; line-height: 0.88; font-size: clamp(40px, 5vw, 74px); color: var(--color-plate-ink); white-space: nowrap; } +.dfooter__wordmark .hy { color: var(--color-night-love); } +.dfooter__motto { margin-top: 20px; max-width: 32ch; font-family: var(--font-archivo); font-size: 15px; line-height: 1.55; color: var(--color-night-text); text-wrap: pretty; } +.dfooter__nav { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px 24px; align-self: start; font-family: var(--font-plex); font-size: 11px; letter-spacing: 0.14em; text-transform: uppercase; } +.dfooter__nav a { display: flex; gap: 12px; color: var(--color-plate-ink); transition: padding-left 280ms var(--ease-in), color 200ms ease; } +.dfooter__nav a:hover { padding-left: 6px; color: var(--color-night-foam); } +.dfooter__nav a .n { color: var(--acc, var(--color-night-foam)); } +.dfooter__meta { align-self: start; font-family: var(--font-plex); font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-signal-meta); } +.dfooter__meta .live { display: flex; align-items: center; gap: 10px; color: var(--color-plate-ink); } +.dfooter__meta .row { margin-top: 10px; } +.dfooter__meta a { color: var(--color-signal-meta); } +.dfooter__meta a:hover { color: var(--color-night-foam); } diff --git a/src/styles/tokens.css b/src/styles/tokens.css @@ -31,6 +31,12 @@ @font-face { font-family: 'EB Garamond'; font-style: normal; font-weight: 600; font-display: swap; src: url('../fonts/eb-garamond-600.woff2') format('woff2'); } @font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/ibm-plex-mono-400.woff2') format('woff2'); } @font-face { font-family: 'IBM Plex Mono'; font-style: normal; font-weight: 600; font-display: swap; src: url('../fonts/ibm-plex-mono-600.woff2') format('woff2'); } +/* DM Mono — the site mono, matching daemon-sec.xyz (labels, eyebrows, meta, + the redesigned homepage). Self-hosted latin subsets; no external request. */ +@font-face { font-family: 'DM Mono'; font-style: normal; font-weight: 300; font-display: swap; src: url('../fonts/dm-mono-300.woff2') format('woff2'); } +@font-face { font-family: 'DM Mono'; font-style: normal; font-weight: 400; font-display: swap; src: url('../fonts/dm-mono-400.woff2') format('woff2'); } +@font-face { font-family: 'DM Mono'; font-style: italic; font-weight: 400; font-display: swap; src: url('../fonts/dm-mono-400-italic.woff2') format('woff2'); } +@font-face { font-family: 'DM Mono'; font-style: normal; font-weight: 500; font-display: swap; src: url('../fonts/dm-mono-500.woff2') format('woff2'); } /* Noto Sans Display — variable cut, the wordmark/display face: heavy, compressed, uppercase. */ @font-face { font-family: 'Noto Sans Display'; font-style: normal; font-weight: 100 900; font-stretch: 62.5% 100%; font-display: swap; src: url('../fonts/noto-sans-display-var-latin.woff2') format('woff2'); } @@ -338,3 +344,50 @@ --fuzz-fil-3: 144 122 169; color: var(--fg); } + + +/* ============================================================================ + DÆMON main-site palette — the fixed, absolute Rosé Pine token set the + redesigned homepage and shared chrome are authored against, extracted + verbatim from daemon-sec.xyz's compiled @theme. These do NOT flip with the + mode: `dawn` is always the cream family, `night` always the ink family, + `signal` the dark plate chrome. The page's *ground* follows the mode through + the theme-relative tokens above (--base / --text / --pine …); the tokens + below name the two poles the homepage inverts between — see the hero's P/Q + and .plate / .plate-dawn. + ========================================================================== */ +:root { + /* Paper (Rosé Pine Dawn) */ + --color-dawn-base: #faf4ed; --color-dawn-surface: #fffaf3; --color-dawn-panel: #fdf8f1; --color-dawn-overlay: #f2e9e1; + --color-dawn-ink: #575279; --color-dawn-subtle: #635f7c; --color-dawn-muted: #6e6a86; + --color-dawn-pine: #017194; --color-dawn-foam: #00747b; --color-dawn-gold: #9b5800; + --color-dawn-love: #af416c; --color-dawn-iris: #795aa2; --color-dawn-rose: #b4423d; + --color-dawn-band: #4e3969; --color-dawn-band-ledger: #015a77; + + /* Plates (Rosé Pine night) */ + --color-night-base: #191724; --color-night-surface: #1f1d2e; --color-night-text: #e0def4; + --color-night-subtle: #908caa; --color-night-muted: #8b87a3; + --color-night-pine: #3e8fb0; --color-night-foam: #9ccfd8; --color-night-gold: #f6c177; + --color-night-love: #eb6f92; --color-night-love-lift: #ee85a2; + --color-night-iris: #c4a7e7; --color-night-rose: #ebbcba; + + /* Signal / plate chrome */ + --color-signal-plate: #171322; --color-signal-meta: #c9c3e0; --color-signal-dim: #9a91c4; + --color-signal-pine: #31a3c4; --color-plate-ink: #faf4ed; + + /* Type — DM Mono is now the site mono, matching daemon-sec.xyz. `--font-mono` + is re-pointed here so every existing eyebrow/label picks it up too. */ + --font-plex: 'DM Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; + --font-mono: 'DM Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace; + --font-archivo: 'Archivo', 'Helvetica Neue', Helvetica, Arial, sans-serif; + + /* The redesigned homepage's measure — wider than --maxw for the band grids. */ + --container-archive: 1560px; +} + +/* Theme-relative surfaces the paper homepage sections use, so they invert with + the mode. --line is ink@16% and rides --text automatically. */ +:root, :root[data-theme='light'] { --panel: #fdf8f1; --line: color-mix(in srgb, var(--text) 16%, transparent); } +:root[data-theme='dark'] { --panel: #1f1d2e; } +.plate { --panel: #1f1d2e; } +.plate-dawn { --panel: #fdf8f1; }