HeroDeck.astro (10207B)
1 --- 2 import Icon from './Icon.astro'; 3 import facets from '../data/facets.json'; 4 import { SOURCES, SOURCE_META, SOURCE_ROUTE } from '../lib/taxonomy'; 5 import { url } from '../lib/url'; 6 7 /** 8 * The home hero — a dark signal-field plate carrying the glitching, decoding 9 * DÆMONBINS wordmark and an inverse (cream) ledger of the five collections. The 10 * ledger's active deck cycles on a loop and takes over on hover/focus 11 * (scripts/hero.ts); the stat counters roll up from zero on load. The whole 12 * thing is server-rendered and fully legible with JS off — the script only 13 * adds the motion, and it all collapses under prefers-reduced-motion. 14 * 15 * Ported in spirit from daemon-sec.xyz's HeroLoop (the deck-lighting inverse 16 * plate) — rebuilt as progressive enhancement over static markup rather than a 17 * canvas cinematic, so it stays framework-free and degrades cleanly. 18 */ 19 const c = facets.counts; 20 const decks = SOURCES.map((id, i) => { 21 const s = SOURCE_META[id]; 22 return { 23 n: String(i + 1).padStart(2, '0'), 24 tag: s.tag, 25 label: s.label, 26 accent: s.accent, 27 blurb: s.blurb, 28 count: c.bySource[id] ?? 0, 29 route: SOURCE_ROUTE[id], 30 }; 31 }); 32 const WORDMARK = 'DÆMONBINS'; 33 --- 34 <section class="hero-deck plate bleed" data-hero aria-label="DÆMONBins"> 35 <canvas class="hero-deck__field" data-fuzz aria-hidden="true"></canvas> 36 <span class="banner__scrim" aria-hidden="true"></span> 37 <span class="grain grain--local" aria-hidden="true"></span> 38 <span class="scanlines" aria-hidden="true"></span> 39 40 <div class="wrap hero-deck__body"> 41 <div class="hero-deck__lede"> 42 <p class="eyebrow" style="--acc: var(--love);"> 43 <span class="eyebrow__n">00</span> 44 <span class="eyebrow__mark">^:</span> 45 <span>DÆMONBins</span> 46 <span class="eyebrow__rule" aria-hidden="true"></span> 47 </p> 48 49 <h1 class="hero-deck__wordmark glitch" data-text={WORDMARK} data-unscramble={WORDMARK}>{WORDMARK}</h1> 50 <p class="hero-deck__tagline">the Off-the-Land Almanac</p> 51 <p class="hero-deck__blurb"> 52 GTFOBins, LOLBAS, WADComs and LOOBins, with DÆMON's command references. 53 Filter by access, environment and execution context. Configure templates and keep useful commands close. 54 </p> 55 56 <div class="hero-deck__actions"> 57 <a class="btn" href={url('catalog')}><Icon name="terminal" /> Open the catalog</a> 58 <button class="btn btn--ghost" data-search-open type="button"><Icon name="search" /> Search</button> 59 <span class="hero-deck__hint">or press <kbd>/</kbd></span> 60 </div> 61 62 <div class="hero-deck__stats"> 63 <div class="hero-deck__stat" style="--acc: var(--love);"> 64 <b data-count={c.techniques}>{c.techniques.toLocaleString()}</b><span>Techniques</span> 65 </div> 66 <div class="hero-deck__stat" style="--acc: var(--foam);"> 67 <b data-count={c.tools}>{c.tools.toLocaleString()}</b><span>Tools</span> 68 </div> 69 <div class="hero-deck__stat" style="--acc: var(--gold);"> 70 <b data-count={c.added}>{c.added.toLocaleString()}</b><span>DÆMON new</span> 71 </div> 72 </div> 73 </div> 74 75 <aside class="hero-deck__panel plate-dawn corners" data-hero-panel> 76 <div class="hero-deck__panelhead"> 77 <span class="hero-deck__paneleyebrow"><span class="mark">^:</span> The collections</span> 78 <span class="hero-deck__panellive"> 79 <span class="dmn-dot" style="--dot: var(--pine);"><i class="dmn-anim-pulse"></i><u class="dmn-anim-ring"></u></span> 80 live 81 </span> 82 </div> 83 84 <p class="hero-deck__deckblurb" data-hero-blurb>{decks[0].blurb}</p> 85 86 <ul class="hero-deck__list"> 87 {decks.map((d, i) => ( 88 <li> 89 <a 90 class:list={['hero-deck__row', i === 0 && 'is-lit']} 91 href={url(d.route)} 92 style={`--acc: var(--${d.accent});`} 93 data-hero-row={i} 94 data-blurb={d.blurb} 95 aria-current={i === 0 ? 'true' : undefined} 96 > 97 <span class="hero-deck__rn">{d.n}</span> 98 <span class="hero-deck__rtag">{d.tag}</span> 99 <span class="hero-deck__rname">{d.label}</span> 100 <span class="hero-deck__rcount">{d.count.toLocaleString()}</span> 101 <span class="hero-deck__rarrow" aria-hidden="true">→</span> 102 </a> 103 </li> 104 ))} 105 </ul> 106 </aside> 107 </div> 108 </section> 109 110 <style> 111 .hero-deck { 112 position: relative; 113 overflow: hidden; 114 background: var(--plate); 115 border-bottom: 1px solid var(--fg); 116 } 117 .hero-deck__field { position: absolute; inset: 0; width: 100%; height: 100%; display: block; } 118 /* The scanlines/grain ride over the field; the body is lifted above them. */ 119 .hero-deck > .scanlines { opacity: 0.4; } 120 121 .hero-deck__body { 122 position: relative; 123 z-index: 2; 124 display: grid; 125 gap: 2rem 3.5rem; 126 align-items: center; 127 padding-block: clamp(2.4rem, 6vw, 4.6rem); 128 } 129 @media (min-width: 960px) { 130 .hero-deck__body { grid-template-columns: minmax(0, 1fr) minmax(340px, 430px); } 131 } 132 133 /* ---- Left: the lede ---------------------------------------------------- */ 134 .hero-deck__wordmark { 135 margin-top: 1.1rem; 136 font-family: var(--font-wordmark); 137 font-weight: 850; 138 font-stretch: 68%; 139 text-transform: uppercase; 140 font-size: clamp(2.7rem, 8.5vw, 5.6rem); 141 line-height: 0.9; 142 letter-spacing: -0.005em; 143 color: var(--fg); 144 } 145 .hero-deck__tagline { 146 margin-top: 0.5rem; 147 font-family: var(--font-lockup); 148 font-style: italic; 149 font-size: clamp(1.15rem, 2.6vw, 1.7rem); 150 line-height: 1.1; 151 color: var(--foam); 152 } 153 .hero-deck__blurb { 154 margin-top: 1.1rem; 155 max-width: 48ch; 156 color: var(--fg-dim); 157 font-size: 0.98rem; 158 line-height: 1.55; 159 text-wrap: pretty; 160 } 161 .hero-deck__actions { 162 margin-top: 1.5rem; 163 display: flex; 164 flex-wrap: wrap; 165 align-items: center; 166 gap: 0.7rem; 167 } 168 .hero-deck__hint { 169 font-family: var(--font-mono); 170 font-size: 11.5px; 171 letter-spacing: 0.04em; 172 color: var(--fg-faint); 173 } 174 .hero-deck__hint kbd { 175 font-family: var(--font-mono); 176 border: 1px solid var(--rule-hi); 177 padding: 0.05rem 0.35rem; 178 color: var(--fg-dim); 179 } 180 181 .hero-deck__stats { 182 margin-top: 1.9rem; 183 padding-top: 1.3rem; 184 border-top: 1px solid var(--rule); 185 display: grid; 186 grid-template-columns: repeat(3, auto); 187 justify-content: start; 188 gap: 0 clamp(1.6rem, 4vw, 3rem); 189 } 190 .hero-deck__stat b { 191 display: block; 192 font-family: var(--font-display); 193 font-weight: 800; 194 font-stretch: 84%; 195 font-size: clamp(1.7rem, 3.4vw, 2.5rem); 196 letter-spacing: -0.03em; 197 line-height: 1; 198 color: var(--acc); 199 font-variant-numeric: tabular-nums; 200 } 201 .hero-deck__stat span { 202 display: block; 203 margin-top: 0.35rem; 204 font-family: var(--font-mono); 205 font-size: 10px; 206 letter-spacing: 0.16em; 207 text-transform: uppercase; 208 color: var(--fg-faint); 209 } 210 211 /* ---- Right: the inverse deck ledger ------------------------------------ */ 212 .hero-deck__panel { 213 background: var(--base); 214 border: 1px solid var(--fg); 215 padding: 1.2rem 1.35rem 0.6rem; 216 color: var(--fg); 217 } 218 .hero-deck__panelhead { 219 display: flex; 220 align-items: center; 221 justify-content: space-between; 222 gap: 1rem; 223 padding-bottom: 0.7rem; 224 border-bottom: 1px solid var(--rule); 225 font-family: var(--font-mono); 226 font-size: 10px; 227 letter-spacing: 0.16em; 228 text-transform: uppercase; 229 color: var(--fg-faint); 230 } 231 .hero-deck__paneleyebrow .mark { color: var(--iris); margin-right: 0.35rem; } 232 .hero-deck__panellive { display: inline-flex; align-items: center; gap: 0.5rem; } 233 234 .hero-deck__deckblurb { 235 margin: 0; 236 padding: 0.9rem 0 1rem; 237 font-size: 0.86rem; 238 line-height: 1.5; 239 color: var(--fg-dim); 240 /* Clamp to four lines so the cycling copy holds a constant height and the 241 ledger below it never jumps as the active deck changes. */ 242 display: -webkit-box; 243 -webkit-line-clamp: 4; 244 line-clamp: 4; 245 -webkit-box-orient: vertical; 246 overflow: hidden; 247 min-height: calc(0.86rem * 1.5 * 4 + 1.9rem); 248 } 249 250 .hero-deck__list { list-style: none; margin: 0; padding: 0; } 251 .hero-deck__row { 252 position: relative; 253 display: grid; 254 grid-template-columns: auto auto minmax(0, 1fr) auto auto; 255 align-items: center; 256 gap: 0.7rem; 257 padding: 0.72rem 0.2rem 0.72rem 0.9rem; 258 border-top: 1px solid var(--rule); 259 color: var(--fg); 260 text-decoration: none; 261 transition: background-color 220ms ease; 262 } 263 .hero-deck__row::before { 264 content: ''; 265 position: absolute; 266 left: 0; top: 0; bottom: 0; 267 width: 2px; 268 background: var(--acc); 269 transform: scaleY(0); 270 transform-origin: 50% 50%; 271 transition: transform 260ms var(--ease-in); 272 } 273 .hero-deck__row.is-lit::before, 274 .hero-deck__row:hover::before { transform: scaleY(1); } 275 .hero-deck__row.is-lit, 276 .hero-deck__row:hover { background: var(--wash-strong); } 277 278 .hero-deck__rn { 279 font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.12em; 280 color: var(--fg-faint); transition: color 200ms ease; 281 } 282 .hero-deck__rtag { 283 font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.14em; 284 text-transform: uppercase; color: var(--acc); 285 } 286 .hero-deck__rname { 287 font-family: var(--font-display); font-weight: 700; font-stretch: 86%; 288 font-size: 1.02rem; letter-spacing: -0.01em; text-transform: uppercase; 289 color: var(--fg); transition: color 200ms ease; min-width: 0; 290 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; 291 } 292 .hero-deck__rcount { 293 font-family: var(--font-mono); font-size: 11px; color: var(--fg-faint); 294 font-variant-numeric: tabular-nums; 295 } 296 .hero-deck__rarrow { color: var(--acc); transition: transform 250ms var(--ease); } 297 .hero-deck__row.is-lit .hero-deck__rn { color: var(--acc); } 298 .hero-deck__row.is-lit .hero-deck__rname { color: var(--acc); } 299 .hero-deck__row.is-lit .hero-deck__rarrow, 300 .hero-deck__row:hover .hero-deck__rarrow { transform: translateX(4px); } 301 </style>