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

HeroLoop.astro (29189B)


      1 ---
      2 import SignalField from './SignalField.astro';
      3 
      4 interface Shelf { n: string; label: string; path: string; accent: string; desc: string }
      5 interface Props {
      6   shelves: Shelf[];
      7   name?: string;
      8   tagline?: string;
      9 }
     10 const { shelves, name = 'DΛΣMӨП-RΣF', tagline = 'DÆMON-REF · Cheatsheet vault' } = Astro.props;
     11 const data = JSON.stringify({ shelves, name, tagline });
     12 ---
     13 <section class="hero-loop bleed signal-hero" data-hero-loop data-hero={data} aria-label="Shelves">
     14   <SignalField />
     15   <noscript>
     16     <div style="padding:56px 44px;max-width:1560px;margin:0 auto;">
     17       <h1 style="margin:0;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}</h1>
     18       <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>
     19       <ul style="margin-top:28px;list-style:none;padding:0;border-top:1px solid var(--line);">
     20         {shelves.map((s) => (
     21           <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>
     22         ))}
     23       </ul>
     24     </div>
     25   </noscript>
     26 </section>
     27 
     28 <script>
     29   // Hero loop — a 1:1 vanilla port of the handoff's hero-loop.js (itself the
     30   // main site's hero-loop.tsx): one 21.4s rAF clock, an authored 1920×1080
     31   // canvas scaled to fit at >=1280px, a fluid flow shape below, pointer/focus
     32   // takeover with an eased `mix`, glyph decode until +0.55s, and the plate as
     33   // the inverse of the page (theme-aware P/Q). Pauses when hidden or scrolled
     34   // out; prefers-reduced-motion holds T=12.0 with no scramble.
     35 
     36   import { mountParticleField, type LitShelf, type ShelfSource } from '../scripts/dmn/particle-field';
     37   import { SHELF_ICONS } from '../scripts/dmn/shelf-icons';
     38 
     39   const W = 1920, H = 1080, PLATE_W = 784, PLATE_H = 780, MIN_HEIGHT = 560, MARQUEE = 92;
     40   const CANVAS_MIN = 1280, FLOW_PLATE_MIN = 860;
     41   const BOOT = 0, CLOSE = 18.8, AUTHORED = 21.4, HELD_FRAME = 12.0;
     42   const STARTS = [3.2, 5.8, 8.4, 11.0, 13.6, 16.2];
     43   const ENDS = STARTS.map((s, i) => (i < 5 ? STARTS[i + 1] : CLOSE));
     44   const GLYPH_POOL = 'ΛΣӨПΔΞΨΩ01#/\\_';
     45 
     46   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)' };
     47   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)' };
     48   const MONO = 'var(--font-plex)', ARCHIVO = 'var(--font-archivo)', WORDMARK_FACE = 'var(--font-wordmark)';
     49 
     50   const clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));
     51   const easeOutCubic = (p: number) => 1 - Math.pow(1 - p, 3);
     52   const easeInOutCubic = (p: number) => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2);
     53   const enter = (T: number, start: number, dur = 0.6) => { const o = easeOutCubic(clamp((T - start) / dur, 0, 1)); return { o, y: (1 - o) * 24 }; };
     54   const draw = (T: number, start: number, dur = 0.8) => easeInOutCubic(clamp((T - start) / dur, 0, 1));
     55   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)));
     56   const hash = (i: number, k: number) => { const s = Math.sin(i * 12.9898 + k * 78.233) * 43758.5453; return s - Math.floor(s); };
     57   const activeAt = (T: number) => { for (let i = 0; i < 6; i++) if (T >= STARTS[i] && T < ENDS[i]) return i; return -1; };
     58   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')}`;
     59 
     60   type Shelf = { n: string; label: string; path: string; accent: string; desc: string };
     61 
     62   function shelfState(T: number, mix: number, held: number | null, shelves: Shelf[]) {
     63     const act: number[] = [], plateAct: number[] = [], plateY: number[] = [], plateLine: number[] = [];
     64     for (let i = 0; i < shelves.length; i++) {
     65       const hv = i === held ? 1 : 0;
     66       act.push(hold(T, STARTS[i], ENDS[i]) * (1 - mix) + hv * mix);
     67       plateAct.push(hold(T, STARTS[i], ENDS[i], 0.4) * (1 - mix) + hv * mix);
     68       plateY.push((1 - enter(T, STARTS[i], 0.5).o) * 40 * (1 - mix));
     69       const line = draw(T, STARTS[i] + 0.15, 0.7) * (1 - draw(T, ENDS[i] - 0.4, 0.4));
     70       plateLine.push(line * (1 - mix) + hv * mix);
     71     }
     72     const fromClock = activeAt(T);
     73     const active = mix > 0.5 ? (held ?? fromClock) : fromClock;
     74     const standing = Math.max(hold(T, BOOT + 1.6, STARTS[0]), hold(T, CLOSE + 0.35, AUTHORED + 1)) * (1 - mix);
     75     return { act, plateAct, plateY, plateLine, standing, active, accent: active >= 0 ? shelves[active].accent : 'love' };
     76   }
     77 
     78   // Tiny DOM helper. Numeric style values need no unit here — every length is
     79   // passed as an explicit px string.
     80   function el(tag: string, style?: Record<string, string>, kids?: any): HTMLElement {
     81     const n = document.createElement(tag);
     82     if (style) Object.assign(n.style, style);
     83     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))); });
     84     return n;
     85   }
     86 
     87   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) {
     88     // Row metrics differ by shape.
     89     const M = mode === 'canvas'
     90       ? { height: '76px', columns: '64px 1fr auto', padLeft: '14px', chipSize: '14px', chipPad: '3px 7px', labelSize: '30px', pathSize: '15px', pathPad: '16px', shift: 10, arrow: true }
     91       : { 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 };
     92 
     93     // ── Shelf row ──
     94     const rows = shelves.map((s) => {
     95       const acc = P[s.accent];
     96       const wash = el('span', { position: 'absolute', inset: '0', background: P.surface, opacity: '0', borderLeft: `3px solid ${acc}`, transformOrigin: 'left', transform: 'scaleX(0)' });
     97       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);
     98       const chipWrap = el('span', { position: 'relative', display: 'flex', alignItems: 'center', paddingLeft: M.padLeft }, chip);
     99       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);
    100       const arrow = M.arrow ? el('span', { display: 'inline-block', marginLeft: '12px', opacity: '0', transform: 'translateX(-8px)' }, '→') : null;
    101       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]);
    102       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;
    103       a.href = s.path;
    104       const idx = shelves.indexOf(s);
    105       a.addEventListener('mouseenter', () => onHold(idx));
    106       a.addEventListener('mouseleave', onRelease);
    107       a.addEventListener('focus', () => onHold(idx));
    108       a.addEventListener('blur', onRelease);
    109       return { a, wash, chip, label, path, arrow };
    110     });
    111     const nav = el('nav', mode === 'canvas'
    112       ? { position: 'absolute', left: '72px', top: '424px', width: '916px' }
    113       : { marginTop: '32px' }, [...rows.map((r) => r.a), el('div', { borderTop: `1px solid ${P.line}` })]);
    114     nav.setAttribute('aria-label', 'Shelves');
    115     nav.setAttribute('data-index-zone', '');
    116 
    117     // ── Plate ──
    118     // The plate's dots: spread across it at rest and gathering into the
    119     // hovered shelf's icon (src/scripts/dmn). They replace the old random
    120     // grid, so the dots on the card are the ones that form the icon. The
    121     // anchor is the open upper half, between the chrome and the shelf block.
    122     const plateCanvas = el('canvas', { position: 'absolute', inset: '0', width: '100%', height: '100%', display: 'block' }) as HTMLCanvasElement;
    123     plateCanvas.className = 'dmn-plate-dots';
    124     const iconAnchor = el('div', { position: 'absolute', left: '40px', right: '40px', top: '92px', height: '300px' });
    125     iconAnchor.setAttribute('data-field-icon', '');
    126     const dotField = el('div', { position: 'absolute', left: '0', top: '0', width: PLATE_W + 'px', height: PLATE_H + 'px' }, [iconAnchor, plateCanvas]);
    127     const plateMeta = { fontFamily: MONO, fontSize: '14px', letterSpacing: '0.10em', textTransform: 'uppercase', color: Q.subtle };
    128     const chromeRight = el('span', { color: P.love }, '— / 06');
    129     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]);
    130     const standing = el('div', { position: 'absolute', left: '40px', right: '40px', bottom: '48px', opacity: '0', transform: 'translateY(16px)' }, [
    131       el('div', { ...plateMeta, marginBottom: '14px' }, 'Standing by'),
    132       el('div', { fontFamily: ARCHIVO, fontWeight: '700', fontSize: '64px', lineHeight: '0.98', letterSpacing: '-0.02em', color: Q.ink }, ['Security lives', el('br'), 'in the details.']),
    133       el('div', { marginTop: '22px', fontFamily: MONO, fontSize: '15px', letterSpacing: '0.04em', color: Q.muted }, '06 shelves · select an entry'),
    134     ]);
    135     const sections = shelves.map((s) => {
    136       const acc = Q[s.accent];
    137       const rule = el('div', { height: '2px', background: acc, marginTop: '26px', transformOrigin: 'left', transform: 'scaleX(0)' });
    138       const block = el('div', { position: 'absolute', left: '40px', right: '40px', bottom: '48px', opacity: '0', transform: 'translateY(40px)' }, [
    139         el('div', { display: 'flex', alignItems: 'center', gap: '14px', marginBottom: '20px' }, [
    140           el('span', { fontFamily: MONO, fontSize: '14px', letterSpacing: '0.08em', padding: '4px 8px', background: acc, color: Q.base }, s.n),
    141           el('span', { fontFamily: MONO, fontSize: '15px', letterSpacing: '0.08em', color: acc }, s.path),
    142         ]),
    143         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),
    144         rule,
    145         el('div', { marginTop: '22px', fontFamily: ARCHIVO, fontSize: '27px', lineHeight: '1.3', color: Q.subtle, maxWidth: '620px' }, s.desc),
    146         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']),
    147       ]);
    148       return { block, rule };
    149     });
    150     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)' }, [
    151       el('div', { position: 'absolute', inset: '0', background: `radial-gradient(120% 80% at 90% 100%, ${Q.surface} 0%, ${Q.base} 60%)` }),
    152       dotField, chrome,
    153       el('div', { position: 'absolute', left: '40px', right: '40px', top: '66px', height: '1px', background: Q.line }),
    154       standing, ...sections.map((s) => s.block),
    155     ]);
    156     plateSurface.setAttribute('aria-hidden', 'true');
    157     plateSurface.setAttribute('data-field-host', '');
    158 
    159     // Plate host — absolute box on canvas, aspect-ratio fitted box on flow.
    160     let plateHost: HTMLElement, plateScaleK = 1;
    161     if (mode === 'canvas') {
    162       plateHost = el('div', { position: 'absolute', left: '1064px', top: '150px', width: PLATE_W + 'px', height: PLATE_H + 'px' }, plateSurface);
    163     } else {
    164       const inner = el('div', { position: 'absolute', left: '0', top: '0', width: PLATE_W + 'px', height: PLATE_H + 'px', transformOrigin: '0 0' }, plateSurface);
    165       plateHost = el('div', { position: 'relative', width: '100%', aspectRatio: `${PLATE_W} / ${PLATE_H}`, overflow: 'hidden' }, inner);
    166       plateHost.setAttribute('aria-hidden', 'true');
    167       (plateHost as any)._inner = inner;
    168     }
    169 
    170     // ── Wordmark ──
    171     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));
    172     // The page's one heading; the glyphs scramble on arrival, so the
    173     // accessible name is the settled spelling.
    174     const wordmark = el('h1', { margin: '0', fontFamily: WORDMARK_FACE, fontWeight: '700', lineHeight: '1', letterSpacing: '-0.02em', color: P.ink, display: 'flex', whiteSpace: 'pre' }, wm);
    175     wordmark.setAttribute('aria-label', name);
    176 
    177     // ── Meta rows + subline ──
    178     const metaBase = (x?: Record<string, string>) => ({ fontFamily: MONO, fontSize: '15px', letterSpacing: '0.10em', textTransform: 'uppercase', color: P.subtle, whiteSpace: 'nowrap', ...(x || {}) });
    179     const recDot = el('div', { width: '8px', height: '8px', borderRadius: '4px', background: P.love, opacity: '1' });
    180     const timecodeEl = el('div', metaBase({ fontVariantNumeric: 'tabular-nums' }), timecode(0));
    181     const sublinePath = el('span', { color: P[shelves[0]?.accent] || P.love }, '/');
    182     let topMeta: HTMLElement, subline: HTMLElement, hairline: HTMLElement, bottomMeta: HTMLElement, root: HTMLElement;
    183 
    184     if (mode === 'canvas') {
    185       hairline = el('div', { position: 'absolute', left: '72px', right: '72px', top: '96px', height: '1px', background: P.ink, transformOrigin: 'left', transform: 'scaleX(0)', opacity: '0.9' });
    186       topMeta = el('div', { position: 'absolute', left: '72px', right: '72px', top: '56px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', opacity: '0' }, [
    187         el('div', metaBase(), tagline),
    188         el('div', { display: 'flex', alignItems: 'center', gap: '10px' }, [recDot, el('div', metaBase({ color: P.love }), 'REC')]),
    189         timecodeEl,
    190       ]);
    191       wordmark.style.fontSize = '138px';
    192       subline = el('div', metaBase({ marginTop: '18px', marginLeft: '4px', color: P.muted, opacity: '0', transform: 'translateY(24px)' }), ['Six shelves. One vault. ', sublinePath]);
    193       const wmBlock = el('div', { position: 'absolute', left: '68px', top: '178px' }, [wordmark, subline]);
    194       bottomMeta = el('div', { position: 'absolute', left: '72px', right: '72px', bottom: '56px', display: 'flex', justifyContent: 'space-between', opacity: '0' }, [
    195         el('div', metaBase({ color: P.muted }), 'Rosé Pine · Dawn · plates inverted'),
    196         el('div', metaBase({ color: P.muted }), 'Scroll ↓'),
    197       ]);
    198       root = el('div', { position: 'absolute', inset: '0', color: P.ink, opacity: '0', overflow: 'hidden', fontFamily: ARCHIVO }, [hairline, topMeta, wmBlock, nav, plateHost, bottomMeta]);
    199     } else {
    200       wordmark.style.fontSize = withPlate ? 'clamp(36px, 6.4vw, 88px)' : 'clamp(28px, 10vw, 80px)';
    201       hairline = el('div', { height: '1px', marginTop: '14px', background: P.ink, transformOrigin: 'left', transform: 'scaleX(0)', opacity: '0.9' });
    202       topMeta = el('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '12px', opacity: '0' }, [
    203         el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)' }), tagline),
    204         el('div', { display: 'flex', alignItems: 'center', gap: '8px' }, [recDot, timecodeEl]),
    205       ]);
    206       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]);
    207       const left = el('div', undefined, [wordmark, subline, nav]);
    208       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]);
    209       const centerWrap = el('div', { flex: '1', display: 'flex', flexDirection: 'column', justifyContent: 'center', padding: 'clamp(28px, 4vw, 56px) 0' }, center);
    210       bottomMeta = el('div', { paddingTop: '32px', display: 'flex', justifyContent: 'space-between', gap: '12px', opacity: '0' }, [
    211         el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)', color: P.muted }), 'Rosé Pine · Dawn'),
    212         el('div', metaBase({ fontSize: 'clamp(10px, 1.05vw, 13px)', color: P.muted }), 'Scroll ↓'),
    213       ]);
    214       root = el('div', { display: 'flex', flexDirection: 'column', minHeight: `min(calc(100svh - ${MARQUEE}px), 620px)`, padding: 'clamp(28px, 4vw, 56px) clamp(20px, 3.8vw, 72px)', color: P.ink, opacity: '0', fontFamily: ARCHIVO }, [topMeta, hairline, centerWrap, bottomMeta]);
    215     }
    216 
    217     function paintPlate(T: number, st: any, reveal: number) {
    218       plateSurface.style.clipPath = `inset(0 ${(1 - reveal) * 100}% 0 0)`;
    219       const accentName = st.active >= 0 ? shelves[st.active].accent : 'love';
    220       const accentCol = Q[accentName];
    221       chromeRight.textContent = st.active >= 0 ? `${shelves[st.active].n} / 06` : '— / 06';
    222       chromeRight.style.color = accentCol;
    223       standing.style.opacity = String(st.standing);
    224       standing.style.transform = `translateY(${(1 - st.standing) * 16}px)`;
    225       for (let i = 0; i < sections.length; i++) {
    226         sections[i].block.style.opacity = String(st.plateAct[i]);
    227         sections[i].block.style.transform = `translateY(${st.plateY[i]}px)`;
    228         sections[i].rule.style.transform = `scaleX(${st.plateLine[i]})`;
    229       }
    230     }
    231 
    232     function paint(T: number, st: any) {
    233       const master = Math.min(enter(T, BOOT, 0.5).o, easeOutCubic(clamp((AUTHORED - T) / 0.5, 0, 1)));
    234       const reveal = draw(T, BOOT + 0.9, 1.0) * (1 - draw(T, CLOSE + 1.2, 0.8));
    235       const rule = draw(T, BOOT + 0.25, 0.9), pulse = 0.55 + 0.45 * Math.sin(T * 5), sub = enter(T, BOOT + 2.1);
    236       root.style.opacity = String(master);
    237       hairline.style.transform = `scaleX(${rule})`;
    238       topMeta.style.opacity = String(enter(T, BOOT + 0.2).o);
    239       recDot.style.opacity = String(pulse);
    240       timecodeEl.textContent = timecode(st.E ?? T);
    241       bottomMeta.style.opacity = String(enter(T, BOOT + 2.4).o);
    242       // Wordmark decode
    243       const roll = Math.floor(T * 18);
    244       for (let i = 0; i < wm.length; i++) {
    245         const g = name[i], at = BOOT + 0.35 + i * 0.16, revealed = T >= at + 0.55, e = enter(T, at, 0.5);
    246         const ch = revealed ? g : GLYPH_POOL[Math.floor(hash(i, roll) * GLYPH_POOL.length)];
    247         const node = wm[i];
    248         if (node.textContent !== ch) node.textContent = ch;
    249         node.style.opacity = String(e.o);
    250         node.style.transform = `translateY(${e.y}px)`;
    251         node.style.color = g === '-' ? P.love : revealed ? P.ink : P.muted;
    252         node.style.fontFamily = revealed ? WORDMARK_FACE : MONO;
    253         node.style.fontWeight = revealed ? '700' : '500';
    254       }
    255       // Subline
    256       subline.style.opacity = String(sub.o);
    257       subline.style.transform = `translateY(${sub.y}px)`;
    258       sublinePath.textContent = st.active >= 0 ? shelves[st.active].path : '/';
    259       sublinePath.style.color = P[st.accent];
    260       // Rows
    261       for (let i = 0; i < rows.length; i++) {
    262         const a = st.act[i], s = shelves[i], acc = P[s.accent], lit = a > 0.5;
    263         const en = enter(T, BOOT + 1.3 + i * 0.13, 0.55);
    264         const R = rows[i];
    265         R.a.style.opacity = String(en.o);
    266         R.a.style.transform = `translateY(${en.y}px)`;
    267         R.wash.style.opacity = String(a);
    268         R.wash.style.transform = `scaleX(${a})`;
    269         R.chip.style.color = lit ? P.base : P.muted;
    270         R.chip.style.background = lit ? acc : 'transparent';
    271         R.chip.style.boxShadow = `inset 0 0 0 1px ${lit ? acc : P.line}`;
    272         R.label.style.color = a > 0.01 ? acc : P.ink;
    273         R.label.style.opacity = String(0.55 + 0.45 * Math.max(a, st.active >= 0 ? 0 : 1));
    274         R.label.style.transform = `translateX(${a * M.shift}px)`;
    275         R.path.style.opacity = String(0.6 + 0.4 * a);
    276         if (R.arrow) { R.arrow.style.opacity = String(a); R.arrow.style.transform = `translateX(${(1 - a) * -8}px)`; }
    277       }
    278       paintPlate(T, st, reveal);
    279       if (mode === 'flow' && withPlate) {
    280         const k = plateHost.clientWidth / PLATE_W;
    281         if (k > 0 && Math.abs(k - plateScaleK) > 0.001) { plateScaleK = k; (plateHost as any)._inner.style.transform = `scale(${k})`; }
    282       }
    283     }
    284     return { root, paint, plateCanvas };
    285   }
    286 
    287   function initHero() {
    288     const host = document.querySelector('[data-hero-loop]') as HTMLElement | null;
    289     if (!host || host.dataset.bound) return;
    290     host.dataset.bound = '1';
    291     let cfg: { shelves: Shelf[]; name: string; tagline: string };
    292     try { cfg = JSON.parse(host.dataset.hero || '{}'); } catch { return; }
    293     const shelves = cfg.shelves || [];
    294     if (!shelves.length) return;
    295 
    296     const reduced = matchMedia('(prefers-reduced-motion: reduce)');
    297     let hover: { idx: number | null; last: number | null } = { idx: null, last: null };
    298 
    299     // Which shelf a hand is holding, for the plate's icon. Hover only: the
    300     // tour cycles the rows and the plate's copy, the icon answers a pointer.
    301     let lit: LitShelf = { index: null, accent: null, held: false };
    302     const litSubs = new Set<(s: LitShelf) => void>();
    303     const shelfSource: ShelfSource = {
    304       get: () => lit,
    305       subscribe(f) { litSubs.add(f); return () => { litSubs.delete(f); }; },
    306     };
    307     const setLit = (i: number | null) => {
    308       const next: LitShelf = i === null ? { index: null, accent: null, held: false } : { index: i, accent: shelves[i].accent, held: true };
    309       if (next.index === lit.index && next.held === lit.held) return;
    310       lit = next;
    311       litSubs.forEach((f) => f(lit));
    312     };
    313     const onHold = (i: number) => { hover = { idx: i, last: i }; setLit(i); };
    314     const onRelease = () => { hover = { idx: null, last: hover.last }; setLit(null); };
    315     let destroyPlate: (() => void) | null = null;
    316     const mountPlate = (cv: HTMLCanvasElement, qFam: 'dawn' | 'night') => {
    317       destroyPlate?.();
    318       destroyPlate = mountParticleField(cv, {
    319         icons: SHELF_ICONS,
    320         shelf: shelfSource,
    321         cloud: false,
    322         restFill: true,
    323         tone: 'plate',
    324         // The plate is the page's inverse, painted from the fixed poles.
    325         vars: {
    326           ink: qFam === 'night' ? '--color-night-text' : '--color-dawn-ink',
    327           base: `--color-${qFam}-base`,
    328           iris: `--color-${qFam}-iris`,
    329           love: `--color-${qFam}-love`,
    330         },
    331         accentVar: (a) => `--color-${qFam}-${a}`,
    332       });
    333     };
    334 
    335     let current: { root: HTMLElement; paint: (T: number, st: any) => void } | null = null;
    336     let mode: 'canvas' | 'flow' | null = null, withPlate = false, dark = document.documentElement.getAttribute('data-theme') === 'dark';
    337     let carriedT = 0, carriedE = 0, carriedMix = 0, raf = 0, prev = 0, visible = !document.hidden, inView = true;
    338 
    339     function rebuild() {
    340       const w = host.clientWidth || window.innerWidth;
    341       const canvas = window.innerWidth >= CANVAS_MIN;
    342       mode = canvas ? 'canvas' : 'flow';
    343       withPlate = !canvas && window.innerWidth >= FLOW_PLATE_MIN;
    344       dark = document.documentElement.getAttribute('data-theme') === 'dark';
    345       const P = dark ? NIGHT : DAWN, Q = dark ? DAWN : NIGHT;
    346       // Preserve the field when the hero rebuilds for theme or viewport changes.
    347       for (const child of Array.from(host.children)) {
    348         if (!child.hasAttribute('data-signal')) child.remove();
    349       }
    350       if (canvas) {
    351         const stage = el('div', { position: 'absolute', left: '0', top: '0', transformOrigin: '0 0' });
    352         host.style.height = `max(${MIN_HEIGHT}px, min(calc(100vh - ${MARQUEE}px), ${(H / W) * 100}vw))`;
    353         const built = build(w, 'canvas', false, shelves, cfg.name, cfg.tagline, P, Q, onHold, onRelease);
    354         stage.append(built.root);
    355         host.append(stage);
    356         mountPlate(built.plateCanvas, dark ? 'dawn' : 'night');
    357         const fit = () => {
    358           const bw = host.clientWidth, bh = host.clientHeight, scale = bw > 0 && bh > 0 ? Math.min(bw / W, bh / H) : 0;
    359           stage.style.width = W + 'px'; stage.style.height = H + 'px';
    360           stage.style.left = ((bw - W * scale) / 2) + 'px'; stage.style.top = ((bh - H * scale) / 2) + 'px';
    361           stage.style.transform = `scale(${scale})`;
    362         };
    363         fit();
    364         current = built;
    365         (host as any)._fit = fit;
    366       } else {
    367         host.style.height = '';
    368         const built = build(w, 'flow', withPlate, shelves, cfg.name, cfg.tagline, P, Q, onHold, onRelease);
    369         host.append(built.root);
    370         if (withPlate) mountPlate(built.plateCanvas, dark ? 'dawn' : 'night');
    371         else { destroyPlate?.(); destroyPlate = null; }
    372         current = built;
    373         (host as any)._fit = null;
    374       }
    375       // Paint one frame immediately so a paused/first render is correct.
    376       paintFrame();
    377     }
    378 
    379     function paintFrame() {
    380       if (!current) return;
    381       const T = reduced.matches ? HELD_FRAME : carriedT;
    382       const st = { ...shelfState(T, carriedMix, hover.last, shelves), E: reduced.matches ? HELD_FRAME : carriedE };
    383       current.paint(T, st);
    384     }
    385 
    386     const running = () => !reduced.matches && visible && inView;
    387     function tick(now: number) {
    388       if (!running()) { raf = 0; return; }
    389       const dt = Math.min(0.05, (now - prev) / 1000); prev = now;
    390       // The boot plays once; the tour then wraps from the last shelf
    391       // straight back to the first instead of closing and booting again.
    392       // Every entrance has finished by STARTS[0], so the seam is invisible.
    393       carriedT += dt;
    394       carriedE += dt;
    395       if (carriedT >= CLOSE) carriedT = STARTS[0] + (carriedT - CLOSE);
    396       const target = hover.idx !== null ? 1 : 0;
    397       carriedMix += (target - carriedMix) * Math.min(1, dt * 14);
    398       paintFrame();
    399       raf = requestAnimationFrame(tick);
    400     }
    401     function start() { if (raf || !running()) { if (!running()) paintFrame(); return; } prev = performance.now(); raf = requestAnimationFrame(tick); }
    402     function stop() { if (raf) cancelAnimationFrame(raf); raf = 0; }
    403 
    404     rebuild();
    405     start();
    406 
    407     // Reactivity: breakpoint + theme rebuild, visibility + in-view pause.
    408     const canvasMq = matchMedia(`(min-width: ${CANVAS_MIN}px)`), plateMq = matchMedia(`(min-width: ${FLOW_PLATE_MIN}px)`);
    409     const onBp = () => { stop(); rebuild(); start(); };
    410     canvasMq.addEventListener('change', onBp);
    411     plateMq.addEventListener('change', onBp);
    412     reduced.addEventListener('change', () => { stop(); paintFrame(); start(); });
    413     window.addEventListener('daemonmodechange', () => { stop(); rebuild(); start(); });
    414     const ro = new ResizeObserver(() => { const f = (host as any)._fit; if (f) f(); });
    415     ro.observe(host);
    416     const onVis = () => { visible = !document.hidden; visible ? start() : stop(); };
    417     document.addEventListener('visibilitychange', onVis);
    418     const io = new IntersectionObserver(([e]) => { inView = e.isIntersecting; inView ? start() : stop(); });
    419     io.observe(host);
    420 
    421     document.addEventListener('astro:before-swap', () => {
    422       stop(); ro.disconnect(); io.disconnect();
    423       destroyPlate?.(); destroyPlate = null;
    424       document.removeEventListener('visibilitychange', onVis);
    425       canvasMq.removeEventListener('change', onBp); plateMq.removeEventListener('change', onBp);
    426     }, { once: true });
    427   }
    428 
    429   document.addEventListener('astro:page-load', initHero);
    430   if (document.readyState !== 'loading') initHero();
    431   else document.addEventListener('DOMContentLoaded', initHero);
    432 </script>