feat(theme): animate public background with aurora and particles, polish landing pages

Add background_effect (aurora/particles), background_overlay tint and
opacity to the theme manager, rendered site-wide by ThemeVars on every
public page. Polish the home and register pages (hero mascot, live stat
pulse, date pills, photo strip, CTA band, theme-aware register intro,
i18n for home/register section).
This commit is contained in:
openhands committed 2026-09-18 12:44:39 +02:00
1 parent 5923b736fa
commit d131124515
9 files changed
+573 -25

No files matched your search

+107 -9
View File
@@ -249,6 +249,23 @@ export default async function Home() {
/>
<AmbientOrbs />
<div className="hero-ring" aria-hidden="true" style={{ zIndex: 3 }} />
{/* Floating mascot — decorative, hidden on smaller screens. */}
<div
aria-hidden="true"
className="pointer-events-none absolute bottom-3 left-4 hidden lg:block xl:bottom-5 xl:left-8"
style={{ zIndex: 2 }}
>
<div className="brand-halo left-1/2 top-1/2 h-40 w-40 -translate-x-1/2 -translate-y-1/2 xl:h-48 xl:w-48" />
<Image
src="/assets/images/FrankwithBag.gif"
alt=""
width={104}
height={138}
priority
unoptimized
className="relative animate-float object-contain drop-shadow-xl"
/>
</div>
<div
className="relative flex flex-col items-center gap-5 px-4 py-12 text-center sm:gap-8 sm:px-6 sm:py-14 md:px-12 md:py-24"
style={{ zIndex: 2 }}
@@ -409,7 +426,7 @@ export default async function Home() {
{features.map((f) => (
<SurfaceCard
key={f.title}
className="group card-glow relative p-4 sm:p-5 text-center transition-all duration-300 hover:-translate-y-1.5"
className="group card-glow card-hairline relative p-4 sm:p-5 text-center transition-all duration-300 hover:-translate-y-1.5"
>
<div
className="mx-auto mb-3 flex h-11 w-11 sm:h-12 sm:w-12 items-center justify-center rounded-xl transition-transform duration-300 group-hover:scale-110 group-hover:rotate-3"
@@ -487,7 +504,7 @@ export default async function Home() {
].map((s) => (
<SurfaceCard
key={s.label}
className="card-glow relative p-3 sm:p-5 text-center transition-all duration-300 ease-out hover:-translate-y-1.5 hover:shadow-xl"
className="card-glow card-hairline relative p-3 sm:p-5 text-center transition-all duration-300 ease-out hover:-translate-y-1.5 hover:shadow-xl"
>
<div
className="mb-1 text-2xl sm:text-3xl font-black tracking-tight md:text-4xl"
@@ -510,9 +527,17 @@ export default async function Home() {
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
color: "var(--color-text-muted)",
color: s.live
? "var(--color-primary-readable, var(--color-primary))"
: "var(--color-text-muted)",
}}
>
{s.live && (
<span className="relative flex h-1.5 w-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-primary" />
</span>
)}
<Image
src={s.icon}
alt=""
@@ -569,7 +594,8 @@ export default async function Home() {
{recentUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-0.5 rounded-lg px-1 py-1.5 transition-colors hover:bg-[color-mix(in_srgb,var(--color-primary)_5%,transparent)]"
title={u.username}
className="flex flex-col items-center gap-0.5 rounded-lg px-1 py-1.5 transition-all duration-200 hover:bg-[color-mix(in_srgb,var(--color-primary)_5%,transparent)] hover:ring-1 hover:ring-[color-mix(in_srgb,var(--color-primary)_35%,transparent)]"
>
<UserAvatarThumbnail
figure={u.look}
@@ -628,6 +654,16 @@ export default async function Home() {
→
</span>
</div>
<div
className="absolute left-2.5 top-2.5 rounded-full px-2 py-0.5 text-[10px] font-bold backdrop-blur-sm"
style={{
background:
"color-mix(in srgb, #000 45%, transparent)",
color: "#fff",
}}
>
{formatDate(a.createdAt, "date", "")}
</div>
<div
className="absolute bottom-0 left-0 w-full p-2.5 sm:p-3"
style={{
@@ -670,12 +706,14 @@ export default async function Home() {
recentPhotos.length > 0 && (
<Reveal>
<SurfaceCard title={tp("recentPhotos")} icon={ICON_CAMERA}>
<div className="mx-auto grid max-w-md grid-cols-4 gap-2">
<div className="mx-auto grid max-w-xl grid-cols-4 gap-3">
{recentPhotos.slice(0, 4).map((p) => (
<Link
key={p.id}
href="/photos"
className="block aspect-square overflow-hidden rounded-xl border transition-all duration-200 hover:shadow-md hover:scale-[1.02]"
title=""
aria-label={tp("recentPhotos")}
className="block aspect-[4/3] overflow-hidden rounded-xl border transition-all duration-300 ease-out hover:scale-[1.03] hover:shadow-xl hover:ring-2 hover:ring-[color-mix(in_srgb,var(--color-primary)_45%,transparent)]"
style={{
borderColor:
"color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
@@ -683,9 +721,9 @@ export default async function Home() {
>
<Image
src={p.url}
alt=""
width={200}
height={200}
alt={tp("recentPhotos")}
width={280}
height={210}
className="h-full w-full object-cover"
unoptimized
/>
@@ -696,6 +734,66 @@ export default async function Home() {
</Reveal>
)
)}
{/* ── Join the fun ── */}
<Reveal>
<section
aria-labelledby="join-cta-title"
className="relative overflow-hidden rounded-3xl p-6 text-center sm:p-10"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 12%, transparent), color-mix(in srgb, var(--color-accent) 12%, transparent))",
border:
"1px solid color-mix(in srgb, var(--color-primary) 22%, transparent)",
}}
>
<div
aria-hidden="true"
className="brand-halo left-1/2 top-0 h-64 w-[560px] max-w-full -translate-x-1/2 -translate-y-1/3"
/>
<h2
id="join-cta-title"
className="relative text-2xl font-black tracking-tight text-balance sm:text-3xl"
style={{ color: "var(--color-text-readable)" }}
>
{tp("joinCtaTitle")}
</h2>
<p
className="relative mx-auto mt-2 max-w-xl text-sm sm:text-base"
style={{ color: "var(--color-text-muted)" }}
>
{tp("joinCtaBody")}
</p>
<div className="relative mt-6 flex flex-wrap items-center justify-center gap-3">
<Link
href="/register"
className="btn-shine group inline-flex items-center gap-2 rounded-xl px-6 py-3 text-sm font-bold text-primary-foreground shadow-lg transition-transform duration-300 hover:-translate-y-0.5"
style={{
background:
"linear-gradient(135deg, var(--color-primary), color-mix(in srgb, var(--color-accent) 55%, var(--color-primary)))",
}}
>
{tp("ctaJoin")}
<span className="transition-transform duration-300 group-hover:translate-x-0.5">
→
</span>
</Link>
<Link
href="/community"
className="inline-flex items-center rounded-xl px-6 py-3 text-sm font-bold transition-all duration-300 hover:-translate-y-0.5"
style={{
background:
"color-mix(in srgb, var(--color-text-muted) 8%, transparent)",
border:
"1px solid color-mix(in srgb, var(--color-text-muted) 18%, transparent)",
color: "var(--color-text-readable)",
}}
>
{tp("browseCommunity")}
</Link>
</div>
</section>
</Reveal>
</div>
);
}
+12 -10
View File
@@ -65,16 +65,16 @@ export default async function RegisterPage() {
<div
className="absolute inset-0"
style={{
background: `url(/assets/images/background.png) center/cover no-repeat`,
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-primary) 10%, transparent), color-mix(in srgb, var(--color-accent) 10%, transparent))",
}}
/>
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(135deg, color-mix(in srgb, var(--color-surface) 88%, transparent) 0%, color-mix(in srgb, var(--color-surface) 35%, transparent) 100%)",
}}
/>
aria-hidden="true"
className="absolute inset-0 overflow-hidden"
>
<div className="brand-halo left-1/2 top-0 h-64 w-[460px] max-w-full -translate-x-1/2 -translate-y-1/2" />
</div>
<div className="relative">
<div className="flex items-center gap-4">
<div className="shrink-0 animate-float">
@@ -135,7 +135,8 @@ export default async function RegisterPage() {
{recentUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-1.5 rounded-xl border px-1 py-2 transition-all duration-200 hover:-translate-y-0.5"
title={u.username}
className="flex flex-col items-center gap-1.5 rounded-xl border px-1 py-2 transition-all duration-200 hover:-translate-y-0.5 hover:ring-1 hover:ring-[color-mix(in_srgb,var(--color-primary)_35%,transparent)]"
style={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
@@ -163,7 +164,7 @@ export default async function RegisterPage() {
{latestUsers.length > 0 && (
<SurfaceCard
title="Newest citizens"
title={tpr("newestCitizens")}
icon={ICON_FRIENDS}
bodyClassName="p-4"
className="card-glow"
@@ -172,7 +173,8 @@ export default async function RegisterPage() {
{latestUsers.map((u) => (
<div
key={u.username}
className="flex flex-col items-center gap-1.5 rounded-xl border px-1 py-2 transition-all duration-200 hover:-translate-y-0.5"
title={u.username}
className="flex flex-col items-center gap-1.5 rounded-xl border px-1 py-2 transition-all duration-200 hover:-translate-y-0.5 hover:ring-1 hover:ring-[color-mix(in_srgb,var(--color-primary)_35%,transparent)]"
style={{
background:
"color-mix(in srgb, var(--color-primary) 4%, transparent)",
+142
View File
@@ -590,6 +590,148 @@ html.dark body.site-bg:has([data-admin]) {
background-blend-mode: normal;
}
/* ── Animated site backdrop (Theme Manager effects) ────────────────
A fixed, decorative layer between the body wallpaper and the content.
Theme-driven: aurora + particles are pure CSS on transform/opacity, so
they stay cheap and pause for prefers-reduced-motion automatically. */
.site-backdrop {
position: fixed;
inset: 0;
z-index: -1;
overflow: hidden;
pointer-events: none;
contain: layout paint style;
}
.site-backdrop > span {
position: absolute;
inset: 0;
pointer-events: none;
}
body:has([data-admin]) .site-backdrop {
display: none;
}
/* Aurora blobs (large, soft radial gradients — no filters). */
.site-backdrop > .site-backdrop-aurora {
inset: -12% -10%;
overflow: hidden;
}
.site-backdrop-blob {
position: absolute;
border-radius: 999px;
opacity: 0.42;
will-change: transform;
animation: aurora-drift 20s ease-in-out infinite alternate;
}
.site-backdrop-blob-1 {
width: clamp(320px, 44vw, 620px);
height: clamp(320px, 44vw, 620px);
top: -14%;
left: -10%;
background: radial-gradient(
circle at 32% 32%,
color-mix(in srgb, var(--color-primary) 60%, transparent) 0%,
transparent 68%
);
}
.site-backdrop-blob-2 {
width: clamp(280px, 38vw, 540px);
height: clamp(280px, 38vw, 540px);
top: -10%;
right: -8%;
background: radial-gradient(
circle at 62% 40%,
color-mix(in srgb, var(--color-accent) 48%, transparent) 0%,
transparent 68%
);
animation-delay: -7s;
animation-duration: 24s;
}
.site-backdrop-blob-3 {
width: clamp(360px, 50vw, 700px);
height: clamp(360px, 50vw, 700px);
bottom: -22%;
left: 12%;
background: radial-gradient(
circle at 50% 50%,
color-mix(in srgb, var(--color-primary) 34%, transparent) 0%,
color-mix(in srgb, var(--color-accent) 18%, transparent) 42%,
transparent 72%
);
animation-delay: -13s;
animation-duration: 28s;
}
@keyframes aurora-drift {
0% {
transform: translate3d(0, 0, 0) scale(1) rotate(0deg);
}
100% {
transform: translate3d(6vw, 5vh, 0) scale(1.16) rotate(6deg);
}
}
html.dark .site-backdrop-blob {
opacity: 0.55;
}
/* Floating particles drifting upward. */
.site-backdrop-particles {
inset: 0;
}
.site-backdrop-dot {
position: absolute;
bottom: -4vh;
width: calc(var(--dot-size, 3) * 1px + 1px);
height: calc(var(--dot-size, 3) * 1px + 1px);
border-radius: 999px;
background: color-mix(in srgb, var(--color-primary) 70%, transparent);
box-shadow: 0 0 8px color-mix(in srgb, var(--color-primary) 55%, transparent);
opacity: 0;
animation: particle-rise var(--dot-duration, 12s) linear infinite;
animation-delay: var(--dot-delay, 0s);
}
@keyframes particle-rise {
0% {
transform: translate3d(0, 0, 0);
opacity: 0;
}
10% {
opacity: var(--dot-alpha, 0.45);
}
88% {
opacity: var(--dot-alpha, 0.45);
}
100% {
transform: translate3d(var(--dot-drift, 2vw), -108vh, 0);
opacity: 0;
}
}
html.dark .site-backdrop-dot {
background: color-mix(in srgb, var(--color-primary) 82%, transparent);
}
/* Overlay tint above image/effects keeps custom backgrounds readable. */
.site-backdrop-overlay {
inset: 0;
background: var(--background-overlay, transparent);
}
/* Show only the selected effect(s). */
.site-backdrop[data-effect="none"] .site-backdrop-particles,
.site-backdrop[data-effect="none"] .site-backdrop-aurora,
.site-backdrop[data-effect="aurora"] .site-backdrop-particles,
.site-backdrop[data-effect="particles"] .site-backdrop-aurora {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.site-backdrop-blob {
animation: none;
}
.site-backdrop-dot {
animation: none;
opacity: 0;
}
}
.article-body {
line-height: 1.7;
}