feat(landing): add animated shadergradient hero background (#411)

A self-hosted animated warm gradient now sits behind the hero: a captured
WebGL mesh loop (webm + mp4 + webp poster, no framework or Three.js) in a
top band that fades to cream before the tool search. The headline goes
white, badges become dark glass, and the navbar is more opaque so nav,
badges, headline, and subtitle all stay legible over the gradient.
Reduced-motion and small screens fall back to the still poster.

Also carries the hero copy already present in the working tree (new
headline, badge labels, and subtitle), which is entangled with these edits.

Claude-Session: https://claude.ai/code/session_01MbQGV1k1mneFidgGyDnmDQ
This commit is contained in:
SnapOtter
2026-07-03 14:41:39 +08:00
committed by GitHub
parent b4375e558d
commit 5500facc5d
6 changed files with 68 additions and 49 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.
Binary file not shown.
+22 -48
View File
@@ -6,10 +6,10 @@ import HeroSearch from "./HeroSearch.astro";
import TrustSignals from "./TrustSignals.astro"; import TrustSignals from "./TrustSignals.astro";
const trustBadges = [ const trustBadges = [
"On-premise", "Self-hosted",
"Privacy-first", "Privacy-sensitive",
"GDPR & HIPAA ready", "Compliance-friendly",
"Air-gap ready", "Air-gap capable",
"SAML SSO", "SAML SSO",
"Audit logging", "Audit logging",
"Open source", "Open source",
@@ -17,30 +17,40 @@ const trustBadges = [
--- ---
<section class="relative overflow-hidden px-6 pt-28 pb-12 md:pt-32 md:pb-16"> <section class="relative overflow-hidden px-6 pt-28 pb-12 md:pt-32 md:pb-16">
<!-- Gradient mesh background --> <!-- Gradient mesh background (CSS fallback beneath the shadergradient loop) -->
<div class="hero-mesh" aria-hidden="true"></div> <div class="hero-mesh" aria-hidden="true"></div>
<div class="hero-mesh-extra" aria-hidden="true"></div> <div class="hero-mesh-extra" aria-hidden="true"></div>
<!-- ShaderGradient warm-wash loop: captured, self-hosted, no framework. -->
<!-- Poster (still) doubles as the reduced-motion and load-failure fallback. -->
<div class="hero-gradient" aria-hidden="true">
<!-- biome-ignore lint/a11y/useMediaCaption: decorative silent background loop with no audio track. -->
<video autoplay muted loop playsinline preload="metadata" tabindex="-1">
<source src="/hero-gradient.webm?v=5" type="video/webm" />
<source src="/hero-gradient.mp4?v=5" type="video/mp4" />
</video>
</div>
<div class="relative mx-auto max-w-4xl text-center"> <div class="relative mx-auto max-w-4xl text-center">
<!-- Enterprise trust badges --> <!-- Enterprise trust badges -->
<div class="animate-fade-up flex flex-wrap items-center justify-center gap-2"> <div class="animate-fade-up flex flex-wrap items-center justify-center gap-2">
{trustBadges.map((badge) => ( {trustBadges.map((badge) => (
<span class="inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/5 px-3 py-1 text-xs font-medium text-primary-dark"> <span class="inline-flex items-center gap-1.5 rounded-full border border-white/25 bg-black/25 px-3 py-1 text-xs font-medium text-white backdrop-blur-sm">
<svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> <svg class="h-3 w-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
{badge} {badge}
</span> </span>
))} ))}
</div> </div>
<!-- Headline: one sentence, rotating modality + fixed privacy promise --> <!-- Headline: one precise promise for privacy-sensitive buyers -->
<h1 class="animate-fade-up mt-8 font-display text-4xl font-extrabold tracking-tight sm:text-5xl md:text-6xl lg:text-7xl" style="animation-delay: 0.1s;"> <h1 class="animate-fade-up mt-8 font-display text-4xl font-extrabold tracking-tight text-white sm:text-5xl md:text-6xl lg:text-7xl" style="animation-delay: 0.1s; text-shadow: 0 2px 30px rgba(90, 20, 0, 0.28);">
Every <span class="word-rotator" aria-hidden="true"><span class="word-rotator-track"><span>file</span><span>image</span><span>video</span><span>audio</span><span>PDF</span><span>files</span><span>file</span></span></span><span class="sr-only">file</span> tool you need. Private file processing, self-hosted.
<span class="mt-3 block bg-gradient-to-r from-primary to-primary-light bg-clip-text text-2xl font-bold text-transparent sm:text-3xl md:text-4xl">Your files never leave your network.</span> <span class="mt-3 block text-2xl font-bold text-white/90 sm:text-3xl md:text-4xl">Inside your infrastructure.</span>
</h1> </h1>
<!-- Subtitle: one calm supporting line --> <!-- Subtitle: one calm supporting line -->
<p class="animate-fade-up mx-auto mt-6 max-w-2xl text-lg text-muted md:text-xl" style="animation-delay: 0.15s;"> <p class="animate-fade-up mx-auto mt-6 max-w-2xl text-lg text-white/90 md:text-xl" style="animation-delay: 0.15s; text-shadow: 0 1px 16px rgba(90, 20, 0, 0.22);">
The file-processing suite for teams that keep sensitive data in-house. Run image, video, audio, PDF, and document tools with local AI, batch workflows, and enterprise controls.
</p> </p>
<!-- Hero tool search --> <!-- Hero tool search -->
@@ -53,39 +63,3 @@ const trustBadges = [
<TrustSignals /> <TrustSignals />
</div> </div>
</section> </section>
<style>
/* Pure-CSS rotating word (no client JS). Cycles the file type in the headline. */
.word-rotator {
display: inline-block;
height: 1em;
overflow: hidden;
vertical-align: top;
color: var(--color-primary);
}
.word-rotator-track {
display: flex;
flex-direction: column;
animation: word-rotate 13.2s infinite;
}
.word-rotator-track > span {
display: block;
height: 1em;
line-height: 1;
white-space: nowrap;
}
@keyframes word-rotate {
0%, 12% { transform: translateY(0); }
16.66%, 28.66% { transform: translateY(-1em); }
33.33%, 45.33% { transform: translateY(-2em); }
50%, 62% { transform: translateY(-3em); }
66.66%, 78.66% { transform: translateY(-4em); }
83.33%, 95.33% { transform: translateY(-5em); }
100% { transform: translateY(-6em); }
}
@media (prefers-reduced-motion: reduce) {
.word-rotator-track {
animation: none;
}
}
</style>
+1 -1
View File
@@ -15,7 +15,7 @@ const links = [
]; ];
--- ---
<nav class="fixed top-0 left-0 right-0 z-50 bg-background/70 backdrop-blur-xl after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:bg-gradient-to-r after:from-transparent after:via-primary/20 after:to-transparent"> <nav class="fixed top-0 left-0 right-0 z-50 bg-background/90 backdrop-blur-xl after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:bg-gradient-to-r after:from-transparent after:via-primary/20 after:to-transparent">
<div class="mx-auto flex h-16 max-w-6xl items-center justify-between px-6"> <div class="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
<!-- Logo --> <!-- Logo -->
<a href="/" class="flex items-center gap-2.5"> <a href="/" class="flex items-center gap-2.5">
+45
View File
@@ -162,6 +162,51 @@ code, pre, kbd {
animation: mesh-drift 25s ease-in-out infinite reverse; animation: mesh-drift 25s ease-in-out infinite reverse;
} }
/* ─── HERO SHADERGRADIENT WARM-WASH LOOP ─── */
/* A captured shadergradient-style loop, self-hosted (no framework/Three.js). */
/* Softness is baked into the asset, so the runtime blur here is light. */
.hero-gradient {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
opacity: 1;
transform: scale(1.05); /* push blurred edges off-screen for a full-bleed band */
filter: blur(6px) saturate(1.06);
/* Deep-warm fallback keeps white hero text legible if the asset ever fails to load. */
background-color: #7e3d24;
background-image: url("/hero-gradient-poster.webp?v=5");
background-size: cover;
background-position: center;
/* Vivid band behind the white headline, faded to the cream page before the search box. */
-webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 46%, transparent 66%);
mask-image: linear-gradient(to bottom, #000 0%, #000 46%, transparent 66%);
}
.hero-gradient video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
/* On small screens, drop the video and keep the still poster to save battery/data. */
@media (max-width: 767px) {
.hero-gradient video {
display: none;
}
}
/* Respect reduced-motion: freeze the mesh drift and show the still, not the loop. */
@media (prefers-reduced-motion: reduce) {
.hero-gradient video {
display: none;
}
.hero-mesh::before,
.hero-mesh::after,
.hero-mesh-extra {
animation: none;
}
}
/* ─── SCROLL REVEAL ─── */ /* ─── SCROLL REVEAL ─── */
.reveal { .reveal {
opacity: 0; opacity: 0;