feat: cinematic scroll-driven landing pages with GSAP ScrollTrigger
- Enhanced HeroSection with pinned scroll animations, parallax layers, sticky storytelling sections, and scroll progress indicator - Rewrote TuxedoVideoHero with GSAP scroll-driven parallax video scaling, floating decorative orbs, and animated content reveals - Added scroll-triggered reveals (.reveal-text, .reveal-scale, .reveal-slide) to Tuxedo Corn index page with counter animations - Applied Apple-style hero sequences with smooth scrub animations - Integrated scroll timeline with progress bar showing scroll position
This commit is contained in:
+183
-35
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { motion, useInView } from "framer-motion";
|
import { motion, useInView, AnimatePresence } from "framer-motion";
|
||||||
import { useCart } from "@/context/CartContext";
|
import { useCart } from "@/context/CartContext";
|
||||||
import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero";
|
import TuxedoVideoHero from "@/components/storefront/TuxedoVideoHero";
|
||||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||||
@@ -13,6 +13,13 @@ import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
|||||||
import BrandStylesProvider from "@/components/storefront/BrandStylesProvider";
|
import BrandStylesProvider from "@/components/storefront/BrandStylesProvider";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { getBrandSettingsPublic } from "@/actions/brand-settings";
|
import { getBrandSettingsPublic } from "@/actions/brand-settings";
|
||||||
|
import { gsap } from "gsap";
|
||||||
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
|
|
||||||
|
// Register GSAP plugins
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
type Brand = {
|
type Brand = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -494,6 +501,112 @@ export default function TuxedoPage() {
|
|||||||
document.getElementById("products")?.scrollIntoView({ behavior: "smooth" });
|
document.getElementById("products")?.scrollIntoView({ behavior: "smooth" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── GSAP SCROLL ANIMATIONS ──────────────────────────────────────────────
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined") return;
|
||||||
|
|
||||||
|
const ctx = gsap.context(() => {
|
||||||
|
// Parallax floating elements
|
||||||
|
gsap.utils.toArray<Element>(".parallax-float").forEach((el, i) => {
|
||||||
|
gsap.to(el, {
|
||||||
|
y: -60 - i * 15,
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start: "top bottom",
|
||||||
|
end: "bottom top",
|
||||||
|
scrub: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Staggered text reveal on scroll
|
||||||
|
const revealTextElements = gsap.utils.toArray<Element>(".reveal-text");
|
||||||
|
revealTextElements.forEach((el) => {
|
||||||
|
gsap.fromTo(
|
||||||
|
el,
|
||||||
|
{ opacity: 0, y: 40, skewY: 2 },
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
skewY: 0,
|
||||||
|
duration: 1,
|
||||||
|
ease: "power3.out",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start: "top 85%",
|
||||||
|
toggleActions: "play none none reverse",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scale reveal for cards
|
||||||
|
const revealScaleElements = gsap.utils.toArray<Element>(".reveal-scale");
|
||||||
|
revealScaleElements.forEach((el, i) => {
|
||||||
|
gsap.fromTo(
|
||||||
|
el,
|
||||||
|
{ opacity: 0, scale: 0.9, y: 40 },
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
y: 0,
|
||||||
|
duration: 0.8,
|
||||||
|
delay: i * 0.08,
|
||||||
|
ease: "back.out(1.4)",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start: "top 85%",
|
||||||
|
toggleActions: "play none none reverse",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Counter animations
|
||||||
|
const counterElements = gsap.utils.toArray<Element>(".counter-animate");
|
||||||
|
counterElements.forEach((el) => {
|
||||||
|
const target = parseInt(el.getAttribute("data-target") || "0", 10);
|
||||||
|
const obj = { val: 0 };
|
||||||
|
gsap.to(obj, {
|
||||||
|
val: target,
|
||||||
|
duration: 2,
|
||||||
|
ease: "power2.out",
|
||||||
|
onUpdate: () => {
|
||||||
|
if (el.textContent !== null) {
|
||||||
|
el.textContent = Math.floor(obj.val).toLocaleString();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start: "top 85%",
|
||||||
|
toggleActions: "play none none none",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Horizontal reveal for section headers
|
||||||
|
gsap.utils.toArray<Element>(".reveal-slide").forEach((el) => {
|
||||||
|
gsap.fromTo(
|
||||||
|
el,
|
||||||
|
{ opacity: 0, x: -40 },
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
x: 0,
|
||||||
|
duration: 0.8,
|
||||||
|
ease: "power3.out",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start: "top 85%",
|
||||||
|
toggleActions: "play none none reverse",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => ctx.revert();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const featuredProducts = products.slice(0, 3);
|
const featuredProducts = products.slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -547,12 +660,12 @@ export default function TuxedoPage() {
|
|||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
<div className="mb-14 flex items-end justify-between">
|
<div className="mb-14 flex items-end justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Delivery Stops</p>
|
<p className="reveal-slide text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Delivery Stops</p>
|
||||||
<h2 className="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
<h2 className="reveal-text text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||||
Upcoming<br className="hidden md:block" /> Stops
|
Upcoming<br className="hidden md:block" /> Stops
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
<div className="reveal-scale mt-6 h-px w-12 bg-emerald-600" />
|
||||||
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
<p className="reveal-text mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
||||||
Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.
|
Find a nearby stop and preorder your corn. Pickup is easy and guaranteed.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -594,13 +707,13 @@ export default function TuxedoPage() {
|
|||||||
|
|
||||||
<section id="products" className="py-28 bg-white">
|
<section id="products" className="py-28 bg-white">
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
<div className="mb-14">
|
<div className="reveal-slide mb-14">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Farm-Direct</p>
|
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-600 mb-4">Farm-Direct</p>
|
||||||
<h2 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
<h2 className="reveal-text text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-[1.05]">
|
||||||
This Week's<br className="hidden md:block" /> Harvest
|
This Week's<br className="hidden md:block" /> Harvest
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-6 h-px w-12 bg-emerald-600" />
|
<div className="reveal-scale mt-6 h-px w-12 bg-emerald-600" />
|
||||||
<p className="mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
<p className="reveal-text mt-6 max-w-2xl text-lg text-stone-500 leading-relaxed">
|
||||||
Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.
|
Preorder for pickup at a stop, or have cooler boxes shipped directly to your door after the season.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -638,38 +751,73 @@ export default function TuxedoPage() {
|
|||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="story" className="py-24 bg-stone-950">
|
<section id="story" className="py-32 bg-stone-950 relative overflow-hidden">
|
||||||
|
{/* Parallax decorative elements */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none">
|
||||||
|
<div className="parallax-float absolute top-1/4 left-1/4 w-96 h-96 rounded-full opacity-5" style={{
|
||||||
|
background: "radial-gradient(circle, rgba(16,185,129,0.3) 0%, transparent 70%)",
|
||||||
|
filter: "blur(80px)",
|
||||||
|
}} />
|
||||||
|
<div className="parallax-float absolute bottom-1/4 right-1/4 w-64 h-64 rounded-full opacity-5" style={{
|
||||||
|
background: "radial-gradient(circle, rgba(255,215,0,0.3) 0%, transparent 70%)",
|
||||||
|
filter: "blur(60px)",
|
||||||
|
animationDelay: "0.5s",
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<LayoutContainer>
|
<LayoutContainer>
|
||||||
<div className="text-center max-w-2xl mx-auto">
|
<div className="text-center max-w-3xl mx-auto relative z-10">
|
||||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-emerald-400/60 mb-4">Our Story</p>
|
<p className="reveal-text text-[11px] font-semibold uppercase tracking-widest text-emerald-400/60 mb-4">Our Story</p>
|
||||||
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-white leading-[1.05] mb-6">
|
<h2 className="reveal-text text-4xl md:text-5xl lg:text-6xl font-black tracking-tight text-white leading-[1.05] mb-8">
|
||||||
Three Generations of Sweet Corn Excellence
|
Three Generations of<br className="hidden md:block" /> Sweet Corn Excellence
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mx-auto mt-6 mb-8 h-px w-12 bg-emerald-600/50" />
|
<div className="reveal-scale mx-auto mt-8 mb-10 h-px w-16 bg-gradient-to-r from-emerald-600 to-amber-500" />
|
||||||
<p className="text-stone-400 leading-relaxed text-lg">
|
<p className="reveal-text text-stone-400 leading-relaxed text-lg md:text-xl max-w-2xl mx-auto">
|
||||||
Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years.
|
Tuxedo Corn is the exclusive grower and shipper of Olathe Sweet Sweet Corn — developed for Colorado's high-altitude mountain climate and grown by the same family for over 40 years.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center justify-center gap-12 mt-10">
|
|
||||||
|
{/* Stats with counter animation */}
|
||||||
|
<div className="reveal-scale flex items-center justify-center gap-12 md:gap-16 mt-16 flex-wrap">
|
||||||
{[
|
{[
|
||||||
{ stat: "40+", label: "Years" },
|
{ stat: "40", suffix: "+", label: "Years Growing" },
|
||||||
{ stat: "3", label: "Generations" },
|
{ stat: "3", suffix: "", label: "Generations" },
|
||||||
{ stat: "100%", label: "Hand-Picked" },
|
{ stat: "100", suffix: "%", label: "Hand-Picked" },
|
||||||
].map(({ stat, label }) => (
|
].map((item, i) => (
|
||||||
<div key={label} className="text-center">
|
<div key={i} className="text-center">
|
||||||
<p className="text-3xl sm:text-4xl font-black text-white">{stat}</p>
|
<div className="text-4xl md:text-5xl lg:text-6xl font-black text-white" style={{ textShadow: "0 4px 30px rgba(16,185,129,0.3)" }}>
|
||||||
<p className="mt-1 text-xs text-stone-500 uppercase tracking-widest">{label}</p>
|
<span className="counter-animate" data-target={parseInt(item.stat, 10)}>0</span>
|
||||||
|
<span style={{ color: "#10b981" }}>{item.suffix}</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-[10px] uppercase tracking-[0.2em] text-stone-500">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
|
||||||
href="/tuxedo/about"
|
<div className="reveal-scale mt-16">
|
||||||
className="inline-flex items-center gap-2.5 mt-12 rounded-full bg-emerald-700 px-8 py-4 text-sm font-bold text-white hover:bg-emerald-600 active:bg-emerald-800 transition-colors"
|
<Link
|
||||||
>
|
href="/tuxedo/about"
|
||||||
Read Our Story
|
className="group inline-flex items-center gap-3 rounded-full px-10 py-4 text-sm font-bold transition-all duration-300"
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
style={{
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
background: "linear-gradient(135deg, #059669 0%, #10b981 100%)",
|
||||||
</svg>
|
boxShadow: "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||||
</Link>
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.transform = "translateY(-3px)";
|
||||||
|
e.currentTarget.style.boxShadow = "0 12px 40px rgba(16, 185, 129, 0.4), inset 0 1px 0 rgba(255,255,255,0.2)";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.transform = "translateY(0)";
|
||||||
|
e.currentTarget.style.boxShadow = "0 8px 32px rgba(16, 185, 129, 0.3), inset 0 1px 0 rgba(255,255,255,0.2)";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Read Our Story</span>
|
||||||
|
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import { gsap } from "gsap";
|
||||||
|
import { ScrollTrigger } from "gsap/ScrollTrigger";
|
||||||
|
|
||||||
|
// Register GSAP plugins
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
gsap.registerPlugin(ScrollTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
type TuxedoVideoHeroProps = {
|
type TuxedoVideoHeroProps = {
|
||||||
eyebrow?: string;
|
eyebrow?: string;
|
||||||
@@ -21,42 +28,9 @@ const VIDEO_URL =
|
|||||||
const OLATHE_SWEET_LOGO_DARK =
|
const OLATHE_SWEET_LOGO_DARK =
|
||||||
"https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo-dark.png";
|
"https://wnzkhezyhnfzhkhiflrp.supabase.co/storage/v1/object/public/brand-logos/64294306-5f42-463d-a5e8-2ad6c81a96de/olathe-sweet-logo-dark.png";
|
||||||
|
|
||||||
// Staggered animation variants
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
const fadeUpVariants = {
|
// CINEMATIC HERO - SCROLL-DRIVEN ANIMATIONS
|
||||||
hidden: { opacity: 0, y: 24 },
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
visible: (delay: number = 0) => ({
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: {
|
|
||||||
duration: 0.8,
|
|
||||||
ease: [0.25, 0.1, 0.25, 1] as [number, number, number, number],
|
|
||||||
delay,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerVariants = {
|
|
||||||
hidden: {},
|
|
||||||
visible: {
|
|
||||||
transition: {
|
|
||||||
staggerChildren: 0.15,
|
|
||||||
delayChildren: 0.3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Scroll indicator animation
|
|
||||||
const bounceVariants = {
|
|
||||||
animate: {
|
|
||||||
y: [0, 8, 0],
|
|
||||||
transition: {
|
|
||||||
duration: 1.6,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut" as const,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function TuxedoVideoHero({
|
export default function TuxedoVideoHero({
|
||||||
eyebrow,
|
eyebrow,
|
||||||
title,
|
title,
|
||||||
@@ -69,7 +43,10 @@ export default function TuxedoVideoHero({
|
|||||||
}: TuxedoVideoHeroProps) {
|
}: TuxedoVideoHeroProps) {
|
||||||
const logoSrc = olatheSweetLogoUrl ?? OLATHE_SWEET_LOGO_DARK;
|
const logoSrc = olatheSweetLogoUrl ?? OLATHE_SWEET_LOGO_DARK;
|
||||||
const sectionRef = useRef<HTMLElement>(null);
|
const sectionRef = useRef<HTMLElement>(null);
|
||||||
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [scrollProgress, setScrollProgress] = useState(0);
|
||||||
|
|
||||||
// Trigger entrance animation on mount
|
// Trigger entrance animation on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -77,6 +54,103 @@ export default function TuxedoVideoHero({
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// ─── GSAP SCROLL ANIMATIONS ────────────────────────────────────────────────
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window === "undefined" || !sectionRef.current) return;
|
||||||
|
|
||||||
|
const ctx = gsap.context(() => {
|
||||||
|
// Scroll progress tracking
|
||||||
|
ScrollTrigger.create({
|
||||||
|
trigger: sectionRef.current,
|
||||||
|
start: "top top",
|
||||||
|
end: "bottom top",
|
||||||
|
scrub: true,
|
||||||
|
onUpdate: (self) => {
|
||||||
|
setScrollProgress(self.progress);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hero content pin - fade out on scroll
|
||||||
|
if (contentRef.current) {
|
||||||
|
gsap.to(contentRef.current, {
|
||||||
|
y: -80,
|
||||||
|
opacity: 0,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: sectionRef.current,
|
||||||
|
start: "top top",
|
||||||
|
end: "40% top",
|
||||||
|
scrub: 1.2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Video scale + parallax
|
||||||
|
if (videoRef.current) {
|
||||||
|
gsap.to(videoRef.current, {
|
||||||
|
scale: 1.15,
|
||||||
|
y: 100,
|
||||||
|
ease: "power2.inOut",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: sectionRef.current,
|
||||||
|
start: "top top",
|
||||||
|
end: "50% top",
|
||||||
|
scrub: 1.5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Staggered entrance for content elements
|
||||||
|
const contentElements = gsap.utils.toArray<Element>(".hero-reveal");
|
||||||
|
contentElements.forEach((el, i) => {
|
||||||
|
gsap.fromTo(
|
||||||
|
el,
|
||||||
|
{ opacity: 0, y: 40, scale: 0.95 },
|
||||||
|
{
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
duration: 1,
|
||||||
|
ease: "power3.out",
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start: "top 90%",
|
||||||
|
toggleActions: "play none none reverse",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parallax floating elements
|
||||||
|
const floatElements = gsap.utils.toArray<Element>(".parallax-float");
|
||||||
|
floatElements.forEach((el, idx) => {
|
||||||
|
gsap.to(el, {
|
||||||
|
y: -80 - idx * 20,
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: el,
|
||||||
|
start: "top bottom",
|
||||||
|
end: "bottom top",
|
||||||
|
scrub: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll indicator bounce on scroll
|
||||||
|
gsap.to(".scroll-indicator", {
|
||||||
|
y: 20,
|
||||||
|
opacity: 0,
|
||||||
|
scrollTrigger: {
|
||||||
|
trigger: sectionRef.current,
|
||||||
|
start: "10% top",
|
||||||
|
end: "30% top",
|
||||||
|
scrub: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, sectionRef);
|
||||||
|
|
||||||
|
return () => ctx.revert();
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Smooth scroll to stops section
|
// Smooth scroll to stops section
|
||||||
const handlePrimaryClick = () => {
|
const handlePrimaryClick = () => {
|
||||||
const stopsSection = document.getElementById("stops");
|
const stopsSection = document.getElementById("stops");
|
||||||
@@ -88,160 +162,326 @@ export default function TuxedoVideoHero({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<>
|
||||||
ref={sectionRef}
|
{/* ─── SCROLL PROGRESS BAR ──────────────────────────────────────────── */}
|
||||||
className="relative min-h-[600px] md:min-h-[780px] flex items-center"
|
<div className="fixed top-0 left-0 right-0 h-1 z-[1000] bg-black/20">
|
||||||
>
|
<div
|
||||||
{/* Background video */}
|
className="h-full bg-gradient-to-r from-emerald-600 via-emerald-400 to-amber-400"
|
||||||
<video
|
style={{
|
||||||
autoPlay
|
transform: `scaleX(${scrollProgress})`,
|
||||||
muted
|
transformOrigin: "left",
|
||||||
loop
|
transition: "transform 0.1s ease-out",
|
||||||
playsInline
|
}}
|
||||||
className="absolute inset-0 w-full h-[110%] object-cover"
|
/>
|
||||||
style={{ zIndex: 0 }}
|
</div>
|
||||||
src={VIDEO_URL}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Gold/Warm gradient overlay */}
|
{/* ─── CINEMATIC HERO SECTION ──────────────────────────────────────── */}
|
||||||
<div
|
<section
|
||||||
className="absolute inset-0 pointer-events-none"
|
ref={sectionRef}
|
||||||
style={{
|
className="relative min-h-screen flex items-center overflow-hidden"
|
||||||
zIndex: 1,
|
|
||||||
background: "linear-gradient(135deg, rgba(255,215,0,0.1) 0%, rgba(255,193,7,0.06) 50%, rgba(255,235,0,0.04) 100%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Vignette overlay - black to yellow gradient */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 pointer-events-none"
|
|
||||||
style={{
|
|
||||||
zIndex: 2,
|
|
||||||
background: "radial-gradient(ellipse at center, rgba(255,200,0,0.1) 0%, rgba(0,0,0,0.5) 50%, rgba(0,0,0,0.95) 100%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Dark gradient overlay */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 pointer-events-none"
|
|
||||||
style={{
|
|
||||||
zIndex: 3,
|
|
||||||
background: "linear-gradient(to top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.25) 50%, rgba(0,0,0,0.1) 100%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Content container */}
|
|
||||||
<div
|
|
||||||
className="relative z-10 mx-auto w-full max-w-6xl px-6 pb-20 md:pb-28 pt-28 md:pt-36 flex flex-col justify-end"
|
|
||||||
style={{ minHeight: "600px" }}
|
|
||||||
>
|
>
|
||||||
<motion.div
|
{/* Background video with parallax */}
|
||||||
className="max-w-2xl"
|
<video
|
||||||
variants={containerVariants}
|
ref={videoRef}
|
||||||
initial="hidden"
|
autoPlay
|
||||||
animate={isVisible ? "visible" : "hidden"}
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
className="absolute inset-0 w-full h-[120%] object-cover"
|
||||||
|
style={{ zIndex: 0 }}
|
||||||
|
src={VIDEO_URL}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Gradient overlays */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
zIndex: 1,
|
||||||
|
background: "linear-gradient(135deg, rgba(255,215,0,0.12) 0%, rgba(255,193,7,0.06) 50%, rgba(255,235,0,0.04) 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
zIndex: 2,
|
||||||
|
background: "radial-gradient(ellipse at center, rgba(255,200,0,0.08) 0%, rgba(0,0,0,0.4) 50%, rgba(0,0,0,0.9) 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none"
|
||||||
|
style={{
|
||||||
|
zIndex: 3,
|
||||||
|
background: "linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.2) 50%, rgba(0,0,0,0.05) 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* ─── FLOATING DECORATIVE ELEMENTS ──────────────────────────────── */}
|
||||||
|
<div className="absolute inset-0 pointer-events-none z-[4]">
|
||||||
|
{/* Golden orb top-left */}
|
||||||
|
<div
|
||||||
|
className="parallax-float absolute top-1/4 left-1/4 w-64 h-64 rounded-full opacity-20"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle, rgba(255,215,0,0.4) 0%, transparent 70%)",
|
||||||
|
filter: "blur(40px)",
|
||||||
|
animation: "float-slow 10s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Emerald orb bottom-right */}
|
||||||
|
<div
|
||||||
|
className="parallax-float absolute bottom-1/3 right-1/4 w-48 h-48 rounded-full opacity-15"
|
||||||
|
style={{
|
||||||
|
background: "radial-gradient(circle, rgba(16,185,129,0.4) 0%, transparent 70%)",
|
||||||
|
filter: "blur(30px)",
|
||||||
|
animation: "float-delayed 12s ease-in-out infinite",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Subtle grain overlay */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-[0.03]"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E")`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── CONTENT CONTAINER ──────────────────────────────────────────── */}
|
||||||
|
<div
|
||||||
|
ref={contentRef}
|
||||||
|
className="relative z-10 mx-auto w-full max-w-6xl px-6 pb-32 pt-32 flex flex-col justify-end"
|
||||||
>
|
>
|
||||||
{/* Logo */}
|
<div className="max-w-3xl">
|
||||||
<motion.div
|
{/* Logo with glow effect */}
|
||||||
className="mb-8 relative h-16 md:h-20 w-[280px] md:w-[350px]"
|
<motion.div
|
||||||
variants={fadeUpVariants}
|
className="hero-reveal mb-10 relative h-20 md:h-24 w-[320px] md:w-[400px]"
|
||||||
>
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
<Image
|
animate={isVisible ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.9 }}
|
||||||
src={logoSrc}
|
transition={{ duration: 1, delay: 0.1, ease: [0.22, 0.61, 0.36, 1] }}
|
||||||
alt="Olathe Sweet"
|
|
||||||
fill
|
|
||||||
style={{ objectFit: "contain" }}
|
|
||||||
className="opacity-95"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Eyebrow */}
|
|
||||||
{eyebrow && (
|
|
||||||
<motion.p
|
|
||||||
className="text-[11px] font-semibold uppercase tracking-[0.25em] text-emerald-400/80 mb-5"
|
|
||||||
variants={fadeUpVariants}
|
|
||||||
custom={0.1}
|
|
||||||
>
|
>
|
||||||
{eyebrow}
|
<div className="absolute -inset-8 rounded-full bg-emerald-500/10 blur-2xl" />
|
||||||
</motion.p>
|
<Image
|
||||||
)}
|
src={logoSrc}
|
||||||
|
alt="Olathe Sweet"
|
||||||
|
fill
|
||||||
|
style={{ objectFit: "contain" }}
|
||||||
|
className="drop-shadow-2xl"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Eyebrow with animated underline */}
|
||||||
<motion.h1
|
<div className="hero-reveal mb-6">
|
||||||
className="text-5xl md:text-7xl font-black tracking-tight text-white leading-[1.05]"
|
{eyebrow && (
|
||||||
variants={fadeUpVariants}
|
<p className="text-[11px] font-bold uppercase tracking-[0.3em] text-amber-400/80">
|
||||||
custom={0.25}
|
{eyebrow}
|
||||||
>
|
</p>
|
||||||
{title}
|
)}
|
||||||
</motion.h1>
|
<div className="mt-3 h-px w-16 bg-gradient-to-r from-emerald-500 to-transparent" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Main Title - Apple-style reveal */}
|
||||||
<motion.p
|
<h1
|
||||||
className="mt-5 md:mt-6 text-lg md:text-2xl text-white/60 leading-relaxed font-light max-w-lg"
|
className="hero-reveal text-6xl md:text-7xl lg:text-8xl font-black tracking-tight text-white leading-[0.95] mb-6"
|
||||||
variants={fadeUpVariants}
|
style={{
|
||||||
custom={0.4}
|
textShadow: "0 4px 30px rgba(0,0,0,0.3), 0 0 60px rgba(255,215,0,0.1)",
|
||||||
>
|
}}
|
||||||
{description}
|
>
|
||||||
</motion.p>
|
{title.split(" ").map((word, idx) => (
|
||||||
|
<span
|
||||||
{/* CTAs */}
|
key={idx}
|
||||||
<motion.div
|
className="inline-block mr-4"
|
||||||
className="mt-8 md:mt-10 flex flex-wrap items-center gap-4"
|
style={{
|
||||||
variants={fadeUpVariants}
|
animationDelay: `${0.2 + idx * 0.1}s`,
|
||||||
custom={0.55}
|
}}
|
||||||
>
|
|
||||||
{primaryButton && (
|
|
||||||
<button
|
|
||||||
onClick={handlePrimaryClick}
|
|
||||||
className="rounded-full bg-emerald-600 hover:bg-emerald-500 active:bg-emerald-700 px-8 md:px-10 py-3.5 md:py-4 text-sm font-bold tracking-widest uppercase transition-all duration-200 hover:shadow-xl hover:shadow-emerald-900/40 hover:-translate-y-0.5 active:translate-y-0"
|
|
||||||
>
|
|
||||||
{primaryButton}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{secondaryButton && (
|
|
||||||
<button
|
|
||||||
onClick={onSecondaryClick}
|
|
||||||
className="flex items-center gap-2 text-sm font-semibold text-white/70 hover:text-white transition-colors duration-200 group"
|
|
||||||
>
|
|
||||||
<span className="uppercase tracking-widest text-[11px]">{secondaryButton}</span>
|
|
||||||
<svg
|
|
||||||
className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-1"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={2}
|
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
{word}
|
||||||
</svg>
|
</span>
|
||||||
</button>
|
))}
|
||||||
)}
|
</h1>
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
{/* Animated scroll indicator */}
|
{/* Description with typewriter-like reveal */}
|
||||||
<motion.div
|
<p
|
||||||
className="absolute bottom-8 left-1/2 -translate-x-1/2 z-20"
|
className="hero-reveal text-xl md:text-2xl text-white/70 leading-relaxed max-w-xl mb-10"
|
||||||
variants={bounceVariants}
|
style={{ fontWeight: 300 }}
|
||||||
animate="animate"
|
>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTAs with hover effects */}
|
||||||
|
<div className="hero-reveal flex flex-wrap items-center gap-5">
|
||||||
|
{primaryButton && (
|
||||||
|
<motion.button
|
||||||
|
onClick={handlePrimaryClick}
|
||||||
|
className="group relative rounded-full px-10 py-4 text-sm font-bold tracking-widest uppercase overflow-hidden"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #059669 0%, #10b981 100%)",
|
||||||
|
boxShadow: "0 8px 32px rgba(16, 185, 129, 0.4), inset 0 1px 0 rgba(255,255,255,0.2)",
|
||||||
|
}}
|
||||||
|
whileHover={{ scale: 1.02 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
>
|
||||||
|
<span className="relative z-10 flex items-center gap-3">
|
||||||
|
<span>{primaryButton}</span>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 transition-transform group-hover:translate-x-1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(135deg, #10b981 0%, #059669 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
{secondaryButton && (
|
||||||
|
<button
|
||||||
|
onClick={onSecondaryClick}
|
||||||
|
className="group flex items-center gap-3 text-sm font-semibold"
|
||||||
|
style={{ color: "rgba(255,255,255,0.8)" }}
|
||||||
|
>
|
||||||
|
<span className="uppercase tracking-widest text-[11px]">{secondaryButton}</span>
|
||||||
|
<span
|
||||||
|
className="w-10 h-10 rounded-full border flex items-center justify-center transition-all duration-300 group-hover:bg-white/10 group-hover:border-white/30"
|
||||||
|
style={{ borderColor: "rgba(255,255,255,0.3)" }}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 transition-transform group-hover:translate-x-0.5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats row */}
|
||||||
|
<div className="hero-reveal mt-16 flex flex-wrap gap-12">
|
||||||
|
{[
|
||||||
|
{ stat: "40+", label: "Years Growing" },
|
||||||
|
{ stat: "3", label: "Generations" },
|
||||||
|
{ stat: "100%", label: "Hand-Picked" },
|
||||||
|
].map((item, i) => (
|
||||||
|
<div key={i} className="text-center">
|
||||||
|
<div
|
||||||
|
className="text-3xl md:text-4xl font-black text-white"
|
||||||
|
style={{ textShadow: "0 2px 20px rgba(0,0,0,0.3)" }}
|
||||||
|
>
|
||||||
|
{item.stat}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-[10px] uppercase tracking-[0.2em] mt-1"
|
||||||
|
style={{ color: "rgba(255,255,255,0.5)" }}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ─── SCROLL INDICATOR ────────────────────────────────────────────── */}
|
||||||
|
<div
|
||||||
|
className="scroll-indicator absolute bottom-10 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-3"
|
||||||
|
style={{ pointerEvents: "auto" }}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={handlePrimaryClick}
|
onClick={handlePrimaryClick}
|
||||||
className="flex flex-col items-center gap-2 text-white/50 hover:text-white/80 transition-colors duration-200"
|
className="flex flex-col items-center gap-2 text-white/50 hover:text-white/80 transition-colors duration-200"
|
||||||
aria-label="Scroll to stops"
|
aria-label="Scroll to stops"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] uppercase tracking-[0.2em] font-medium">Scroll</span>
|
<span className="text-[10px] uppercase tracking-[0.25em] font-medium">Explore</span>
|
||||||
<svg
|
<div
|
||||||
className="h-5 w-5"
|
className="w-6 h-10 rounded-full border border-white/30 flex items-start justify-center p-1.5"
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
<div
|
||||||
</svg>
|
className="w-1.5 h-3 rounded-full bg-white animate-bounce"
|
||||||
|
style={{ animationDuration: "1.5s" }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
{/* ─── CORNER DECORATIONS ───────────────────────────────────────────── */}
|
||||||
|
<div
|
||||||
|
className="parallax-float absolute top-8 right-8 opacity-30"
|
||||||
|
style={{ animationDelay: "1s" }}
|
||||||
|
>
|
||||||
|
<svg className="w-16 h-16" viewBox="0 0 64 64" fill="none">
|
||||||
|
<path
|
||||||
|
d="M32 4C48 16, 56 32, 48 56C40 64, 24 64, 8 56C0 32, 16 16, 32 4"
|
||||||
|
stroke="rgba(255,215,0,0.5)"
|
||||||
|
strokeWidth="1"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M32 12C44 22, 48 36, 42 52C36 58, 26 58, 18 52C12 36, 20 22, 32 12"
|
||||||
|
stroke="rgba(255,215,0,0.3)"
|
||||||
|
strokeWidth="1"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="parallax-float absolute bottom-20 left-8 opacity-20"
|
||||||
|
style={{ animationDelay: "1.5s" }}
|
||||||
|
>
|
||||||
|
<svg className="w-12 h-12" viewBox="0 0 48 48" fill="none">
|
||||||
|
<circle cx="24" cy="24" r="20" stroke="rgba(16,185,129,0.4)" strokeWidth="1" />
|
||||||
|
<circle cx="24" cy="24" r="14" stroke="rgba(16,185,129,0.3)" strokeWidth="1" />
|
||||||
|
<circle cx="24" cy="24" r="8" stroke="rgba(16,185,129,0.2)" strokeWidth="1" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ─── GLOBAL ANIMATIONS ──────────────────────────────────────────────── */}
|
||||||
|
<style jsx>{`
|
||||||
|
@keyframes float-slow {
|
||||||
|
0%, 100% { transform: translate(0, 0); }
|
||||||
|
33% { transform: translate(15px, -20px); }
|
||||||
|
66% { transform: translate(-10px, 10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-delayed {
|
||||||
|
0%, 100% { transform: translate(0, 0); }
|
||||||
|
33% { transform: translate(-20px, 15px); }
|
||||||
|
66% { transform: translate(10px, -10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 100% { transform: translateY(0); }
|
||||||
|
50% { transform: translateY(6px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-bounce {
|
||||||
|
animation: bounce 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance optimizations */
|
||||||
|
.parallax-float {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-reveal {
|
||||||
|
will-change: opacity, transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions */
|
||||||
|
button, a {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user