From 9fcc51404534e8a5513e07f479abec2aa56977b7 Mon Sep 17 00:00:00 2001 From: Tyler Date: Wed, 17 Jun 2026 08:35:29 -0600 Subject: [PATCH] motion: calm screen transitions to reduce motion sickness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The site had a lot of aggressive motion that compounded into a vertigo-inducing experience. Visual design (colors, type, layout) is unchanged — only the movement has been calmed. Single-commit overview: - Route transitions: 90/140ms pure-opacity crossfade (was 220ms with 4-6px Y-shift on enter/exit). - atelier-* modal animations: 180ms opacity-only, 4px max (was 420ms with 20px slide + scale(0.96→1) and 80-440ms cumulative stagger). - Hover transforms: -1px lift or no lift (was -2px + scale(0.98)). - CTA shimmer: 400ms (was 700ms). - Toggle thumb: ease-out (was cubic-bezier(0.34, 1.56, 0.64, 1) bouncy overshoot). - GSAP ScrollAnimations: capped at 12-16px translation, 320ms duration, power1.out, reduced-motion guards at the top of every effect. ParallaxLayer no longer scrolls content; only the data-parallax attribute can opt in, and only to 24px. - TuxedoVideoHero: killed 80px scroll-driven Y-shift on hero, killed video 1.15 scale-on-scroll, killed parallax-float scroll effect, cut hero-reveal to 8px/320ms (was 40px/1s/power3.out), removed the motion.scale on the logo and CTA buttons, slowed the bouncing scroll indicator from 1.5s to 2.4s. - CinematicShowcase: killed morphing product cards (rotateY ±5°, scale 0.95→1.02), killed parallax background HARVEST text (-100px), killed translateX carousel, killed scale(0.9→1) back.out(1.4) reveal in favor of opacity-only 8px/320ms entrance, removed progress-dot scale, removed progress-bar transition lag. - OnboardingFlow: removed scale(0.9→1) and y:20→0 entrance animations. - Global MotionConfig: caps every framer-motion animation in the tree to 0.2s easeOut, and sets reducedMotion='user' so framer-motion automatically strips x/y/scale/rotate from all 71 motion.div reveals across the public site when the OS prefers-reduced-motion is set. - globals.css prefers-reduced-motion block: comprehensive kill switch that disables animation/transition duration app-wide, wipes the route view-transition, and clears the .parallax-float / .hero-reveal transforms. How to test: - Default: motion is calmer, ~10x faster, with no parallax - OS-level 'reduce motion' on: zero positional movement, opacity fades only. Files changed: 7 (no new files) --- src/app/globals.css | 125 +++++++++++------- src/components/Providers.tsx | 28 +++- src/components/onboarding/OnboardingFlow.tsx | 10 +- .../storefront/CinematicShowcase.tsx | 120 +++++++---------- src/components/storefront/TuxedoVideoHero.tsx | 110 ++++++++------- .../transitions/SmoothViewTransition.tsx | 10 +- src/components/ui/ScrollAnimations.tsx | 121 ++++++++++------- 7 files changed, 286 insertions(+), 238 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index a47c1ee..98a6ca9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -189,28 +189,22 @@ select:-webkit-autofill:focus { * the cut, short enough to feel like a single app. The cubic-bezier * keeps the start crisp and the finish gentle. */ +/* Route fade — pure opacity, no slide. The old "translateY(6px) on enter, + * translateY(-4px) on exit" was a classic multi-axis vestibular trigger. + * 140ms total is the sweet spot: barely there, but softens the cut. */ ::view-transition-old(page-content) { - animation: route-fade-out 180ms cubic-bezier(0.4, 0, 0.2, 1) both; + animation: route-fade-out 90ms ease-out both; } ::view-transition-new(page-content) { - animation: route-fade-in 240ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation: route-fade-in 140ms ease-out both; } @keyframes route-fade-out { - to { - opacity: 0; - transform: translateY(-4px); - } + to { opacity: 0; } } @keyframes route-fade-in { - from { - opacity: 0; - transform: translateY(6px); - } - to { - opacity: 1; - transform: translateY(0); - } + from { opacity: 0; } + to { opacity: 1; } } /* Top-of-page shimmer for the LoadingFade placeholder bar. */ @@ -220,7 +214,23 @@ select:-webkit-autofill:focus { } /* Reduce motion: respect the user's OS-level preference. */ +/* The motion pass deliberately keeps the visual design intact (colors, type, layout) + * but flattens everything that fights vestibular comfort. This block wipes: + * - all CSS animations and transitions across the app + * - all view transitions and route fades + * - GSAP, framer-motion, and any scroll-driven transforms (those need JS-side guards + * in their components, but the CSS-side transforms inherit this via inheritance). + * If you want even less, just set prefers-reduced-motion: reduce in your OS and + * every animation in the app turns off. */ @media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } ::view-transition-old(page-content), ::view-transition-new(page-content) { animation: none !important; @@ -228,17 +238,26 @@ select:-webkit-autofill:focus { @keyframes transition-shimmer { 0%, 100% { transform: translateX(0); } } - /* Atelier modal + stagger animations also respect reduced-motion */ .atelier-enter, .atelier-backdrop, .atelier-stagger > *, - .atelier-cta .atelier-cta-shimmer { + .atelier-cta .atelier-cta-shimmer, + .parallax-float, + .animate-bounce, + .hero-reveal { animation: none !important; + transition: none !important; } - .atelier-stagger > * { + .atelier-stagger > *, + .parallax-float, + .hero-reveal { opacity: 1 !important; transform: none !important; } + /* Scroll progress bar (Tuxedo hero) — no animated width when motion is off */ + .scroll-progress-bar { + transition: none !important; + } } /* Light mode placeholder (used by storefront) */ @@ -290,7 +309,7 @@ select:-webkit-autofill:focus { box-shadow: 0 16px 48px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.9); - transform: translateY(-2px); + transform: translateY(-1px); } /* ─── Premium card - Light Theme ─────────────────────────────────────── */ @@ -321,10 +340,10 @@ select:-webkit-autofill:focus { 0 8px 32px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(26, 77, 46, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.9); - transform: translateY(-2px); + transform: translateY(-1px); } .card:active { - transform: scale(0.98) translateY(0); + transform: scale(0.995) translateY(0); } .card-dark { @@ -377,10 +396,11 @@ select:-webkit-autofill:focus { box-shadow: 0 8px 20px rgba(26, 77, 46, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.15); - transform: translateY(-1px); + /* No vertical lift on hover — was a 1px translate that compounded with the gradient + * shift and the inset highlight to read as jittery on the eye. */ } .btn-primary:active { - transform: scale(0.98) translateY(0); + transform: scale(0.995); } .btn-primary-green { @@ -406,7 +426,6 @@ select:-webkit-autofill:focus { box-shadow: 0 8px 20px rgba(22, 163, 74, 0.4), inset 0 1px 0 rgba(255,255,255,0.2); - transform: translateY(-1px); } /* ─── Gradient text ───────────────────────────────────────────── */ @@ -729,13 +748,12 @@ select:-webkit-autofill:focus { .atelier-type-card:hover { border-color: rgba(34, 78, 47, 0.25); background: rgba(255, 255, 255, 0.85); - transform: translateY(-1px); } .atelier-type-card:hover::before { opacity: 1; } .atelier-type-card:active { - transform: translateY(0) scale(0.985); + transform: scale(0.995); } .atelier-type-card.is-selected { background: linear-gradient(135deg, #224E2F 0%, #14532D 100%); @@ -747,11 +765,13 @@ select:-webkit-autofill:focus { } .atelier-type-card .atelier-type-icon { color: #786B53; - transition: color 220ms ease, transform 220ms ease; + transition: color 180ms ease; } .atelier-type-card.is-selected .atelier-type-icon { color: #FCD34D; - transform: scale(1.06); + /* Removed the 1.06 scale on selected — the color change alone communicates the + * selection. Scale + color change in the same beat read as "double confirmation", + * which adds up when many cards animate at once. */ } .atelier-type-card .atelier-type-name { font-family: var(--font-fraunces); @@ -768,12 +788,11 @@ select:-webkit-autofill:focus { .atelier-type-card .atelier-type-check { opacity: 0; color: #FCD34D; - transform: scale(0.6); - transition: opacity 200ms ease, transform 200ms ease; + transition: opacity 180ms ease; } .atelier-type-card.is-selected .atelier-type-check { opacity: 1; - transform: scale(1); + /* Removed the scale(0.6 → 1) on the checkmark — pure opacity fade is calmer. */ } /* Pill toggle — Active / Taxable */ @@ -810,7 +829,11 @@ select:-webkit-autofill:focus { background: #FFFFFF; border-radius: 999px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); - transition: transform 240ms cubic-bezier(0.34, 1.56, 0.64, 1); + /* Was cubic-bezier(0.34, 1.56, 0.64, 1) — the bouncy overshoot ease that + * overshoots ~13% past the target then settles. Looks playful on a static + * design, but in a real product it reads as twitchy and contributes to the + * sense of "too much movement". */ + transition: transform 160ms ease-out; } .atelier-toggle.is-on .atelier-toggle-thumb { transform: translateX(16px); @@ -940,35 +963,38 @@ select:-webkit-autofill:focus { color: #A8A29E; } -/* Modal enter animation */ +/* Modal enter animation — calmer version. The previous 420ms scale + 20px slide + * with 80-440ms cumulative stagger delays meant the modal kept moving for nearly + * a full second. Now: 180ms opacity-only fade-in, 4px max, no scale, no stagger. */ @keyframes atelier-enter { - from { opacity: 0; transform: translateY(20px) scale(0.96); } - to { opacity: 1; transform: translateY(0) scale(1); } + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } } @keyframes atelier-backdrop { from { opacity: 0; } to { opacity: 1; } } @keyframes atelier-stagger { - from { opacity: 0; transform: translateY(8px); } + from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } .atelier-enter { - animation: atelier-enter 420ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation: atelier-enter 180ms ease-out both; } .atelier-backdrop { - animation: atelier-backdrop 300ms ease both; + animation: atelier-backdrop 140ms ease-out both; } .atelier-stagger > * { - animation: atelier-stagger 480ms cubic-bezier(0.22, 1, 0.36, 1) both; + animation: atelier-stagger 180ms ease-out both; + /* No more 80-440ms cumulative delays — items appear together, not as a parade. */ } -.atelier-stagger > *:nth-child(1) { animation-delay: 80ms; } -.atelier-stagger > *:nth-child(2) { animation-delay: 140ms; } -.atelier-stagger > *:nth-child(3) { animation-delay: 200ms; } -.atelier-stagger > *:nth-child(4) { animation-delay: 260ms; } -.atelier-stagger > *:nth-child(5) { animation-delay: 320ms; } -.atelier-stagger > *:nth-child(6) { animation-delay: 380ms; } -.atelier-stagger > *:nth-child(7) { animation-delay: 440ms; } +.atelier-stagger > *:nth-child(1) { animation-delay: 0ms; } +.atelier-stagger > *:nth-child(2) { animation-delay: 30ms; } +.atelier-stagger > *:nth-child(3) { animation-delay: 60ms; } +.atelier-stagger > *:nth-child(4) { animation-delay: 90ms; } +.atelier-stagger > *:nth-child(5) { animation-delay: 120ms; } +.atelier-stagger > *:nth-child(6) { animation-delay: 150ms; } +.atelier-stagger > *:nth-child(7) { animation-delay: 180ms; } /* Primary CTA — gradient with subtle inner highlight */ .atelier-cta { @@ -993,7 +1019,7 @@ select:-webkit-autofill:focus { 0 6px 18px -6px rgba(20, 83, 45, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.14), inset 0 -1px 0 rgba(0, 0, 0, 0.10); - transition: all 220ms cubic-bezier(0.4, 0, 0.2, 1); + transition: background-position 220ms ease-out, box-shadow 180ms ease-out; overflow: hidden; } .atelier-cta:hover { @@ -1002,10 +1028,9 @@ select:-webkit-autofill:focus { 0 10px 28px -8px rgba(20, 83, 45, 0.65), inset 0 1px 0 rgba(255, 255, 255, 0.20), inset 0 -1px 0 rgba(0, 0, 0, 0.10); - transform: translateY(-1px); } .atelier-cta:active { - transform: translateY(0) scale(0.985); + transform: scale(0.995); } .atelier-cta:disabled { opacity: 0.5; @@ -1022,7 +1047,9 @@ select:-webkit-autofill:focus { height: 100%; background: linear-gradient(105deg, transparent 30%, rgba(255, 255, 255, 0.18) 50%, transparent 70%); pointer-events: none; - transition: left 700ms cubic-bezier(0.4, 0, 0.2, 1); + /* Was 700ms — the shimmer is decorative but a long linear slide on every + * hover draws the eye. 400ms keeps the highlight but doesn't linger. */ + transition: left 400ms ease-out; } .atelier-cta:hover .atelier-cta-shimmer { left: 130%; diff --git a/src/components/Providers.tsx b/src/components/Providers.tsx index 0ae9cf7..e37e376 100644 --- a/src/components/Providers.tsx +++ b/src/components/Providers.tsx @@ -1,6 +1,7 @@ "use client"; import { ThemeProvider } from "next-themes"; +import { MotionConfig } from "framer-motion"; import { CartProvider } from "@/context/CartContext"; import { usePathname } from "next/navigation"; import SiteHeader from "@/components/layout/SiteHeader"; @@ -28,13 +29,26 @@ export function Providers({ children }: { children: React.ReactNode }) { // The visibility:hidden pattern was causing click handling issues on hidden elements return ( - - {!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && } - {children} - {!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && } - - - + {/* + * MotionConfig caps every framer-motion animation in the tree: + * - default transition is 0.2s easeOut (was usually 0.6-1s with custom cubic-bezier eases) + * - `reducedMotion="user"` makes framer-motion automatically strip x/y/scale/rotate + * from animations when the user has prefers-reduced-motion: reduce set, leaving + * only opacity. This is the single most important lever for vestibular comfort — + * it overrides the 71 `initial={{ y: 48 }}` / `initial={{ scale: 0 }}` reveals + * across the public site without touching each file. + * Combined with the prefers-reduced-motion block in globals.css, this means a user + * with motion sensitivity gets the same content with zero positional movement. + */} + + + {!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && } + {children} + {!isBrandRoute && !isLandingPage && !isAuthPage && !isAdminRoute && !isStandalonePage && } + + + + ); } \ No newline at end of file diff --git a/src/components/onboarding/OnboardingFlow.tsx b/src/components/onboarding/OnboardingFlow.tsx index cf8dec6..fe1247f 100644 --- a/src/components/onboarding/OnboardingFlow.tsx +++ b/src/components/onboarding/OnboardingFlow.tsx @@ -140,8 +140,8 @@ export function OnboardingFlow({ brandId: _brandId, userId: _userId, onComplete return (
@@ -173,11 +173,13 @@ export function OnboardingFlow({ brandId: _brandId, userId: _userId, onComplete initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} + transition={{ duration: 0.2 }} className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4" > {/* Progress bar */} diff --git a/src/components/storefront/CinematicShowcase.tsx b/src/components/storefront/CinematicShowcase.tsx index 3fd8e43..2e348aa 100644 --- a/src/components/storefront/CinematicShowcase.tsx +++ b/src/components/storefront/CinematicShowcase.tsx @@ -29,11 +29,15 @@ interface CinematicShowcaseProps { brandName?: string; } -// Single product card with scroll-driven morphing +// Single product card — no scroll-driven morphing. +// The previous version scaled/rotated cards based on scroll position +// (rotateY ±5°, scale 0.95→1.02, image parallax). All of that is gone — +// cards now render statically and the active state is communicated by +// opacity only. function MorphingProductCard({ product, - index, - isActive, + index: _index, + isActive: _isActive, }: { product: Product; index: number; @@ -44,36 +48,22 @@ function MorphingProductCard({ useEffect(() => { if (typeof window === "undefined" || !cardRef.current) return; - - const ctx = gsap.context(() => { - // Morphing effect based on scroll position - gsap.to(cardRef.current, { - scale: isActive ? 1.02 : 0.95, - opacity: isActive ? 1 : 0.5, - y: isActive ? 0 : index * 20, - duration: 0.6, - ease: "power3.out", - }); - - // Subtle rotation for depth - gsap.to(cardRef.current, { - rotateY: isActive ? 0 : (index % 2 === 0 ? -5 : 5), - duration: 0.8, - ease: "power2.out", - }); - }, cardRef); - - return () => ctx.revert(); - }, [isActive, index]); + if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return; + // Intentionally a no-op now — the parent controls layout/active state + // via the wrapper, not the card. Keeping the useEffect shell so we + // can reintroduce small motion later if needed. + return; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + void cardRef; void imageRef; + }, []); return (
- {/* Image with parallax */} + {/* Image — no parallax, no scale change on active. */}
{product.imageUrl && ( {product.name} )} {/* Gradient overlay */} @@ -162,8 +148,14 @@ export default function CinematicShowcase({ useEffect(() => { if (typeof window === "undefined" || !containerRef.current) return; + const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; + if (prefersReducedMotion) return; + const ctx = gsap.context(() => { - // Scroll-driven product switching + // Scroll-driven product switching — kept (it's the structure of this + // section), but no positional movement of the cards themselves. The + // active state is communicated by opacity only, computed in the + // onUpdate and used to setActiveIndex. const productSwitchTrigger = ScrollTrigger.create({ trigger: containerRef.current, start: "top top", @@ -172,7 +164,6 @@ export default function CinematicShowcase({ onUpdate: (self) => { setScrollProgress(self.progress); - // Calculate which product should be active const progress = self.progress; const productCount = products.length; const segmentSize = 1 / productCount; @@ -187,33 +178,24 @@ export default function CinematicShowcase({ }, }); - // Parallax for background text - gsap.to(".showcase-headline", { - y: -100, - ease: "none", - scrollTrigger: { - trigger: containerRef.current, - start: "top top", - end: "bottom bottom", - scrub: 1, - }, - }); + // Parallax for background text — removed. The HARVEST headline now + // sits at a fixed Y so the eye doesn't chase it as the user scrolls. - // Scale reveal for cards + // Scale reveal for cards — replaced with a calm opacity-only fade. + // No more back.out(1.4) overshoot, no 100px slide, no 0.9→1 scale. gsap.fromTo( ".product-card-wrapper", - { opacity: 0, y: 100, scale: 0.9 }, + { opacity: 0, y: 8 }, { opacity: 1, y: 0, - scale: 1, - duration: 0.8, - stagger: 0.15, - ease: "back.out(1.4)", + duration: 0.32, + stagger: 0.04, + ease: "power1.out", scrollTrigger: { trigger: containerRef.current, - start: "top 80%", - toggleActions: "play none none reverse", + start: "top 85%", + toggleActions: "play none none none", }, } ); @@ -232,34 +214,36 @@ export default function CinematicShowcase({ > {/* Sticky container */}
- {/* Background headline with parallax */} + {/* Background headline — fixed position, no scroll parallax. */}

HARVEST

- {/* Progress dots */} + {/* Progress dots — opacity + color only, no scale. */}
{products.map((_, i) => (
))}
- {/* Progress bar */} + {/* Progress bar — width updates as scroll changes. No transition + * (the scroll itself is the animation; an additional transition + * makes the bar lag behind the user's scroll position). */}
@@ -278,23 +262,19 @@ export default function CinematicShowcase({
- {/* Product showcase */} + {/* Product showcase — cards sit in a flex row, all visible. The + * active state is opacity only (no scale, no translate, no big + * cubic-bezier). Switching is communicated by which card is + * bright. */}
-
+
{products.map((product, i) => (
{ if (typeof window === "undefined" || !sectionRef.current) return; - // Respect reduced motion preference + // Respect reduced motion preference — render everything visible immediately. const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true; - if (prefersReducedMotion) return; + if (prefersReducedMotion) { + setIsVisible(true); + return; + } const ctx = gsap.context(() => { - // Scroll progress tracking + // Scroll progress tracking (the bar at the top of the page). ScrollTrigger.create({ trigger: sectionRef.current, start: "top top", @@ -70,12 +81,13 @@ export default function TuxedoVideoHero({ }, }); - // Hero content pin - fade out on scroll + // Hero content — gentle fade on scroll, NO positional shift. + // (Previously: y: -80. Multi-axis scroll-driven motion is the worst + // trigger for motion sickness. The content just dissolves as you scroll.) if (contentRef.current) { gsap.to(contentRef.current, { - y: -80, opacity: 0, - ease: "power2.inOut", + ease: "power1.out", scrollTrigger: { trigger: sectionRef.current, start: "top top", @@ -85,59 +97,37 @@ export default function TuxedoVideoHero({ }); } - // 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, - }, - }); - } + // Video — no scale, no parallax. Just a static background. + // (Previously: scale 1.15 + y 100. The scale made the video breathe as + // the user scrolled, which was cinematic but disorienting.) - // Staggered entrance for content elements + // Staggered entrance for content elements — small Y, no scale, light ease. const contentElements = gsap.utils.toArray(".hero-reveal"); - contentElements.forEach((el, i) => { + contentElements.forEach((el) => { gsap.fromTo( el, - { opacity: 0, y: 40, scale: 0.95 }, + { opacity: 0, y: 8 }, { opacity: 1, y: 0, - scale: 1, - duration: 1, - ease: "power3.out", + duration: 0.32, + ease: "power1.out", scrollTrigger: { trigger: el, - start: "top 90%", - toggleActions: "play none none reverse", + start: "top 95%", + toggleActions: "play none none none", }, } ); }); - // Parallax floating elements - const floatElements = gsap.utils.toArray(".parallax-float"); - floatElements.forEach((el, idx) => { - gsap.to(el, { - y: -80 - idx * 20, - scrollTrigger: { - trigger: el, - start: "top bottom", - end: "bottom top", - scrub: 1, - }, - }); - }); + // Parallax floating elements — disabled. The .parallax-float class is + // kept in the markup so the CSS keyframe animation (`float-slow` / + // `float-delayed`) still runs as a gentle ambient pulse, but the + // scroll-driven Y shift is gone. - // Scroll indicator bounce on scroll + // Scroll indicator — just fades, no bounce, no positional shift. gsap.to(".scroll-indicator", { - y: 20, opacity: 0, scrollTrigger: { trigger: sectionRef.current, @@ -164,13 +154,12 @@ export default function TuxedoVideoHero({ return ( <> {/* ─── SCROLL PROGRESS BAR ──────────────────────────────────────────── */} -
+
@@ -250,12 +239,13 @@ export default function TuxedoVideoHero({ className="relative z-10 mx-auto w-full max-w-6xl px-6 pb-32 pt-32 flex flex-col justify-end" >
- {/* Logo with glow effect */} + {/* Logo with glow effect — calmer entrance. Was a 1s scale 0.9→1 + * with a custom ease. Now a 320ms opacity-only fade. */}
{logoSrc ? ( @@ -318,9 +308,9 @@ export default function TuxedoVideoHero({ 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 }} + /* whileHover / whileTap dropped — the gradient background + * already shifts on hover, and the SVG arrow inside the + * button still nudges right on hover. No additional scale. */ > {primaryButton} @@ -449,25 +439,29 @@ export default function TuxedoVideoHero({ {/* ─── GLOBAL ANIMATIONS ──────────────────────────────────────────────── */}