Add logo loading screen gated on animation readiness

- Full-screen loader whose only content is the logo animating: the "B"
  draws in and fills, the base facets trace in, then a wave runs along
  them until the page is ready (pure CSS, plays before JS)
- Loader gate holds the loader until GSAP + fonts and every 3D scene's
  first frame are ready, then waits 1s (min 1.4s total) before leaving;
  hard 8s limit, head-script fallback if JS never boots
- Scroll reveals, SplitText and scene entry animations start only once
  the loader leaves, so above-the-fold entries play in view
- Internal links fade the loader back in before navigating, so page
  changes are one seamless dark transition
- No loader for no-JS or reduced-motion visitors

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-11-05 07:17:54 +01:00
co-authored by Claude Opus 5.5
parent 6e7219d30b
commit c3514d2607
9 changed files with 432 additions and 21 deletions
+131
View File
@@ -115,6 +115,137 @@ html.motion:not(.motion-fallback) [data-reveal-stagger] > * {
margin-bottom: -0.08em;
}
/* ---- Page loader (see src/components/loader) ---- */
.page-loader {
display: none;
}
html.motion .page-loader {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: #0a0a0a;
opacity: 1;
visibility: visible;
transition:
opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1),
visibility 0s linear 0s;
}
html.motion.loader-done .page-loader {
opacity: 0;
visibility: hidden;
pointer-events: none;
transition:
opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1),
visibility 0s linear 0.7s;
}
html.loader-gone .page-loader * {
animation-play-state: paused !important;
}
html.motion.loader-leaving .page-loader {
transition-duration: 0.32s, 0s;
}
/* -- the logo -- */
.page-loader-logo {
--d0: 0.1s; /* start offset for the whole sequence */
width: 56px;
height: auto;
overflow: visible;
color: #fff;
transition:
opacity 0.45s ease,
transform 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}
html.loader-done .page-loader-logo {
opacity: 0;
transform: scale(1.06);
}
/* Page exits never show it. */
html.loader-leaving .page-loader-logo {
opacity: 0;
}
.pl-shape {
fill: currentColor;
fill-opacity: 0;
stroke: currentColor;
stroke-width: 9;
stroke-linejoin: round;
stroke-dasharray: 1 1;
stroke-dashoffset: 1;
}
/* 1. the "B" draws itself in, then fills */
.pl-letter {
animation:
pl-draw 1s cubic-bezier(0.65, 0, 0.35, 1) var(--d0) forwards,
pl-fill 0.55s ease calc(var(--d0) + 0.65s) forwards;
}
/* 2. the facets trace in and rise into place, left → right */
.pl-facet .pl-shape {
transform-box: fill-box;
transform-origin: center;
animation:
pl-draw 0.6s ease-out calc(var(--d0) + 0.45s + var(--i) * 0.08s)
forwards,
pl-fill 0.45s ease calc(var(--d0) + 0.7s + var(--i) * 0.08s)
forwards,
pl-rise 0.7s cubic-bezier(0.22, 1, 0.36, 1)
calc(var(--d0) + 0.45s + var(--i) * 0.08s) both;
}
/* 3. while waiting: a signal runs along the facets */
.pl-facet {
transform-box: fill-box;
transform-origin: center;
animation: pl-wave 1.8s ease-in-out
calc(var(--d0) + 1.35s + var(--i) * 0.11s) infinite;
}
@keyframes pl-draw {
to {
stroke-dashoffset: 0;
}
}
@keyframes pl-fill {
to {
fill-opacity: 1;
}
}
@keyframes pl-rise {
from {
transform: translateY(70px);
}
to {
transform: none;
}
}
@keyframes pl-wave {
0%,
45%,
100% {
transform: none;
color: #fff;
}
20% {
transform: translateY(-46px);
color: #34d399;
}
}
/* Hero graphics (<xl): stacked above the text; fades out downward into it */
@media (max-width: 79.999rem) {
.hero-graphic {