Website for the Lede browser extension.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add floating pill nav with smooth entrance animation

Wraps header and hero in a .hero-shell grid so the nav overlays the
mast gradient when integrated. On scroll, adds site-header--scrolled
which snaps to position: fixed; @keyframes drive the glass fade-in and
translateY slide since CSS transitions don't survive a simultaneous
position reflow. Exit is instant (user is near scroll-top, sticky
re-appears at the same visual position). Includes rAF-coalesced scroll
handler and full prefers-reduced-motion support.

+165 -19
+31
src/components/SiteHeader.astro
··· 57 57 </div> 58 58 </div> 59 59 </header> 60 + 61 + <script is:inline> 62 + (function () { 63 + var header = document.querySelector(".site-header"); 64 + if (!header) return; 65 + /* Hysteresis: avoids borderline flicker when scroll sits near the threshold */ 66 + var enter = 36; 67 + var exit = 12; 68 + var floated = false; 69 + var cls = "site-header--scrolled"; 70 + var raf = 0; 71 + function sync() { 72 + var y = window.scrollY || document.documentElement.scrollTop; 73 + var next = floated; 74 + if (!floated && y > enter) next = true; 75 + else if (floated && y < exit) next = false; 76 + if (next === floated) return; 77 + floated = next; 78 + header.classList.toggle(cls, floated); 79 + } 80 + function onScroll() { 81 + if (raf) return; 82 + raf = requestAnimationFrame(function () { 83 + raf = 0; 84 + sync(); 85 + }); 86 + } 87 + sync(); 88 + window.addEventListener("scroll", onScroll, { passive: true }); 89 + })(); 90 + </script>
+4 -2
src/pages/index.astro
··· 28 28 29 29 <BaseLayout ogUrl={ogUrl} ogImage={ogImage}> 30 30 <div class="page"> 31 - <SiteHeader tangled={TANGLED_REPO_URL} /> 32 31 <main id="main" class="page-main"> 33 - <HeroWithProof tangled={TANGLED_REPO_URL} /> 32 + <div class="hero-shell"> 33 + <SiteHeader tangled={TANGLED_REPO_URL} /> 34 + <HeroWithProof tangled={TANGLED_REPO_URL} /> 35 + </div> 34 36 <EditorialValue /> 35 37 36 38 <PrivacyBand />
+130 -17
src/styles/global.css
··· 74 74 --radius-sm: 6px; 75 75 --radius-md: 8px; 76 76 --radius-pill: 14px; 77 + /* Floated site header — slightly rounder than cards, tuned for blur + shadow */ 78 + --radius-nav-float: 13px; 79 + --nav-float-max: 1280px; 77 80 78 81 --ease-out: cubic-bezier(0.22, 1, 0.36, 1); 79 82 /* Softer deceleration for first-paint choreography (no bounce) */ ··· 240 243 flex: 1; 241 244 } 242 245 246 + /* Mast gradient paints the full hero; nav sits in the same grid cell so it reads integrated */ 247 + .hero-shell { 248 + display: grid; 249 + grid-template-areas: "stack"; 250 + position: relative; 251 + } 252 + 253 + .hero-shell > .site-header, 254 + .hero-shell > .hero-mast { 255 + grid-area: stack; 256 + } 257 + 258 + .hero-shell > .hero-mast { 259 + position: relative; 260 + z-index: 0; 261 + } 262 + 263 + .hero-shell > .site-header { 264 + position: sticky; 265 + top: 0; 266 + align-self: start; 267 + justify-self: stretch; 268 + width: 100%; 269 + z-index: 50; 270 + } 271 + 243 272 .wrap { 244 273 width: min(1120px, 100% - 2 * var(--page-gutter)); 245 274 margin-inline: auto; ··· 329 358 color: var(--color-ink); 330 359 } 331 360 332 - /* ——— Header ——— */ 361 + /* ——— Header ——— 362 + In .hero-shell: sits over the mast gradient (integrated at top). 363 + Floated: one painted surface on ::after (inset ring + shadow) avoids radius seam glitches. 364 + 365 + Transition strategy: 366 + - sticky→fixed is a layout change; transitions don't reliably start from the pre-change value. 367 + - We use @keyframes (not transition) for the glass entrance — animations always fire. 368 + - translateY slide on the pill parent covers the geometry snap visually. 369 + - Exit is instant (glass snaps off, user is near top so sticky re-appears at same visual pos). */ 333 370 .site-header { 334 - position: sticky; 335 - top: 0; 336 - z-index: 50; 371 + position: relative; 337 372 isolation: isolate; 338 - background: color-mix(in oklch, var(--color-canvas) 96%, transparent); 339 - backdrop-filter: blur(6px); 340 - border-bottom: 1px solid var(--color-border); 341 - box-shadow: 0 1px 0 color-mix(in oklch, var(--color-brand) 8%, transparent); 373 + z-index: 40; 374 + background: transparent; 375 + border: 1px solid transparent; 376 + border-radius: 0; 377 + box-shadow: none; 378 + transition: border-radius 0.55s var(--ease-reveal); 342 379 } 343 380 344 - .site-header::before { 381 + .site-header::after { 345 382 content: ""; 346 383 position: absolute; 347 - inset: 0 0 auto 0; 348 - height: 3px; 349 - background: linear-gradient( 350 - 90deg, 351 - color-mix(in oklch, var(--color-brand) 88%, transparent), 352 - color-mix(in oklch, var(--color-brand) 35%, var(--color-canvas)) 353 - ); 384 + inset: 0; 385 + z-index: 0; 386 + box-sizing: border-box; 387 + border-radius: inherit; 388 + border: 1px solid transparent; 389 + background: transparent; 390 + backdrop-filter: none; 391 + -webkit-backdrop-filter: none; 392 + opacity: 0; 354 393 pointer-events: none; 394 + box-shadow: none; 395 + /* No transition here — entrance is via @keyframes; exit is instant (glass snaps off at scroll≈0) */ 396 + transition: border-radius 0.55s var(--ease-reveal); 397 + transform: translateZ(0); 398 + -webkit-backface-visibility: hidden; 399 + backface-visibility: hidden; 400 + } 401 + 402 + /* Full glass styles in scrolled state — opacity driven by the animation below */ 403 + .site-header.site-header--scrolled::after { 404 + opacity: 1; 405 + border-color: transparent; 406 + border-radius: inherit; 407 + background: color-mix(in oklch, var(--color-canvas) 86%, transparent); 408 + backdrop-filter: blur(18px) saturate(1.35); 409 + -webkit-backdrop-filter: blur(18px) saturate(1.35); 410 + box-shadow: 411 + inset 0 0 0 1px var(--color-border), 412 + 0 14px 36px oklch(0% 0 0 / 0.07), 413 + 0 32px 72px oklch(0% 0 0 / 0.09); 414 + } 415 + 416 + /* Pill entrance: slide down + glass fade — both as keyframes so they fire despite the position change */ 417 + @keyframes nav-glass-in { 418 + from { opacity: 0; } 419 + to { opacity: 1; } 420 + } 421 + 422 + @keyframes nav-pill-in { 423 + from { transform: translateY(-10px); } 424 + to { transform: translateY(0); } 425 + } 426 + 427 + @media (prefers-reduced-motion: no-preference) { 428 + .site-header.site-header--scrolled::after { 429 + animation: nav-glass-in 0.42s var(--ease-reveal) both; 430 + } 431 + 432 + .hero-shell > .site-header.site-header--scrolled { 433 + animation: nav-pill-in 0.45s var(--ease-reveal) both; 434 + } 435 + } 436 + 437 + .hero-shell > .site-header.site-header--scrolled { 438 + position: fixed; 439 + top: var(--space-sm); 440 + left: calc(var(--space-2xs) + env(safe-area-inset-left, 0px)); 441 + right: calc(var(--space-2xs) + env(safe-area-inset-right, 0px)); 442 + z-index: 60; 443 + width: auto; 444 + max-width: var(--nav-float-max); 445 + margin-inline: auto; 446 + border-radius: var(--radius-nav-float); 447 + border-color: transparent; 448 + box-shadow: none; 449 + overflow: clip; 450 + will-change: transform; 451 + } 452 + 453 + .site-header .wrap { 454 + position: relative; 455 + z-index: 1; 355 456 } 356 457 357 458 .site-header__inner { ··· 360 461 justify-content: space-between; 361 462 gap: var(--space-md); 362 463 padding-block: var(--space-sm); 464 + } 465 + 466 + .hero-shell > .site-header.site-header--scrolled .site-header__inner { 467 + padding-inline: clamp(var(--space-sm), 2.5vw, var(--space-lg)); 363 468 } 364 469 365 470 .site-header__actions { ··· 832 937 .footer-links a, 833 938 .brand-lockup, 834 939 .journey-strip__cell a, 835 - .hero-highlights li { 940 + .hero-highlights li, 941 + .site-header, 942 + .site-header::after { 836 943 transition: none; 944 + animation: none !important; 837 945 } 838 946 839 947 .journey-strip__cell:hover, ··· 921 1029 var(--color-canvas) 42%, 922 1030 color-mix(in oklch, var(--color-subtle) 88%, var(--color-canvas)) 100% 923 1031 ); 1032 + } 1033 + 1034 + /* Nav overlays the mast paint; reserve vertical space for the sticky header strip */ 1035 + .hero-shell > .hero-mast { 1036 + padding-top: calc(3.5rem + clamp(var(--space-md), 2.25vw + 0.65rem, var(--space-xl))); 924 1037 } 925 1038 926 1039 .hero-mast .eyebrow {