STYLED - Add CSS hero entrance, title halo, and scroll cue
Replace the framer-motion hero entrance with pure-CSS staggered animations so the hero is painted immediately instead of flashing blank until hydration. Add a focused brand glow behind the hero title and an animated scroll cue at the bottom of the home hero. Entrance and cue respect prefers-reduced-motion.
This commit is contained in:
@@ -422,6 +422,95 @@ html[lang="ar"] .eyebrow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Hero entrance (pure CSS so it never flashes blank before hydration) ---- */
|
||||||
|
.hero-rise {
|
||||||
|
opacity: 0;
|
||||||
|
animation: hero-rise-in 0.62s cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content > *:nth-child(1) {
|
||||||
|
animation-delay: 0.06s;
|
||||||
|
}
|
||||||
|
.hero-content > *:nth-child(2) {
|
||||||
|
animation-delay: 0.14s;
|
||||||
|
}
|
||||||
|
.hero-content > *:nth-child(3) {
|
||||||
|
animation-delay: 0.22s;
|
||||||
|
}
|
||||||
|
.hero-content > *:nth-child(4) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
.hero-content > *:nth-child(5) {
|
||||||
|
animation-delay: 0.38s;
|
||||||
|
}
|
||||||
|
.hero-content > *:nth-child(6) {
|
||||||
|
animation-delay: 0.46s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hero-rise-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(18px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focused glow directly behind the hero title */
|
||||||
|
.hero-title {
|
||||||
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 46%;
|
||||||
|
z-index: -1;
|
||||||
|
width: 84%;
|
||||||
|
height: 74%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background: radial-gradient(closest-side, hsl(var(--brand-primary) / 0.2), transparent 78%);
|
||||||
|
filter: blur(48px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .hero-title::before {
|
||||||
|
background: radial-gradient(closest-side, hsl(var(--brand-primary) / 0.3), transparent 78%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll cue pinned to the bottom of the home hero */
|
||||||
|
.hero-scroll-anchor {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
left: 50%;
|
||||||
|
z-index: 10;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
opacity: 0;
|
||||||
|
animation: hero-fade-in 0.6s ease 0.7s forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-scroll-anchor:hover {
|
||||||
|
transform: translateX(-50%) translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hero-fade-in {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.hero-rise,
|
||||||
|
.hero-scroll-anchor {
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hero-title-line {
|
.hero-title-line {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding-inline: 0.02em;
|
padding-inline: 0.02em;
|
||||||
|
|||||||
@@ -1,30 +1,11 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { ArrowDown } from "lucide-react";
|
import { ArrowDown } from "lucide-react";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
|
|
||||||
import { Container } from "@/components/layout/container";
|
import { Container } from "@/components/layout/container";
|
||||||
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
|
import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
const heroItemVariants = {
|
|
||||||
hidden: { opacity: 0, y: 16 },
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: { duration: 0.54, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const heroContainerVariants = {
|
|
||||||
hidden: {},
|
|
||||||
visible: {
|
|
||||||
transition: { staggerChildren: 0.13, delayChildren: 0.08 },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
type HeroTone = "default" | "accent" | "soft";
|
type HeroTone = "default" | "accent" | "soft";
|
||||||
type HeroLineAlign = "start" | "center" | "end";
|
type HeroLineAlign = "start" | "center" | "end";
|
||||||
|
|
||||||
@@ -128,6 +109,14 @@ export function HeroShell({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
||||||
|
{variant === "home" ? (
|
||||||
|
<HeroScrollLink
|
||||||
|
href="#home-content"
|
||||||
|
label="Scroll to content"
|
||||||
|
className="hero-scroll-anchor hidden sm:inline-flex"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -136,27 +125,14 @@ export function HeroContentMotion({
|
|||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: HeroContentMotionProps) {
|
}: HeroContentMotionProps) {
|
||||||
return (
|
return <div className={cn("hero-content", className)}>{children}</div>;
|
||||||
<motion.div
|
|
||||||
className={className}
|
|
||||||
initial="hidden"
|
|
||||||
animate="visible"
|
|
||||||
variants={heroContainerVariants}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeroMotionItem({
|
export function HeroMotionItem({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: HeroContentMotionProps) {
|
}: HeroContentMotionProps) {
|
||||||
return (
|
return <div className={cn("hero-rise", className)}>{children}</div>;
|
||||||
<motion.div className={className} variants={heroItemVariants}>
|
|
||||||
{children}
|
|
||||||
</motion.div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeroTitle({
|
export function HeroTitle({
|
||||||
@@ -167,7 +143,7 @@ export function HeroTitle({
|
|||||||
}: HeroTitleProps) {
|
}: HeroTitleProps) {
|
||||||
const isArabic = locale === "ar";
|
const isArabic = locale === "ar";
|
||||||
const titleClassName = cn(
|
const titleClassName = cn(
|
||||||
"text-balance font-semibold text-[hsl(var(--hero-ink))]",
|
"hero-title hero-rise text-balance font-semibold text-[hsl(var(--hero-ink))]",
|
||||||
variant === "home"
|
variant === "home"
|
||||||
? isArabic
|
? isArabic
|
||||||
? "mt-6 flex w-full max-w-[26rem] flex-col gap-y-1 px-4 text-[clamp(2.85rem,11vw,5.9rem)] leading-[1.06] tracking-[-0.03em] sm:max-w-[30rem] sm:px-0 md:max-w-[36rem] lg:max-w-[32rem]"
|
? "mt-6 flex w-full max-w-[26rem] flex-col gap-y-1 px-4 text-[clamp(2.85rem,11vw,5.9rem)] leading-[1.06] tracking-[-0.03em] sm:max-w-[30rem] sm:px-0 md:max-w-[36rem] lg:max-w-[32rem]"
|
||||||
@@ -177,7 +153,7 @@ export function HeroTitle({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.h1 className={titleClassName} variants={heroItemVariants}>
|
<h1 className={titleClassName}>
|
||||||
{lines.map((line, index) => (
|
{lines.map((line, index) => (
|
||||||
<span
|
<span
|
||||||
key={`${index}-${line.text}`}
|
key={`${index}-${line.text}`}
|
||||||
@@ -202,7 +178,7 @@ export function HeroTitle({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</motion.h1>
|
</h1>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user