global.css (47348B)
1 /* ============================================================================ 2 Base elements, layout, chrome, and components. 3 Built out of the objects in chrome.css. Nothing here rounds a corner, 4 blurs a background, or casts a shadow — see the form-language note at 5 the top of tokens.css for why. 6 ========================================================================== */ 7 8 *, *::before, *::after { box-sizing: border-box; } 9 * { margin: 0; } 10 11 html { 12 color-scheme: light; 13 scroll-behavior: smooth; 14 scroll-padding-top: 82px; 15 -webkit-text-size-adjust: 100%; 16 } 17 :root[data-theme='dark'] { color-scheme: dark; } 18 19 body { 20 font-family: var(--font-body); 21 font-size: var(--step-0); 22 line-height: 1.6; 23 color: var(--fg); 24 background: var(--bg); 25 min-height: 100vh; 26 -webkit-font-smoothing: antialiased; 27 text-rendering: optimizeLegibility; 28 overflow-x: hidden; 29 } 30 31 ::selection { background: var(--selection); } 32 33 a { color: inherit; text-decoration: none; } 34 img, svg { display: block; max-width: 100%; } 35 button { font: inherit; color: inherit; cursor: pointer; background: none; border: none; } 36 37 h1, h2, h3, h4 { 38 font-family: var(--font-display); 39 font-weight: 700; 40 line-height: 1.06; 41 letter-spacing: -0.02em; 42 } 43 44 /* Square, like everything else — a rounded focus ring on a square page 45 reads as a rendering artefact rather than as focus. */ 46 :focus-visible { 47 outline: 2px solid var(--accent); 48 outline-offset: 2px; 49 } 50 51 /* Page content sits above the grain film. */ 52 main, .site-header, .site-footer { position: relative; z-index: 2; } 53 54 /* Script adds these hooks only after the content is ready, which prevents a 55 failed bundle from leaving the archive invisible. The tiny lateral drift 56 keeps the entrance in the site's terminal/register language rather than a 57 generic card float. */ 58 @media (prefers-reduced-motion: no-preference) { 59 /* The daemon-sec entrance: about a second on a curve that spends most 60 of its travel in the last fifth. */ 61 .scroll-reveal { 62 opacity: 0; 63 transform: translate3d(0, 28px, 0); 64 transition: 65 opacity var(--dur-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms), 66 transform var(--dur-reveal) var(--ease-out-expo) var(--reveal-delay, 0ms); 67 will-change: opacity, transform; 68 } 69 .scroll-reveal.is-revealed { 70 opacity: 1; 71 transform: translate3d(0, 0, 0); 72 will-change: auto; 73 } 74 } 75 76 .scroll-progress { 77 position: fixed; 78 z-index: 90; 79 inset: 0 0 auto; 80 height: 2px; 81 pointer-events: none; 82 background: linear-gradient(90deg, var(--pine), var(--iris), var(--love)); 83 transform: scaleX(var(--scroll-progress, 0)); 84 transform-origin: 0 50%; 85 } 86 @media (prefers-reduced-motion: no-preference) { 87 .scroll-progress { transition: transform 90ms linear; } 88 } 89 90 /* ---- Layout -------------------------------------------------------------- */ 91 .wrap { 92 width: 100%; 93 max-width: var(--maxw); 94 margin-inline: auto; 95 padding-inline: clamp(1.15rem, 3.2vw, 2.75rem); 96 } 97 /* Full-bleed: a band breaks the measure and runs edge to edge, but its 98 *contents* return to it. This is the page's basic rhythm — paper on the 99 measure, bands across the whole width. */ 100 .bleed { width: 100%; } 101 .stack > * + * { margin-top: var(--gap, 1rem); } 102 .accent-text { color: var(--accent); } 103 104 /* ---- Masthead ------------------------------------------------------------ */ 105 /* Opaque, not frosted: the bar has to hold its own value against whatever 106 scrolls under it, and on this page that is as often a dark band as it is 107 cream. A translucent bar over a passing band inverts mid-scroll. */ 108 .site-header { 109 position: sticky; 110 top: 0; 111 z-index: 60; 112 border-bottom: 1px solid var(--fg); 113 background: var(--base); 114 } 115 /* The `.segbar` object (chrome.css) draws a `border-block`; the header sits 116 flush with the viewport top, where that top hairline reads as a stray line 117 above the bar — keep the bottom rule only. */ 118 .site-header.segbar { border-top: none; } 119 /* No gaps inside the bar: the brand/nav split is carried by the auto margin 120 on `.mast-nav`, and the segments within each cluster sit flush so the 121 hairlines `.seg + .seg` draws are the only dividers — tmux, not a row of 122 boxed buttons. */ 123 .site-header .wrap { 124 display: flex; 125 align-items: stretch; 126 gap: 0; 127 min-height: 58px; 128 } 129 130 /* The wordmark segment of the status bar. Noto Sans Display — heavy, 131 compressed, uppercase, tracked tight — is the daemon-sec signature face; 132 the masthead brand is its smallest application. Same `.brand` hook as 133 before, so the footer lockup picks the face up too. */ 134 .brand { 135 font-family: var(--font-wordmark); 136 font-weight: 850; 137 font-stretch: 70%; 138 text-transform: uppercase; 139 font-size: 1.18rem; 140 letter-spacing: 0.01em; 141 display: inline-flex; 142 align-items: center; 143 gap: 0.06rem; 144 white-space: nowrap; 145 color: var(--fg); 146 } 147 .brand .slash { color: var(--accent); } 148 .brand .blink { color: var(--accent); } 149 @media (prefers-reduced-motion: no-preference) { 150 .brand .blink { animation: dmn-blink 1.15s steps(1) infinite; } 151 } 152 /* On the masthead the brand is the accent-filled left segment 153 (`.seg--accent`), so its ink flips to the base and the glyph garnish 154 follows — accent slashes on an accent ground would be invisible. The 155 footer lockup keeps the un-filled treatment above. Scoped rather than 156 changed in place because `.brand` is shared with the footer. */ 157 .site-header .brand { color: var(--base); } 158 .site-header .brand .slash, 159 .site-header .brand .blink { color: color-mix(in oklab, var(--base) 78%, transparent); } 160 161 /* The numbered nav. The entries are `.seg`s, so the JetBrains Mono 162 furniture type, the full-height flex and the hairline dividers all come 163 from chrome.css; what is left here is the colour logic. Each entry owns a 164 hue and its index wears it, so the bar reads as the colour legend the 165 rest of the site keys back to rather than as five grey words. The index 166 keeps its space at rest — revealing it on hover would reflow the whole 167 bar every time the pointer crossed an item. */ 168 .mast-nav { 169 display: flex; 170 align-items: stretch; 171 margin-left: auto; 172 } 173 .mast-nav a { 174 color: var(--fg-dim); 175 transition: color 200ms ease, background-color 220ms ease; 176 } 177 .mast-nav a:hover { color: var(--fg); background: var(--wash-strong); } 178 .mast-nav a[aria-current='page'] { color: var(--fg); } 179 .mast-nav .n { 180 font-size: var(--step-micro); 181 color: var(--fg-faint); 182 transition: color 200ms ease; 183 } 184 .mast-nav a:hover .n, 185 .mast-nav a[aria-current='page'] .n { color: var(--acc); } 186 @media (max-width: 900px) { .mast-nav .lbl { display: none; } } 187 @media (max-width: 640px) { .mast-nav { display: none; } } 188 189 .icon-btn.mobile-nav-trigger { display: none; } 190 .mobile-nav-glyph { 191 display: grid; 192 gap: 4px; 193 width: 16px; 194 } 195 .mobile-nav-glyph i { 196 display: block; 197 width: 100%; 198 height: 1px; 199 background: currentColor; 200 transform-origin: center; 201 transition: transform 180ms ease, opacity 140ms ease; 202 } 203 .mobile-nav-trigger[aria-expanded='true'] .mobile-nav-glyph i:first-child { 204 transform: translateY(5px) rotate(45deg); 205 } 206 .mobile-nav-trigger[aria-expanded='true'] .mobile-nav-glyph i:nth-child(2) { opacity: 0; } 207 .mobile-nav-trigger[aria-expanded='true'] .mobile-nav-glyph i:last-child { 208 transform: translateY(-5px) rotate(-45deg); 209 } 210 211 .mobile-nav { display: none; } 212 @media (max-width: 640px) { 213 .icon-btn.mobile-nav-trigger { display: grid; } 214 .mobile-nav:not([hidden]) { 215 position: absolute; 216 top: 100%; 217 left: 0; 218 right: 0; 219 display: grid; 220 background: var(--base); 221 border-bottom: 1px solid var(--fg); 222 } 223 .mobile-nav a { 224 position: relative; 225 display: grid; 226 grid-template-columns: 2.25rem 1fr; 227 align-items: center; 228 gap: 0.8rem; 229 min-height: 48px; 230 padding: 0.65rem clamp(1.15rem, 3.2vw, 2.75rem); 231 border-bottom: 1px solid var(--rule); 232 font-family: var(--font-mono); 233 font-size: 11px; 234 letter-spacing: var(--track-micro); 235 text-transform: uppercase; 236 color: var(--fg-dim); 237 } 238 .mobile-nav a:last-child { border-bottom: 0; } 239 .mobile-nav a::before { 240 content: ''; 241 position: absolute; 242 inset: 0 auto 0 0; 243 width: 3px; 244 background: var(--acc); 245 transform: scaleY(0); 246 transform-origin: 50% 100%; 247 transition: transform 180ms var(--ease-in); 248 } 249 .mobile-nav a:hover, 250 .mobile-nav a[aria-current='page'] { 251 color: var(--fg); 252 background: color-mix(in oklab, var(--acc) 9%, transparent); 253 } 254 .mobile-nav a:hover::before, 255 .mobile-nav a[aria-current='page']::before { transform: scaleY(1); } 256 .mobile-nav .n { color: var(--acc); } 257 } 258 259 /* The action cluster runs the full height of the bar, flush against the 260 nav, so its own border-left is the divider between the two clusters and 261 `.seg + .seg` draws the rest. */ 262 .mast-actions { 263 display: flex; 264 align-items: stretch; 265 border-left: 1px solid var(--rule); 266 } 267 /* The base `.icon-btn` is a small boxed control — the search modal's close 268 button still uses it as one. In the segbar the same buttons are segments, 269 so the box comes off and they stretch to the bar's full height; the 270 scoped rules below carry that without touching the modal. */ 271 .icon-btn { 272 width: 34px; height: 34px; 273 display: grid; place-items: center; 274 border: 1px solid var(--rule); 275 color: var(--fg-dim); 276 transition: color 200ms ease, border-color 220ms ease, background-color 240ms ease; 277 } 278 .icon-btn:hover { color: var(--base); background: var(--fg); border-color: var(--fg); } 279 .icon-btn svg { width: 17px; height: 17px; } 280 .mast-actions .icon-btn { 281 width: auto; height: auto; 282 padding-inline: 0.8rem; 283 border: 0; 284 } 285 .mast-actions .icon-btn:hover { color: var(--fg); background: var(--wash-strong); } 286 /* `border: 0` above ties `.seg + .seg` in specificity and would eat the 287 dividers between the action segments, so they are re-asserted here. */ 288 .mast-actions .seg + .seg { border-inline-start: 1px solid var(--rule); } 289 290 /* The search segment. `.seg` carries the JetBrains Mono furniture type and 291 the full-height flex; this block is only the state colours and the kbd. */ 292 .search-trigger { 293 color: var(--fg-dim); 294 transition: color 200ms ease, background-color 220ms ease; 295 } 296 .search-trigger:hover { color: var(--fg); background: var(--wash-strong); } 297 .search-trigger kbd { 298 font-family: var(--font-term); font-size: var(--step-micro); 299 padding: 0.08rem 0.32rem; 300 border: 1px solid var(--rule); color: var(--fg-faint); 301 } 302 .search-trigger .st-label { display: none; } 303 @media (min-width: 560px) { .search-trigger .st-label { display: inline; } } 304 305 @media (max-width: 420px) { 306 .site-header .wrap { padding-inline: 0.65rem; } 307 .brand { font-size: 0.93rem; } 308 .mast-actions .icon-btn { padding-inline: 0.55rem; } 309 .search-trigger { padding-inline: 0.55rem; } 310 .search-trigger kbd { display: none; } 311 } 312 313 /* ---- Nav rail (desktop) -------------------------------------------------- */ 314 .layout { display: grid; grid-template-columns: 1fr; gap: 2.5rem; } 315 @media (min-width: 960px) { 316 .layout.has-rail { grid-template-columns: var(--rail) minmax(0, 1fr); } 317 } 318 .rail { position: sticky; top: 78px; align-self: start; max-height: calc(100vh - 100px); overflow-y: auto; display: none; } 319 @media (min-width: 960px) { .rail { display: block; } } 320 .rail h4 { 321 font-family: var(--font-mono); font-size: var(--step-micro); 322 letter-spacing: var(--track-wide); 323 text-transform: uppercase; color: var(--fg-faint); 324 margin: 1.3rem 0 0.6rem; 325 padding-bottom: 0.5rem; 326 border-bottom: 1px solid var(--rule); 327 } 328 .rail a { 329 display: flex; align-items: center; gap: 0.5rem; 330 padding: 0.34rem 0 0.34rem 0.7rem; 331 color: var(--fg-dim); font-size: 0.88rem; 332 border-left: 2px solid var(--rule); 333 transition: color 180ms ease, border-color 180ms ease, padding-left 260ms var(--ease-in); 334 } 335 .rail a:hover { color: var(--fg); border-left-color: var(--fg); padding-left: 1rem; } 336 .rail a[aria-current='page'] { color: var(--accent); border-left-color: var(--accent); } 337 .rail .rail-tag { font-family: var(--font-mono); font-size: var(--step-micro); color: var(--fg-faint); margin-left: auto; } 338 339 /* ---- Badges + tags ------------------------------------------------------- */ 340 .badge { 341 display: inline-flex; align-items: center; gap: 0.35rem; 342 font-family: var(--font-mono); font-size: var(--step-micro); 343 letter-spacing: var(--track-micro); 344 padding: 0.22rem 0.5rem; 345 border: 1px solid var(--rule); color: var(--fg-dim); 346 text-transform: uppercase; 347 } 348 .badge svg { width: 12px; height: 12px; } 349 .badge--beginner { color: var(--foam); border-color: color-mix(in oklab, var(--foam) 50%, transparent); } 350 .badge--intermediate { color: var(--gold); border-color: color-mix(in oklab, var(--gold) 50%, transparent); } 351 .badge--advanced { color: var(--love); border-color: color-mix(in oklab, var(--love) 50%, transparent); } 352 353 .tag-pill { 354 font-family: var(--font-mono); font-size: var(--step-micro); 355 letter-spacing: var(--track-micro); 356 text-transform: uppercase; 357 padding: 0.24rem 0.55rem; 358 border: 1px solid var(--rule); color: var(--fg-dim); 359 transition: color 180ms ease, border-color 180ms ease, background-color 220ms ease; 360 } 361 .tag-pill:hover { color: var(--base); background: var(--fg); border-color: var(--fg); } 362 363 .tags-row { display: flex; flex-wrap: wrap; gap: 0.4rem; } 364 365 /* ---- Domain plates ------------------------------------------------------- */ 366 /* The twelve domains, as tiles. A tile is a slab with its hue on the top 367 edge and nowhere else: twelve fully-coloured cards would be a paint 368 chart, and the point of the colour is to key the domain, not to fill 369 the grid. */ 370 .grid-plates { 371 display: grid; 372 grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 1fr)); 373 gap: 1px; 374 background: var(--rule); 375 border: 1px solid var(--rule); 376 } 377 /* One-pixel gaps over an ink ground rather than real gutters: the grid 378 draws its own rules, so the twelve tiles read as one ruled table 379 instead of as twelve floating objects. */ 380 .plate-tile { 381 display: flex; 382 flex-direction: column; 383 gap: 0.5rem; 384 padding: 1.15rem 1.2rem 1.25rem; 385 background: var(--base); 386 border-top: 2px solid var(--acc); 387 transition: background-color 240ms ease; 388 } 389 .plate-tile:hover { background: var(--wash-strong); } 390 .plate-tile__head { 391 display: flex; 392 align-items: baseline; 393 justify-content: space-between; 394 gap: 0.6rem; 395 } 396 .plate-tile__tag { 397 font-family: var(--font-mono); font-size: var(--step-micro); 398 letter-spacing: var(--track-wide); text-transform: uppercase; 399 color: var(--acc); 400 } 401 .plate-tile__n { 402 font-family: var(--font-mono); font-size: var(--step-micro); 403 letter-spacing: var(--track-micro); color: var(--fg-faint); 404 } 405 .plate-tile__title { 406 font-family: var(--font-display); 407 font-size: 1.32rem; 408 font-weight: 700; 409 font-stretch: 84%; 410 letter-spacing: -0.03em; 411 line-height: 0.98; 412 text-transform: uppercase; 413 color: var(--fg); 414 } 415 .plate-tile__blurb { color: var(--fg-dim); font-size: 0.875rem; line-height: 1.5; } 416 .plate-tile__meta { 417 margin-top: auto; 418 padding-top: 0.7rem; 419 display: flex; align-items: center; justify-content: space-between; 420 font-family: var(--font-mono); font-size: var(--step-micro); 421 letter-spacing: var(--track-micro); text-transform: uppercase; 422 color: var(--fg-faint); 423 } 424 .plate-tile__arrow { color: var(--acc); transition: transform 250ms var(--ease); } 425 .plate-tile:hover .plate-tile__arrow { transform: translateX(4px); } 426 427 /* ---- Record rows --------------------------------------------------------- */ 428 /* The archive's core object: number, kind mark, title, meta — one line, 429 not a card. A title set as a display heading gives every entry the 430 weight of a section and makes a list of fifty unscannable; the point of 431 an index is the shape of the whole list, and that only reads if a row 432 is a row. */ 433 .index { border-top: 1px solid var(--rule); } 434 .record { 435 display: block; 436 border-bottom: 1px solid var(--rule); 437 padding: 1.05rem clamp(0.6rem, 1.6vw, 1.3rem); 438 color: var(--fg); 439 } 440 .record:hover { color: var(--base); } 441 /* Every cell placed explicitly rather than left to auto-flow. Narrow, the 442 row is two lines — number, title and chevron across the top, kind 443 underneath — and at md it collapses to the single line the index is 444 really for. Auto-placement cannot express both: the title has to take 445 the full middle on mobile, and the moment it does the chevron is pushed 446 onto a line of its own. */ 447 .record__grid { 448 display: grid; 449 grid-template-columns: 30px minmax(0, 1fr) auto; 450 align-items: center; 451 column-gap: 1.05rem; 452 row-gap: 0.35rem; 453 } 454 @media (min-width: 768px) { 455 .record__grid { grid-template-columns: 42px 112px minmax(0, 1fr) 108px 18px; row-gap: 0; } 456 } 457 .record__n { 458 grid-column: 1; grid-row: 1; 459 font-family: var(--font-mono); font-size: var(--step-micro); 460 letter-spacing: var(--track-micro); text-transform: uppercase; 461 color: var(--accent); 462 } 463 .record:hover .record__n { color: rgba(250, 244, 237, 0.78); } 464 .record__kind { 465 grid-column: 2; grid-row: 2; 466 display: flex; align-items: center; gap: 0.45rem; 467 font-family: var(--font-mono); font-size: var(--step-micro); 468 letter-spacing: var(--track-micro); text-transform: uppercase; 469 color: var(--acc, var(--fg-faint)); 470 } 471 @media (min-width: 768px) { .record__kind { grid-row: 1; } } 472 /* On the hover ink the dawn accents sit at roughly 1:1, so every one of 473 them takes a cream weight rather than keeping a hue nobody could read. 474 /78 is the floor for the meta layer — cream on ink clears AA at /74 and 475 these are 10px, so they get a little over the line rather than sitting 476 on it. */ 477 .record:hover .record__kind, 478 .record:hover .record__meta { color: rgba(250, 244, 237, 0.78); } 479 .record__kind svg { width: 12px; height: 12px; } 480 .record__title { 481 grid-column: 2; grid-row: 1; 482 font-family: var(--font-display); 483 font-size: 1.02rem; 484 font-weight: 600; 485 letter-spacing: -0.015em; 486 min-width: 0; 487 overflow: hidden; 488 text-overflow: ellipsis; 489 white-space: nowrap; 490 } 491 @media (min-width: 768px) { .record__title { grid-column: 3; } } 492 .record__meta { 493 display: none; 494 font-family: var(--font-mono); font-size: var(--step-micro); 495 letter-spacing: var(--track-micro); text-transform: uppercase; 496 color: var(--fg-faint); 497 } 498 @media (min-width: 768px) { .record__meta { display: block; grid-column: 4; grid-row: 1; } } 499 .record__arrow { 500 grid-column: 3; grid-row: 1; 501 color: var(--fg-faint); 502 transition: transform 250ms var(--ease); 503 } 504 @media (min-width: 768px) { .record__arrow { grid-column: 5; } } 505 .record:hover .record__arrow { color: rgba(250, 244, 237, 0.78); transform: translateX(3px); } 506 .record__arrow svg { width: 15px; height: 15px; } 507 508 /* ---- Hero ---------------------------------------------------------------- */ 509 /* The lockup over the ledger. Full-bleed cream with an ink rule under it, 510 which is the page's hardest value break and the reason the marquee 511 below needs no top edge of its own. */ 512 .hero { 513 position: relative; 514 overflow: hidden; 515 border-bottom: 1px solid var(--fg); 516 background: var(--surface); 517 } 518 .hero__inner { padding-top: clamp(1.6rem, 3.4vw, 2.6rem); } 519 /* Sized in `cqw` against the text column rather than in `vw` against the 520 window, so the lockup fills the space it actually has. In `vw` it is 521 sized against a viewport the column stops tracking at `--maxw`, and past 522 that width the type and the rule under it drift out of line with the 523 rest of the page. */ 524 .hero__lockup { container-type: inline-size; } 525 .hero__line { height: 18.1cqw; } 526 .hero__word { 527 font-family: var(--font-lockup); 528 font-size: 21cqw; 529 line-height: 0.86; 530 font-weight: 600; 531 letter-spacing: 0.012em; 532 color: var(--fg); 533 } 534 @media (prefers-reduced-motion: no-preference) { 535 /* The line keeps settling after the letters have landed: the glyphs 536 finish at 780ms, the tracking at 1500ms. */ 537 .hero__word { animation: dmn-track-in 1500ms var(--ease-in) both; } 538 .hero__glyph { animation: dmn-up 780ms var(--ease-in) both; } 539 } 540 .hero__glyph { 541 display: inline-block; 542 transition: color 220ms ease; 543 } 544 /* Six letters, six accents, taken under the pointer rather than worn at 545 rest — the lockup teaches the key the rest of the site is coloured by, 546 but only to someone who goes looking. */ 547 .hero__glyph:nth-child(1):hover { color: var(--pine); } 548 .hero__glyph:nth-child(2):hover { color: var(--iris); } 549 .hero__glyph:nth-child(3):hover { color: var(--love); } 550 .hero__glyph:nth-child(4):hover { color: var(--rose); } 551 .hero__glyph:nth-child(5):hover { color: var(--gold); } 552 .hero__glyph:nth-child(6):hover { color: var(--foam); } 553 554 /* Ink rule. Timed to land as the last glyph settles — six glyphs at 54ms 555 plus the tail of the 780ms keyframe. Width in `cqw` for the same reason 556 the lockup is: a viewport-relative rule outgrows the column. */ 557 .hero__rule { 558 display: block; 559 height: 3px; 560 width: 64cqw; 561 margin-top: 0.6cqw; 562 transform-origin: 0 50%; 563 background: var(--pine); 564 } 565 @media (prefers-reduced-motion: no-preference) { 566 .hero__rule { animation: dmn-draw 720ms var(--ease-in) 780ms both; } 567 } 568 .hero__subline { height: 7cqw; margin-top: 0.9cqw; } 569 .hero__sub { 570 font-family: var(--font-display); 571 font-size: 5.5cqw; 572 line-height: 0.82; 573 font-weight: 700; 574 font-stretch: 62%; 575 letter-spacing: 0.045em; 576 text-transform: uppercase; 577 color: var(--fg); 578 transform: scaleY(1.5); 579 } 580 /* One gesture rather than letter by letter: it is a subtitle, and two 581 per-glyph runs in a row read as an effect rather than as an entrance. */ 582 .hero__sub > span { display: inline-block; } 583 @media (prefers-reduced-motion: no-preference) { 584 .hero__sub > span { animation: dmn-wipe-in 860ms cubic-bezier(0.22, 0.61, 0.36, 1) 380ms both; } 585 } 586 587 .hero__ledger { 588 margin-top: clamp(1.4rem, 3vw, 2.4rem); 589 padding-bottom: clamp(2rem, 4vw, 3rem); 590 } 591 .hero__card { 592 display: grid; 593 gap: 1.6rem 2.6rem; 594 border: 1px solid var(--fg); 595 background: var(--base); 596 padding: clamp(1.3rem, 2.4vw, 1.9rem); 597 } 598 @media (min-width: 720px) { .hero__card { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); } } 599 .hero__eyebrow { 600 font-family: var(--font-mono); font-size: 10.5px; 601 letter-spacing: 0.2em; text-transform: uppercase; color: var(--pine); 602 } 603 .hero__h2 { 604 margin-top: 0.85rem; 605 font-family: var(--font-display); 606 font-size: clamp(1.6rem, 2.5vw, 2.4rem); 607 line-height: 0.94; 608 font-weight: 700; 609 font-stretch: 84%; 610 letter-spacing: -0.03em; 611 text-transform: uppercase; 612 } 613 .hero__blurb { margin-top: 0.9rem; max-width: 46ch; color: var(--fg); font-size: 0.95rem; line-height: 1.5; } 614 .hero__actions { margin-top: 1.2rem; display: flex; flex-wrap: wrap; gap: 0.6rem; } 615 616 .hero__stats { 617 display: grid; 618 grid-template-columns: repeat(auto-fit, minmax(94px, 1fr)); 619 gap: 1rem 1.3rem; 620 align-content: start; 621 border-top: 1px solid var(--fg); 622 padding-top: 1.1rem; 623 font-family: var(--font-mono); 624 font-size: 11px; 625 line-height: 1.6; 626 letter-spacing: 0.14em; 627 text-transform: uppercase; 628 } 629 .hero__stat b { font-weight: 400; } 630 .hero__stat--iris b { color: var(--iris); } 631 .hero__stat--love b { color: var(--love); } 632 .hero__stat--gold b { color: var(--gold); } 633 634 /* The shell readout beside the card. Borderless on purpose: the card is 635 the printed spec, this is the terminal output next to it, so the two 636 read as different registers rather than as two boxes. */ 637 .whoami { font-family: var(--font-mono); font-size: 12.5px; line-height: 1.6; max-width: 58ch; } 638 .whoami__cmd { display: flex; align-items: center; gap: 0.5rem; color: var(--fg-faint); flex-wrap: wrap; } 639 .whoami__cmd .u { color: var(--pine); } 640 .whoami__cmd .c { color: var(--fg); } 641 .whoami__caret { 642 display: inline-block; width: 0.5em; height: 1.05em; 643 background: color-mix(in oklab, var(--fg) 80%, transparent); 644 transform: translateY(0.12em); 645 } 646 @media (prefers-reduced-motion: no-preference) { 647 .whoami__caret { animation: dmn-blink 1.1s steps(1, end) infinite; } 648 } 649 .whoami__rows { 650 margin-top: 0.9rem; 651 padding-top: 0.9rem; 652 border-top: 1px solid var(--rule-faint); 653 display: grid; 654 grid-template-columns: 4.2rem 1fr; 655 gap: 0.45rem 1.2rem; 656 color: var(--fg-dim); 657 } 658 .whoami__rows dt { color: var(--fg-faint); } 659 .whoami__rows dd { display: flex; align-items: center; gap: 0.5rem; } 660 661 /* ---- Marquee band -------------------------------------------------------- */ 662 /* The index grammar run sideways. The band is a ground, not an accent — 663 see the `--band` note in tokens.css. 664 665 The bottom edge is the one at risk: it meets the page's next section at 666 too close a value to carry itself, which is why the ink rule below is 667 load-bearing rather than decorative. Do not drop it. */ 668 .marquee { 669 position: relative; 670 overflow: hidden; 671 background: var(--band); 672 /* Ink, written out rather than taken from `--fg`: the band pins the 673 night palette for its contents, which would flip this rule to a light 674 one. The edge below meets the cream page and the black tabs meet the 675 section under them, and in both cases it is a *dark* rule that keeps 676 the seam from bleeding. */ 677 border-bottom: 1px solid #575279; 678 --marquee-fg: #faf4ed; 679 /* /78, not /62. Cream on this band clears AA at /74, and these are the 680 smallest words on the page *and* the only ones in motion, so they get 681 a little over the line rather than sitting on it. At /62 the index 682 numbers and sheet counts were legible in a still frame and mush while 683 the band was moving. */ 684 --marquee-dim: rgba(250, 244, 237, 0.78); 685 686 /* Pine, lifted for this band only. 687 Every other night accent clears its floor on `--band`: iris 4.75, 688 foam 5.83, gold 6.06, rose 5.88, love 3.42 — all at or above the 3:1 689 large-text floor the display titles need. Published night pine 690 (#3e8fb0) lands at 2.73 and is the one that fails, because it is the 691 accent nearest the ground in both hue and value. 692 #6fb3d0 is the same hue walked up until it clears with headroom 693 (4.28:1) while staying visibly pine rather than sliding into foam, 694 which sits beside it in the rotation and would collapse two domains 695 onto one colour. */ 696 --pine: #6fb3d0; 697 } 698 .marquee__track { 699 display: flex; 700 width: max-content; 701 will-change: transform; 702 } 703 /* Two identical runs translated as one unit to -50%, which lands the 704 second exactly where the first started — the reset is invisible. */ 705 .marquee__run { 706 display: flex; 707 flex: none; 708 align-items: center; 709 } 710 .marquee__item { 711 display: flex; 712 align-items: baseline; 713 gap: 0.7rem; 714 padding: 0.95rem clamp(1.1rem, 2.4vw, 2rem); 715 white-space: nowrap; 716 font-family: var(--font-mono); 717 /* 11px, not the 10px `--step-micro` every other label on the site uses. 718 This is the one place the furniture type is in motion, and a word 719 sliding past at 70px/s needs the extra pixel that a static eyebrow 720 does not. */ 721 font-size: 11px; 722 letter-spacing: var(--track-micro); 723 text-transform: uppercase; 724 color: var(--marquee-fg); 725 } 726 .marquee__n { color: var(--marquee-dim); } 727 /* The kind *word* stays cream and only its icon takes the hue. Colouring 728 both put three competing colours in every entry, which is what made the 729 band read as noise rather than as an index. */ 730 .marquee__kind { color: var(--marquee-fg); display: inline-flex; align-items: center; gap: 0.4rem; } 731 .marquee__kind svg { width: 11px; height: 11px; color: var(--acc); } 732 /* The credential in display type, the qualifier trailing in the date slot 733 — record-row grammar, horizontally. */ 734 .marquee__title { 735 font-family: var(--font-display); 736 font-size: 1.06rem; 737 font-weight: 700; 738 font-stretch: 84%; 739 letter-spacing: -0.02em; 740 text-transform: uppercase; 741 color: var(--acc); 742 } 743 .marquee__qual { color: var(--marquee-dim); } 744 @media (max-width: 720px) { .marquee__qual { display: none; } } 745 746 /* The band's own tab — `^:` survives in exactly one place. It is the 747 banner's self-naming mark, and the band names itself once; repeated on 748 every record it was furniture. The tab counts its records, because every 749 index on this site counts itself. */ 750 .marquee__tab { 751 position: absolute; 752 top: 0; bottom: 0; 753 z-index: 2; 754 display: flex; 755 align-items: center; 756 gap: 0.5rem; 757 padding-inline: clamp(0.9rem, 2.2vw, 1.5rem); 758 /* The tabs are their own ground: Rosé Pine's black, which is the page's 759 furniture ink rather than another colour competing with the band. The 760 band pins the night palette (`.plate`), so `--base` is that black in 761 both modes. */ 762 background: var(--base); 763 color: #faf4ed; 764 font-family: var(--font-mono); 765 font-size: var(--step-micro); 766 letter-spacing: var(--track-wide); 767 text-transform: uppercase; 768 } 769 .marquee__tab--l { left: 0; } 770 .marquee__tab--r { right: 0; } 771 @media (max-width: 640px) { .marquee__tab--r { display: none; } } 772 .marquee__tab .dot { color: #9ccfd8; } 773 774 /* ---- Section banner ------------------------------------------------------ */ 775 /* The left scrim softens the hero field beneath the heading. */ 776 .banner { 777 position: relative; 778 overflow: hidden; 779 background: var(--plate); 780 border-bottom: 1px solid var(--fg); 781 } 782 .banner__scrim { 783 position: absolute; 784 top: 0; bottom: 0; left: 0; 785 width: 68%; 786 pointer-events: none; 787 background: linear-gradient( 788 90deg, 789 rgb(var(--plate-scrim) / 0.95) 0%, 790 rgb(var(--plate-scrim) / 0.86) 52%, 791 rgb(var(--plate-scrim) / 0) 100% 792 ); 793 } 794 .banner__body { 795 position: relative; 796 z-index: 2; 797 display: grid; 798 gap: 1.8rem 2.6rem; 799 align-items: end; 800 padding-block: clamp(1.5rem, 3.2vw, 2.2rem); 801 } 802 @media (min-width: 900px) { .banner__body { grid-template-columns: minmax(0, 1fr) 280px; } } 803 .banner__title { 804 margin-top: 0.85rem; 805 font-family: var(--font-display); 806 font-size: clamp(1.9rem, 4.2vw, 3.2rem); 807 line-height: 0.92; 808 font-weight: 700; 809 font-stretch: 82%; 810 letter-spacing: -0.035em; 811 text-transform: uppercase; 812 color: var(--fg); 813 } 814 .banner__blurb { margin-top: 0.85rem; max-width: 58ch; color: var(--fg-dim); font-size: 0.95rem; line-height: 1.55; } 815 .banner__ledger { 816 display: grid; 817 gap: 0.55rem; 818 font-family: var(--font-mono); 819 font-size: 11px; 820 letter-spacing: var(--track-micro); 821 text-transform: uppercase; 822 } 823 .banner__row { 824 display: flex; align-items: baseline; justify-content: space-between; 825 gap: 1rem; 826 padding-bottom: 0.5rem; 827 border-bottom: 1px solid var(--rule); 828 color: var(--fg-faint); 829 } 830 .banner__row b { font-weight: 400; color: var(--fg); } 831 .banner__row--acc b { color: var(--acc, var(--accent)); } 832 833 /* ---- Section headers ----------------------------------------------------- */ 834 .section { margin-top: clamp(2.4rem, 5.5vw, 4rem); } 835 .section:last-child { padding-bottom: clamp(2.5rem, 6vw, 4.5rem); } 836 .section-head { 837 display: flex; align-items: flex-end; justify-content: space-between; 838 gap: 1rem; margin-bottom: 1.2rem; 839 } 840 .section-head h2 { 841 margin-top: 0.6rem; 842 font-family: var(--font-display); 843 font-size: clamp(1.5rem, 2.8vw, 2.2rem); 844 font-weight: 700; 845 font-stretch: 84%; 846 letter-spacing: -0.03em; 847 line-height: 0.94; 848 text-transform: uppercase; 849 } 850 851 /* ---- Payloads call-to-action band ---------------------------------------- */ 852 .cta-band { 853 --cta-ground: var(--band-ledger); 854 display: block; 855 background: var(--cta-ground); 856 border-block: 1px solid var(--fg); 857 color: #faf4ed; 858 } 859 .cta-band--internal { --cta-ground: var(--band-internal); } 860 .cta-band__body { 861 display: flex; align-items: center; justify-content: space-between; 862 gap: 1.6rem; flex-wrap: wrap; 863 padding-block: clamp(1.5rem, 3.2vw, 2.3rem); 864 } 865 .cta-band__eyebrow { 866 font-family: var(--font-mono); font-size: var(--step-micro); 867 letter-spacing: var(--track-wide); text-transform: uppercase; 868 color: rgba(250, 244, 237, 0.72); 869 } 870 .cta-band h2 { 871 margin-top: 0.6rem; 872 font-family: var(--font-display); 873 font-size: clamp(1.7rem, 3.4vw, 2.6rem); 874 font-weight: 700; font-stretch: 82%; 875 letter-spacing: -0.035em; line-height: 0.94; 876 text-transform: uppercase; 877 color: #faf4ed; 878 } 879 .cta-band p { margin-top: 0.7rem; max-width: 54ch; color: rgba(250, 244, 237, 0.85); font-size: 0.93rem; line-height: 1.55; } 880 .cta-band .btn { 881 background: transparent; color: #faf4ed; 882 border-color: rgba(250, 244, 237, 0.5); 883 flex: none; 884 } 885 .cta-band:hover .btn { background: #faf4ed; color: var(--cta-ground); border-color: #faf4ed; } 886 887 /* ---- Sheet page ---------------------------------------------------------- */ 888 /* Same measure as the body below it, so the title, the rule under the 889 eyebrow and the prose all start and end on the same two lines. */ 890 .sheet-head { margin-top: 1.6rem; max-width: 1080px; } 891 .sheet-head.plate { 892 max-width: none; 893 margin-top: 0; 894 margin-left: calc(-1 * var(--gutter, 1.25rem)); 895 margin-right: calc(-1 * var(--gutter, 1.25rem)); 896 padding: 3rem var(--gutter, 1.25rem) 2.5rem; 897 min-height: 10rem; 898 border-top: 1px solid var(--rule); 899 } 900 /* Left-to-right gradient scrim: dark on the left where the title sits, 901 fading to near-transparent on the right so the fuzz field bleeds through. 902 Matches the daemon-sec.xyz blog banner treatment. */ 903 .sheet-head.plate::before { 904 content: ''; 905 position: absolute; 906 inset: 0; 907 z-index: 0; 908 background: linear-gradient( 909 90deg, 910 rgb(25 23 36 / 0.96) 0%, 911 rgb(25 23 36 / 0.88) 38%, 912 rgb(25 23 36 / 0.55) 62%, 913 rgb(25 23 36 / 0.15) 100% 914 ); 915 pointer-events: none; 916 } 917 .sheet-head.plate .sheet-title, 918 .sheet-head.plate .sheet-desc, 919 .sheet-head.plate .sheet-meta, 920 .sheet-head.plate .tags-row, 921 .sheet-head.plate .eyebrow { position: relative; z-index: 2; } 922 923 924 .breadcrumb { 925 font-family: var(--font-mono); font-size: var(--step-micro); 926 letter-spacing: var(--track-micro); text-transform: uppercase; 927 color: var(--fg-faint); 928 display: flex; gap: 0.45rem; flex-wrap: wrap; align-items: center; 929 } 930 .breadcrumb a:hover { color: var(--accent); } 931 .breadcrumb .sep { opacity: 0.45; } 932 .sheet-title { 933 margin-top: 0.9rem; 934 font-family: var(--font-display); 935 font-size: clamp(1.9rem, 4vw, 3rem); 936 font-weight: 700; font-stretch: 84%; 937 letter-spacing: -0.035em; line-height: 0.94; 938 text-transform: uppercase; 939 } 940 .sheet-meta { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-top: 1rem; } 941 .sheet-desc { color: var(--fg-dim); font-size: var(--step-1); margin-top: 1rem; max-width: 70ch; } 942 943 /* The reading measure, not the archive measure. `--maxw` is 1440px because 944 the index pages are built out of full-bleed bands and ruled tables; a 945 reading column laid out on it leaves a dead third between the prose and 946 the contents rail. The sheet pages get their own, narrower one. */ 947 .sheet-layout { 948 display: grid; grid-template-columns: minmax(0, 1fr); 949 gap: 2.5rem; margin-top: 2.2rem; 950 max-width: 1080px; 951 } 952 @media (min-width: 1040px) { .sheet-layout { grid-template-columns: minmax(0, 1fr) 220px; } } 953 .toc { align-self: start; } 954 /* Under 1040px the rail has nowhere to sit, so it becomes a collapsed 955 disclosure ahead of the article instead of disappearing. `order` moves it 956 without moving the markup, which keeps the sticky rail as the same node. */ 957 .toc { order: -1; margin-bottom: 0.4rem; padding: 0.9rem 1rem; border: 1px solid var(--rule); } 958 .toc > summary { 959 font-family: var(--font-mono); font-size: var(--step-micro); 960 letter-spacing: var(--track-wide); text-transform: uppercase; 961 color: var(--fg-faint); cursor: pointer; list-style: none; 962 display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; 963 } 964 .toc > summary::-webkit-details-marker { display: none; } 965 /* Square +/- rather than a triangle: the contract rules out rounded UA chrome. */ 966 .toc > summary::after { 967 content: '+'; font-size: 0.95em; line-height: 1; color: var(--fg-faint); 968 } 969 .toc[open] > summary::after { content: '−'; } 970 .toc[open] > summary { margin-bottom: 0.7rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--rule); } 971 .toc > summary:hover { color: var(--accent); } 972 .toc > nav { max-height: 45vh; overflow-y: auto; } 973 @media (min-width: 1040px) { 974 .toc { 975 order: 0; position: sticky; top: 78px; margin-bottom: 0; 976 max-height: calc(100vh - 100px); overflow-y: auto; 977 padding: 0; border: 0; 978 } 979 /* The rail is never collapsible, so the marker goes and the panel stays 980 open even if a narrow-viewport reader closed it before resizing. */ 981 .toc > summary { cursor: default; pointer-events: none; } 982 .toc > summary::after { content: none; } 983 .toc > summary, 984 .toc[open] > summary { 985 margin-bottom: 0.7rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--rule); 986 } 987 .toc > nav { display: block; max-height: none; overflow-y: visible; } 988 } 989 .toc a { 990 display: block; padding: 0.26rem 0 0.26rem 0.7rem; 991 border-left: 2px solid var(--rule); 992 color: var(--fg-faint); font-size: 0.82rem; line-height: 1.4; 993 transition: color 180ms ease, border-color 180ms ease; 994 } 995 .toc a:hover, .toc a.active { color: var(--accent); border-left-color: var(--accent); } 996 .toc a.lvl-3 { padding-left: 1.3rem; font-size: 0.78rem; } 997 998 .prevnext { display: grid; gap: 1px; margin-top: 3rem; background: var(--rule); border: 1px solid var(--rule); } 999 @media (min-width: 620px) { .prevnext { grid-template-columns: 1fr 1fr; } } 1000 .prevnext a { padding: 1.1rem 1.2rem; background: var(--base); transition: background-color 240ms ease; } 1001 .prevnext a:hover { background: var(--wash-strong); } 1002 .prevnext .dir { 1003 font-family: var(--font-mono); font-size: var(--step-micro); 1004 color: var(--fg-faint); letter-spacing: var(--track-wide); text-transform: uppercase; 1005 } 1006 .prevnext .nx { text-align: right; } 1007 .prevnext strong { 1008 display: block; margin-top: 0.4rem; color: var(--fg); 1009 font-family: var(--font-display); font-weight: 600; letter-spacing: -0.015em; 1010 } 1011 1012 /* ---- Footer -------------------------------------------------------------- */ 1013 /* A dark band closing the cream page — the same object as the marquee and 1014 the banners, in its last position. */ 1015 .site-footer { 1016 position: relative; 1017 overflow: hidden; 1018 margin-top: clamp(3rem, 7vw, 5.5rem); 1019 border-top: 1px solid var(--fg); 1020 padding-block: clamp(2rem, 4vw, 3rem); 1021 background: var(--base); 1022 color: var(--fg-dim); 1023 } 1024 .site-footer .wrap { 1025 display: grid; gap: 1.6rem 2.5rem; 1026 align-items: start; 1027 } 1028 @media (min-width: 760px) { .site-footer .wrap { grid-template-columns: minmax(0, 1fr) auto; } } 1029 .site-footer a:hover { color: var(--accent); } 1030 .footer-note { font-size: 0.86rem; max-width: 56ch; line-height: 1.55; margin-top: 0.8rem; } 1031 .footer-mono { 1032 display: block; 1033 font-family: var(--font-mono); font-size: var(--step-micro); 1034 letter-spacing: var(--track-micro); text-transform: uppercase; 1035 line-height: 1.9; 1036 } 1037 @media (min-width: 760px) { .footer-col { text-align: right; } } 1038 1039 /* ---- PDF embed ----------------------------------------------------------- */ 1040 .pdf-frame { width: 100%; height: min(80vh, 900px); border: 1px solid var(--rule); background: var(--code-bg); } 1041 1042 /* ---- Empty / utility ----------------------------------------------------- */ 1043 .muted { color: var(--fg-faint); } 1044 .center { text-align: center; } 1045 .mono { font-family: var(--font-mono); } 1046 .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } 1047 1048 /* ---- Category chips ------------------------------------------------------ */ 1049 .cat-nav { display: flex; flex-wrap: wrap; gap: 1px; background: var(--rule); border: 1px solid var(--rule); } 1050 .cat-chip { 1051 display: inline-flex; align-items: center; gap: 0.45rem; 1052 padding: 0.5rem 0.8rem; 1053 background: var(--base); 1054 font-family: var(--font-mono); font-size: var(--step-micro); 1055 letter-spacing: var(--track-micro); text-transform: uppercase; 1056 color: var(--fg-dim); 1057 transition: background-color 220ms ease, color 200ms ease; 1058 } 1059 .cat-chip:hover { background: var(--wash-strong); color: var(--fg); } 1060 .cat-chip__tag { color: var(--acc); } 1061 .cat-chip[aria-current='page'] { background: var(--acc); color: var(--base); } 1062 .cat-chip[aria-current='page'] .cat-chip__tag { color: var(--base); } 1063 1064 /* ---- PayloadsAllTheThings attribution ------------------------------------ */ 1065 .patt-credit { 1066 display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; 1067 margin: 0.2rem 0 1.8rem; 1068 padding: 0.7rem 0.9rem; 1069 border: 1px solid var(--rule); 1070 border-left: 2px solid var(--iris); 1071 background: var(--wash); 1072 font-size: 0.86rem; 1073 } 1074 .patt-badge { 1075 font-family: var(--font-mono); font-size: var(--step-micro); 1076 letter-spacing: var(--track-micro); text-transform: uppercase; 1077 color: var(--iris); 1078 border: 1px solid color-mix(in oklab, var(--iris) 50%, transparent); 1079 padding: 0.14rem 0.4rem; 1080 flex: none; 1081 } 1082 .patt-credit__text { color: var(--fg-dim); line-height: 1.5; } 1083 .patt-credit__text a { color: var(--iris); border-bottom: 1px solid color-mix(in oklab, var(--iris) 40%, transparent); } 1084 .patt-credit__text a:hover { border-color: var(--iris); } 1085 .patt-lic { display: inline-block; margin-left: 0.5rem; color: var(--fg-faint); font-family: var(--font-mono); font-size: 0.78rem; } 1086 .patt-lic code { background: none; color: var(--fg-faint); } 1087 .patt-src { 1088 margin-left: auto; 1089 display: inline-flex; align-items: center; gap: 0.35rem; 1090 font-family: var(--font-mono); font-size: var(--step-micro); 1091 letter-spacing: var(--track-micro); text-transform: uppercase; 1092 color: var(--fg-dim); 1093 padding: 0.32rem 0.6rem; 1094 border: 1px solid var(--rule); 1095 transition: color 180ms ease, border-color 180ms ease; 1096 } 1097 .patt-src:hover { color: var(--iris); border-color: color-mix(in oklab, var(--iris) 50%, transparent); } 1098 1099 /* ---- Theme toggle: circular-reveal wipe ---------------------------------- */ 1100 /* Scoped to .theme-vt so it outranks the page iris below, which runs on 1101 every navigation. The toggle sets --vt-x/--vt-y (origin) and --vt-r 1102 (end radius) in JS. */ 1103 :root.theme-vt::view-transition-old(root), 1104 :root.theme-vt::view-transition-new(root) { 1105 animation: none; /* cancel the default fade */ 1106 mix-blend-mode: normal; 1107 } 1108 :root.theme-vt::view-transition-old(root) { z-index: 0; } /* previous theme underneath */ 1109 :root.theme-vt::view-transition-new(root) { z-index: 1; } /* new theme wipes in on top */ 1110 1111 @media (prefers-reduced-motion: no-preference) { 1112 :root.theme-vt::view-transition-new(root) { 1113 animation: theme-reveal 0.5s cubic-bezier(0.4, 0, 0.2, 1) both; 1114 } 1115 @keyframes theme-reveal { 1116 from { clip-path: circle(0 at var(--vt-x, 50%) var(--vt-y, 0)); } 1117 to { clip-path: circle(var(--vt-r, 150vmax) at var(--vt-x, 50%) var(--vt-y, 0)); } 1118 } 1119 } 1120 1121 /* ---- Page iris: circular reveal on navigation -------------------------- */ 1122 /* Every internal navigation (Astro's ClientRouter) opens the next page as a 1123 circle from the link that was pressed, the theme wipe's gesture at page 1124 pace. The inline script in Base.astro sets --vt-x/--vt-y and the exact 1125 farthest-corner radius --vt-r. The theme wipe's rules above are scoped to 1126 .theme-vt, which outranks these, so each keeps its own timing. */ 1127 ::view-transition-old(root), 1128 ::view-transition-new(root) { 1129 animation: none; /* replace Astro's default crossfade */ 1130 mix-blend-mode: normal; 1131 } 1132 1133 @media (prefers-reduced-motion: no-preference) { 1134 ::view-transition-new(root) { 1135 animation: page-iris 960ms cubic-bezier(0.4, 0, 0.2, 1) both; 1136 } 1137 @keyframes page-iris { 1138 from { clip-path: circle(0 at var(--vt-x, 50%) var(--vt-y, 50%)); } 1139 to { clip-path: circle(var(--vt-r, 150vmax) at var(--vt-x, 50%) var(--vt-y, 50%)); } 1140 } 1141 } 1142 1143 /* The transition layer sits over the whole page and, left alone, swallows 1144 every click until it finishes: at 960ms, a page that ignores the reader 1145 for a second. The live DOM underneath is already the new page, so clicks 1146 go straight through to it. */ 1147 ::view-transition { 1148 pointer-events: none; 1149 } 1150 1151 /* ============================================================================ 1152 Cyberdeck integration — additive hooks that wire the shared chrome objects 1153 (`.corners`, `.scanlines`, `.plate-dawn`) into the page furniture. Kept in 1154 one block at the foot of the file so the restyle stays reviewable. 1155 ========================================================================== */ 1156 1157 /* ---- Hover-reveal corner brackets ---------------------------------------- */ 1158 /* Domain tiles and index records carry `.corners`, but a grid of always-on 1159 brackets reads as clutter — so they stay clipped until the row is under the 1160 pointer, arriving with the ink wipe. */ 1161 .plate-tile.corners::before, .plate-tile.corners::after, 1162 .record.corners::before, .record.corners::after { 1163 opacity: 0; 1164 transition: opacity 180ms ease; 1165 } 1166 .plate-tile.corners:hover::before, .plate-tile.corners:hover::after, 1167 .record.corners:hover::before, .record.corners:hover::after { 1168 opacity: 1; 1169 } 1170 1171 /* ---- Footer CRT film ----------------------------------------------------- */ 1172 /* The footer is a `.plate`; the scanline sheet rides at z-index 1 over it, so 1173 the content column is lifted above the film and the brackets frame it. */ 1174 .site-footer .wrap { position: relative; z-index: 2; } 1175 .site-footer > .scanlines { opacity: 0.6; } 1176 1177 /* ---- 'On this page' rail as a dawn plate --------------------------------- */ 1178 /* `.corners` would force `position: relative` and un-stick the rail, so sticky 1179 is re-asserted at higher specificity — but only from 1040px up, since below 1180 that the element is the collapsible disclosure and must scroll away. The panel treatment and its brackets 1181 only appear on the night page — in light mode `.plate-dawn` is a no-op and 1182 the rail keeps its bare, borderless look. */ 1183 @media (min-width: 1040px) { .toc.corners { position: sticky; } } 1184 .toc.corners::before, .toc.corners::after { display: none; } 1185 :root[data-theme='dark'] .toc.plate-dawn { 1186 background: var(--surface); 1187 border: 1px solid var(--rule); 1188 padding: 1rem 1.05rem 1.1rem; 1189 } 1190 :root[data-theme='dark'] .toc.plate-dawn.corners::before, 1191 :root[data-theme='dark'] .toc.plate-dawn.corners::after { display: block; } 1192 1193 /* `tabindex="-1"` makes <main> a focus target for the back-to-top handoff. 1194 It is not keyboard-reachable, so suppressing the ring costs no affordance. */ 1195 #main:focus { outline: none; } 1196 1197 /* ── Back to top ─────────────────────────────────────────────────────────── 1198 Square, hairline, no radius or shadow, per the design contract. It sits 1199 clear of the footer rule and out of the way of the mobile nav bar. */ 1200 .to-top { 1201 position: fixed; right: 1rem; bottom: 1rem; z-index: 40; 1202 width: 38px; height: 38px; 1203 display: grid; place-items: center; 1204 background: var(--surface); color: var(--fg-faint); 1205 border: 1px solid var(--rule); 1206 cursor: pointer; 1207 opacity: 0; transform: translateY(6px); 1208 transition: opacity 200ms ease, transform 200ms ease, 1209 color 200ms ease, border-color 200ms ease; 1210 } 1211 .to-top[hidden] { display: none; } 1212 .to-top.is-visible { opacity: 1; transform: translateY(0); } 1213 .to-top:hover { color: var(--accent); border-color: var(--accent); } 1214 .to-top:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; } 1215 /* The icon set ships a right-chevron; pointing it up avoids a new glyph. */ 1216 .to-top svg { width: 14px; height: 14px; transform: rotate(-90deg); } 1217 @media (prefers-reduced-motion: reduce) { 1218 .to-top { transition: none; transform: none; } 1219 .to-top.is-visible { transform: none; } 1220 }