From 5b019052b1b9e1696336476331b63db154d1f386 Mon Sep 17 00:00:00 2001 From: moh Date: Sun, 20 Sep 2026 18:53:48 +0200 Subject: [PATCH] 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. --- app/globals.css | 89 +++++++++++++++++++++++++++++++++ components/layout/site-hero.tsx | 50 +++++------------- 2 files changed, 102 insertions(+), 37 deletions(-) diff --git a/app/globals.css b/app/globals.css index 649f99f..9ff5a70 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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 { display: inline-block; padding-inline: 0.02em; diff --git a/components/layout/site-hero.tsx b/components/layout/site-hero.tsx index 29669c7..3ccba6d 100644 --- a/components/layout/site-hero.tsx +++ b/components/layout/site-hero.tsx @@ -1,30 +1,11 @@ -"use client"; - import type { ReactNode } from "react"; import { ArrowDown } from "lucide-react"; -import { motion } from "framer-motion"; import { Container } from "@/components/layout/container"; import { HeroMotionBackdrop } from "@/components/layout/hero-motion-backdrop"; 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 HeroLineAlign = "start" | "center" | "end"; @@ -128,6 +109,14 @@ export function HeroShell({ {children} + + {variant === "home" ? ( + + ) : null} ); } @@ -136,27 +125,14 @@ export function HeroContentMotion({ children, className, }: HeroContentMotionProps) { - return ( - - {children} - - ); + return
{children}
; } export function HeroMotionItem({ children, className, }: HeroContentMotionProps) { - return ( - - {children} - - ); + return
{children}
; } export function HeroTitle({ @@ -167,7 +143,7 @@ export function HeroTitle({ }: HeroTitleProps) { const isArabic = locale === "ar"; 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" ? 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]" @@ -177,7 +153,7 @@ export function HeroTitle({ ); return ( - +

{lines.map((line, index) => ( ))} - +

); }