1 Commits

Author SHA1 Message Date
denshooter c847a456a3 feat: polish motion with stronger easing, press feedback and reduced motion
- Override Tailwind ease-out/ease-in-out with stronger curves, add ease-drawer
- hoverOnlyWhenSupported so hover styles never stick on touch devices
- Replace transition-all with explicit properties across the public UI
- Press feedback via active:scale-[0.97] instead of hover:scale-105
- Global prefers-reduced-motion handling (CSS + MotionConfig reducedMotion="user")
- Mobile menu is a real drawer: stays mounted, inert when closed, Escape closes
- Progress and music bars animate scaleX/scaleY instead of width/height
- Fix undefined animate-spin-slow, contain hero CTA shine with overflow-hidden

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:31:58 +02:00
20 changed files with 178 additions and 123 deletions
+2 -2
View File
@@ -46,7 +46,7 @@ export default function BooksPage() {
href={`/${locale}`} href={`/${locale}`}
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group" className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
> >
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" /> <ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform duration-200 ease-out" />
<span className="font-bold uppercase tracking-widest text-xs">{common("backToHome")}</span> <span className="font-bold uppercase tracking-widest text-xs">{common("backToHome")}</span>
</Link> </Link>
<h1 className="text-6xl md:text-[10rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase"> <h1 className="text-6xl md:text-[10rem] font-black tracking-tighter text-stone-900 dark:text-stone-50 leading-[0.85] uppercase">
@@ -76,7 +76,7 @@ export default function BooksPage() {
) : ( ) : (
reviews.map((review, i) => ( reviews.map((review, i) => (
<ScrollFadeIn key={review.id} delay={i * 0.1}> <ScrollFadeIn key={review.id} delay={i * 0.1}>
<div className="bg-white dark:bg-stone-900 rounded-[2.5rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col h-full hover:shadow-xl transition-all"> <div className="bg-white dark:bg-stone-900 rounded-[2.5rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm flex flex-col h-full hover:shadow-xl transition-shadow duration-300 ease-out">
{review.book_image && ( {review.book_image && (
<div className="relative aspect-[3/4] rounded-2xl overflow-hidden mb-8 shadow-xl border-4 border-stone-50 dark:border-stone-800"> <div className="relative aspect-[3/4] rounded-2xl overflow-hidden mb-8 shadow-xl border-4 border-stone-50 dark:border-stone-800">
<Image src={review.book_image} alt={review.book_title} fill className="object-cover" sizes="(max-width: 768px) 100vw, 33vw" /> <Image src={review.book_image} alt={review.book_title} fill className="object-cover" sizes="(max-width: 768px) 100vw, 33vw" />
+3 -3
View File
@@ -69,7 +69,7 @@ export default function ProjectDetailClient({
onClick={handleBack} onClick={handleBack}
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-12 group bg-transparent border-none cursor-pointer" className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-12 group bg-transparent border-none cursor-pointer"
> >
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" /> <ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform duration-200 ease-out" />
<span className="font-bold uppercase tracking-widest text-xs"> <span className="font-bold uppercase tracking-widest text-xs">
{tCommon("back")} {tCommon("back")}
</span> </span>
@@ -127,11 +127,11 @@ export default function ProjectDetailClient({
{project.live && project.live !== "#" && ( {project.live && project.live !== "#" && (
<a href={project.live} target="_blank" rel="noopener noreferrer" className="flex items-center justify-between w-full p-5 bg-white text-stone-900 rounded-2xl font-black hover:scale-105 transition-transform group"> <a href={project.live} target="_blank" rel="noopener noreferrer" className="flex items-center justify-between w-full p-5 bg-white text-stone-900 rounded-2xl font-black active:scale-[0.97] transition-transform duration-150 ease-out group">
<span>{project.button_live_label || tDetail("liveDemo")}</span> <span>{project.button_live_label || tDetail("liveDemo")}</span>
<ExternalLink size={20} className="group-hover:translate-x-1 transition-transform" /> <ExternalLink size={20} className="group-hover:translate-x-1 transition-transform duration-200 ease-out" />
</a> </a>
+7 -7
View File
@@ -70,7 +70,7 @@ export default function ProjectsPageClient({
href={`/${locale}`} href={`/${locale}`}
className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group" className="inline-flex items-center gap-2 text-stone-500 hover:text-stone-900 dark:hover:text-white transition-colors mb-10 group"
> >
<ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform" /> <ArrowLeft size={20} className="group-hover:-translate-x-1 transition-transform duration-200 ease-out" />
<span className="font-bold uppercase tracking-widest text-xs">{tCommon("backToHome")}</span> <span className="font-bold uppercase tracking-widest text-xs">{tCommon("backToHome")}</span>
</Link> </Link>
@@ -89,7 +89,7 @@ export default function ProjectsPageClient({
<button <button
key={cat} key={cat}
onClick={() => setSelectedCategory(cat)} onClick={() => setSelectedCategory(cat)}
className={`px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-widest transition-all ${ className={`px-6 py-2 rounded-full text-[10px] font-black uppercase tracking-widest transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.97] ${
selectedCategory === cat selectedCategory === cat
? "bg-stone-900 dark:bg-stone-100 text-white dark:text-stone-900" ? "bg-stone-900 dark:bg-stone-100 text-white dark:text-stone-900"
: "bg-white dark:bg-stone-900 text-stone-500 border border-stone-200 dark:border-stone-800" : "bg-white dark:bg-stone-900 text-stone-500 border border-stone-200 dark:border-stone-800"
@@ -106,7 +106,7 @@ export default function ProjectsPageClient({
placeholder={tList("searchPlaceholder")} placeholder={tList("searchPlaceholder")}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 rounded-2xl py-4 pl-12 pr-6 focus:outline-none focus:ring-2 focus:ring-liquid-mint/30 transition-all shadow-sm" className="w-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 rounded-2xl py-4 pl-12 pr-6 focus:outline-none focus:ring-2 focus:ring-liquid-mint/30 transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-sm"
/> />
</div> </div>
</div> </div>
@@ -125,12 +125,12 @@ export default function ProjectsPageClient({
)) ))
) : ( ) : (
filteredProjects.map((project) => ( filteredProjects.map((project) => (
<motion.div key={project.id} initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }}> <motion.div key={project.id} initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, ease: [0.23, 1, 0.32, 1] }}>
<Link href={`/${locale}/projects/${project.slug}`} className="group block h-full"> <Link href={`/${locale}/projects/${project.slug}`} className="group block h-full">
<div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm h-full hover:shadow-xl transition-all flex flex-col"> <div className="bg-white dark:bg-stone-900 rounded-[3rem] p-10 border border-stone-200/60 dark:border-stone-800/60 shadow-sm h-full hover:shadow-xl transition-shadow duration-300 ease-out flex flex-col">
{project.imageUrl ? ( {project.imageUrl ? (
<div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg"> <div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg">
<Image src={project.imageUrl} alt={project.title} fill className="object-cover group-hover:scale-105 transition-transform duration-700" /> <Image src={project.imageUrl} alt={project.title} fill className="object-cover group-hover:scale-[1.03] transition-transform duration-500 ease-out" />
</div> </div>
) : ( ) : (
<div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg"> <div className="relative aspect-[16/10] rounded-[2rem] overflow-hidden mb-8 border-4 border-stone-50 dark:border-stone-800 shadow-lg">
@@ -146,7 +146,7 @@ export default function ProjectsPageClient({
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<div className="flex justify-between items-start mb-4"> <div className="flex justify-between items-start mb-4">
<h3 className="text-3xl font-black text-stone-900 dark:text-white uppercase tracking-tight">{project.title}</h3> <h3 className="text-3xl font-black text-stone-900 dark:text-white uppercase tracking-tight">{project.title}</h3>
<div className="w-12 h-12 rounded-full bg-stone-50 dark:bg-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 transition-all"> <div className="w-12 h-12 rounded-full bg-stone-50 dark:bg-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 transition-colors duration-200">
<ArrowUpRight size={20} /> <ArrowUpRight size={20} />
</div> </div>
</div> </div>
+2 -2
View File
@@ -167,7 +167,7 @@ const About = () => {
<h3 className="text-xl sm:text-2xl font-black text-stone-900 dark:text-stone-50 flex items-center gap-2 sm:gap-3 uppercase tracking-tighter"> <h3 className="text-xl sm:text-2xl font-black text-stone-900 dark:text-stone-50 flex items-center gap-2 sm:gap-3 uppercase tracking-tighter">
<BookOpen className="text-liquid-purple" size={24} /> {at("library")} <BookOpen className="text-liquid-purple" size={24} /> {at("library")}
</h3> </h3>
<Link href={`/${locale}/books`} className="group/link flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-all"> <Link href={`/${locale}/books`} className="group/link flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-opacity">
{at("viewAll")} <ArrowRight size={14} className="group-hover/link:translate-x-1 transition-transform" /> {at("viewAll")} <ArrowRight size={14} className="group-hover/link:translate-x-1 transition-transform" />
</Link> </Link>
</div> </div>
@@ -226,7 +226,7 @@ const About = () => {
return ( return (
<div key={hobby.id} className="p-3 sm:p-4 md:p-6 bg-stone-50 dark:bg-stone-800 rounded-xl sm:rounded-2xl border border-stone-100 dark:border-stone-700 hover:border-liquid-mint transition-colors group"> <div key={hobby.id} className="p-3 sm:p-4 md:p-6 bg-stone-50 dark:bg-stone-800 rounded-xl sm:rounded-2xl border border-stone-100 dark:border-stone-700 hover:border-liquid-mint transition-colors group">
<div className="flex items-center gap-2 sm:gap-3 mb-1.5 sm:mb-3"> <div className="flex items-center gap-2 sm:gap-3 mb-1.5 sm:mb-3">
<Icon size={16} className="text-liquid-mint group-hover:scale-110 transition-transform shrink-0 sm:w-5 sm:h-5" /> <Icon size={16} className="text-liquid-mint group-hover:scale-110 transition-transform duration-200 ease-out shrink-0 sm:w-5 sm:h-5" />
<h4 className="font-bold text-xs sm:text-sm text-stone-800 dark:text-stone-200 uppercase tracking-tight">{hobby.title}</h4> <h4 className="font-bold text-xs sm:text-sm text-stone-800 dark:text-stone-200 uppercase tracking-tight">{hobby.title}</h4>
</div> </div>
<p className="text-[11px] sm:text-xs text-stone-500 dark:text-stone-400 font-medium leading-relaxed hidden sm:block"> <p className="text-[11px] sm:text-xs text-stone-500 dark:text-stone-400 font-medium leading-relaxed hidden sm:block">
+6 -6
View File
@@ -130,8 +130,8 @@ export default function ActivityFeed({
key={quoteIndex} key={quoteIndex}
initial={{ opacity: 0, y: 10 }} initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }} exit={{ opacity: 0, y: -6, filter: "blur(2px)", transition: { duration: 0.2, ease: [0.23, 1, 0.32, 1] } }}
transition={{ duration: 0.5 }} transition={{ duration: 0.4, ease: [0.23, 1, 0.32, 1] }}
className="space-y-4" className="space-y-4"
> >
<p className="text-base sm:text-lg md:text-xl lg:text-2xl font-light leading-tight text-stone-400 dark:text-stone-400 italic"> <p className="text-base sm:text-lg md:text-xl lg:text-2xl font-light leading-tight text-stone-400 dark:text-stone-400 italic">
@@ -207,14 +207,14 @@ export default function ActivityFeed({
{[0, 1, 2].map((i) => ( {[0, 1, 2].map((i) => (
<motion.div <motion.div
key={i} key={i}
animate={{ height: ["20%", "100%", "20%"] }} animate={{ scaleY: [0.2, 1, 0.2] }}
transition={{ transition={{
duration: 0.8, duration: 0.8,
repeat: Infinity, repeat: Infinity,
delay: i * 0.2, delay: i * 0.2,
ease: "easeInOut" ease: "easeInOut"
}} }}
className="w-[2px] bg-[#1DB954] rounded-full" className="w-[2px] h-full origin-bottom bg-[#1DB954] rounded-full"
/> />
))} ))}
</div> </div>
@@ -225,12 +225,12 @@ export default function ActivityFeed({
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex gap-4 relative z-10" className="flex gap-4 relative z-10"
> >
<div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-500"> <div className="w-16 h-16 rounded-lg overflow-hidden shrink-0 shadow-md relative group-hover:shadow-xl transition-shadow duration-300">
<Image <Image
src={data.music.albumArt} src={data.music.albumArt}
alt="Album Art" alt="Album Art"
fill fill
className="object-cover transition-transform duration-700 group-hover:scale-110" className="object-cover transition-transform duration-500 ease-out group-hover:scale-105"
sizes="64px" sizes="64px"
/> />
</div> </div>
+2 -2
View File
@@ -103,9 +103,9 @@ export default function BentoChat() {
onChange={(e) => setInputValue(e.target.value)} onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()} onKeyDown={(e) => e.key === "Enter" && handleSend()}
placeholder="Ask me..." placeholder="Ask me..."
className="w-full bg-white dark:bg-stone-800 border border-stone-200 dark:border-stone-700 rounded-2xl py-3 pl-4 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-purple/30 transition-all shadow-inner dark:text-white" className="w-full bg-white dark:bg-stone-800 border border-stone-200 dark:border-stone-700 rounded-2xl py-3 pl-4 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-purple/30 transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-inner dark:text-white"
/> />
<button onClick={handleSend} aria-label="Send message" className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-liquid-purple hover:scale-110 transition-transform"> <button onClick={handleSend} aria-label="Send message" className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-liquid-purple active:scale-[0.97] transition-transform duration-150 ease-out">
<Send size={18} /> <Send size={18} />
</button> </button>
</div> </div>
+10 -10
View File
@@ -287,23 +287,23 @@ export default function ChatWidget() {
<motion.div <motion.div
role="button" role="button"
tabIndex={0} tabIndex={0}
initial={{ scale: 0, opacity: 0 }} initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }} animate={{ scale: 1, opacity: 1, transition: { duration: 0.25, ease: [0.23, 1, 0.32, 1] } }}
exit={{ scale: 0, opacity: 0 }} exit={{ scale: 0.9, opacity: 0, transition: { duration: 0.15, ease: [0.23, 1, 0.32, 1] } }}
onClick={() => setIsOpen(true)} onClick={() => setIsOpen(true)}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") { if (e.key === "Enter" || e.key === " ") {
setIsOpen(true); setIsOpen(true);
} }
}} }}
className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-white/80 backdrop-blur-xl text-stone-900 p-3.5 rounded-full shadow-[0_10px_26px_rgba(41,37,36,0.16)] hover:bg-white hover:scale-105 transition-all duration-300 group cursor-pointer border border-white/60 ring-1 ring-white/30" className="fixed bottom-4 left-4 md:bottom-6 md:left-6 z-30 bg-white/80 backdrop-blur-xl text-stone-900 p-3.5 rounded-full shadow-[0_10px_26px_rgba(41,37,36,0.16)] hover:bg-white active:scale-[0.97] transition-[background-color,transform] duration-150 ease-out group cursor-pointer border border-white/60 ring-1 ring-white/30"
aria-label="Open chat" aria-label="Open chat"
> >
<MessageCircle size={24} /> <MessageCircle size={24} />
<span className="absolute top-0 right-0 w-3 h-3 bg-green-500 rounded-full animate-pulse shadow-sm border-2 border-white" /> <span className="absolute top-0 right-0 w-3 h-3 bg-green-500 rounded-full animate-pulse shadow-sm border-2 border-white" />
{/* Tooltip */} {/* Tooltip */}
<span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-3 py-1.5 bg-stone-900/90 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap pointer-events-none z-[100] shadow-xl backdrop-blur-sm"> <span className="absolute bottom-full left-1/2 -translate-x-1/2 mb-3 px-3 py-1.5 bg-stone-900/90 text-stone-50 text-xs font-medium rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-150 ease-out group-hover:delay-300 whitespace-nowrap pointer-events-none z-[100] shadow-xl backdrop-blur-sm">
Chat with AI Chat with AI
</span> </span>
</motion.div> </motion.div>
@@ -318,8 +318,8 @@ export default function ChatWidget() {
initial={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }} initial={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }} animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }} exit={{ opacity: 0, y: 20, scale: 0.95, filter: "blur(10px)" }}
transition={{ type: "spring", damping: 30, stiffness: 400 }} transition={{ type: "spring", duration: 0.35, bounce: 0 }}
className="fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-white/80 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.16)] flex flex-col overflow-hidden border border-white/60 ring-1 ring-white/30" className="origin-bottom-left fixed bottom-20 left-4 right-4 md:bottom-24 md:left-6 md:right-auto z-30 md:w-[380px] h-[60vh] md:h-[550px] max-h-[600px] bg-white/80 backdrop-blur-xl saturate-100 rounded-2xl shadow-[0_12px_40px_rgba(41,37,36,0.16)] flex flex-col overflow-hidden border border-white/60 ring-1 ring-white/30"
> >
{/* Header */} {/* Header */}
<div className="bg-white/70 text-stone-900 p-4 flex items-center justify-between border-b border-white/50"> <div className="bg-white/70 text-stone-900 p-4 flex items-center justify-between border-b border-white/50">
@@ -450,12 +450,12 @@ export default function ChatWidget() {
onKeyPress={handleKeyPress} onKeyPress={handleKeyPress}
placeholder="Ask anything..." placeholder="Ask anything..."
disabled={isLoading} disabled={isLoading}
className="flex-1 px-4 py-3 text-sm bg-[#f5f5f4] text-[#292524] rounded-xl border border-[#e7e5e4] focus:outline-none focus:ring-2 focus:ring-[#e7e5e4] focus:border-[#a8a29e] focus:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#78716c] transition-all shadow-inner" className="flex-1 px-4 py-3 text-sm bg-[#f5f5f4] text-[#292524] rounded-xl border border-[#e7e5e4] focus:outline-none focus:ring-2 focus:ring-[#e7e5e4] focus:border-[#a8a29e] focus:bg-stone-50 disabled:opacity-50 disabled:cursor-not-allowed placeholder:text-[#78716c] transition-[border-color,box-shadow,background-color] duration-150 ease-out shadow-inner"
/> />
<button <button
onClick={handleSend} onClick={handleSend}
disabled={!inputValue.trim() || isLoading} disabled={!inputValue.trim() || isLoading}
className="p-3 bg-[#292524] text-[#fdfcf8] rounded-xl hover:bg-[#44403c] hover:shadow-lg hover:scale-105 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 shadow-md flex items-center justify-center aspect-square" className="p-3 bg-[#292524] text-[#fdfcf8] rounded-xl hover:bg-[#44403c] hover:shadow-lg active:scale-[0.97] transition-[background-color,box-shadow,transform] duration-150 ease-out disabled:opacity-50 disabled:cursor-not-allowed disabled:active:scale-100 shadow-md flex items-center justify-center aspect-square"
aria-label="Send message" aria-label="Send message"
> >
{isLoading ? ( {isLoading ? (
@@ -480,7 +480,7 @@ export default function ChatWidget() {
inputRef.current?.focus(); inputRef.current?.focus();
}} }}
disabled={isLoading} disabled={isLoading}
className="px-3 py-1.5 text-xs font-medium bg-[#f5f5f4] text-[#57534e] rounded-lg hover:bg-[#e7e5e4] hover:text-[#292524] border border-[#e7e5e4] transition-all whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm" className="px-3 py-1.5 text-xs font-medium bg-[#f5f5f4] text-[#57534e] rounded-lg hover:bg-[#e7e5e4] hover:text-[#292524] border border-[#e7e5e4] transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.97] whitespace-nowrap disabled:opacity-50 flex-shrink-0 shadow-sm"
> >
{suggestion} {suggestion}
</button> </button>
+4
View File
@@ -3,6 +3,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { MotionConfig } from "framer-motion";
import { ToastProvider } from "@/components/Toast"; import { ToastProvider } from "@/components/Toast";
import ErrorBoundary from "@/components/ErrorBoundary"; import ErrorBoundary from "@/components/ErrorBoundary";
import { ConsentProvider } from "./ConsentProvider"; import { ConsentProvider } from "./ConsentProvider";
@@ -30,9 +31,12 @@ export default function ClientProviders({
<ErrorBoundary> <ErrorBoundary>
<ConsentProvider> <ConsentProvider>
<ThemeProvider> <ThemeProvider>
{/* Respect prefers-reduced-motion in every Framer Motion animation */}
<MotionConfig reducedMotion="user">
<GatedProviders mounted={mounted}> <GatedProviders mounted={mounted}>
{children} {children}
</GatedProviders> </GatedProviders>
</MotionConfig>
</ThemeProvider> </ThemeProvider>
</ConsentProvider> </ConsentProvider>
</ErrorBoundary> </ErrorBoundary>
+10 -10
View File
@@ -200,7 +200,7 @@ const Contact = () => {
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Email</span> <span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Email</span>
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-liquid-mint transition-colors">contact@dk0.dev</span> <span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-liquid-mint transition-colors">contact@dk0.dev</span>
</div> </div>
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-all"> <div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-colors duration-200">
<Mail size={16} /> <Mail size={16} />
</div> </div>
</a> </a>
@@ -213,7 +213,7 @@ const Contact = () => {
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Code</span> <span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Code</span>
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-liquid-mint transition-colors">GitHub</span> <span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-liquid-mint transition-colors">GitHub</span>
</div> </div>
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-all"> <div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-colors duration-200">
<Github size={16} /> <Github size={16} />
</div> </div>
</a> </a>
@@ -226,7 +226,7 @@ const Contact = () => {
<span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Professional</span> <span className="text-[9px] font-black uppercase tracking-[0.2em] text-stone-600 dark:text-stone-400 mb-1">Professional</span>
<span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-[#0077b5] transition-colors">LinkedIn</span> <span className="text-lg sm:text-xl md:text-2xl font-black text-stone-900 dark:text-stone-50 tracking-tighter uppercase group-hover/link:text-[#0077b5] transition-colors">LinkedIn</span>
</div> </div>
<div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-all"> <div className="w-10 h-10 rounded-full border border-stone-100 dark:border-stone-800 flex items-center justify-center group-hover/link:bg-stone-900 dark:group-hover/link:bg-stone-50 group-hover/link:text-white dark:group-hover/link:text-stone-900 transition-colors duration-200">
<Linkedin size={16} /> <Linkedin size={16} />
</div> </div>
</a> </a>
@@ -266,7 +266,7 @@ const Contact = () => {
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
required required
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium" className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium"
placeholder={tForm("placeholders.name")} placeholder={tForm("placeholders.name")}
/> />
</div> </div>
@@ -283,7 +283,7 @@ const Contact = () => {
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
required required
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium" className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium"
placeholder={tForm("placeholders.email")} placeholder={tForm("placeholders.email")}
/> />
</div> </div>
@@ -301,7 +301,7 @@ const Contact = () => {
onChange={handleChange} onChange={handleChange}
onBlur={handleBlur} onBlur={handleBlur}
required required
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium" className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium"
placeholder={tForm("placeholders.subject")} placeholder={tForm("placeholders.subject")}
/> />
</div> </div>
@@ -318,7 +318,7 @@ const Contact = () => {
onBlur={handleBlur} onBlur={handleBlur}
required required
rows={5} rows={5}
className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-all font-medium resize-none" className="w-full px-4 sm:px-6 py-3 sm:py-4 bg-stone-50 dark:bg-stone-800 border border-stone-100 dark:border-stone-700 rounded-xl sm:rounded-2xl text-sm sm:text-base text-stone-900 dark:text-stone-50 focus:outline-none focus:ring-2 focus:ring-liquid-mint/50 transition-[border-color,box-shadow,background-color] duration-150 ease-out font-medium resize-none"
placeholder={tForm("placeholders.message")} placeholder={tForm("placeholders.message")}
/> />
</div> </div>
@@ -326,9 +326,9 @@ const Contact = () => {
<motion.button <motion.button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.97 }}
whileTap={{ scale: 0.99 }} transition={{ duration: 0.15, ease: [0.23, 1, 0.32, 1] }}
className="w-full py-4 sm:py-5 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-xl sm:rounded-2xl font-black text-xs uppercase tracking-[0.2em] sm:tracking-[0.3em] flex items-center justify-center gap-3 shadow-xl hover:shadow-2xl transition-all disabled:opacity-50" className="w-full py-4 sm:py-5 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-xl sm:rounded-2xl font-black text-xs uppercase tracking-[0.2em] sm:tracking-[0.3em] flex items-center justify-center gap-3 shadow-xl hover:shadow-2xl transition-shadow duration-200 ease-out disabled:opacity-50"
> >
{isSubmitting ? ( {isSubmitting ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" /> <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
+9 -12
View File
@@ -98,12 +98,8 @@ const CurrentlyReading = () => {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, delay: index * 0.1, ease: [0.25, 0.1, 0.25, 1] }} transition={{ duration: 0.5, delay: index * 0.06, ease: [0.23, 1, 0.32, 1] }}
whileHover={{ className="relative overflow-hidden bg-gradient-to-br from-liquid-lavender/15 via-liquid-pink/10 to-liquid-rose/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-lavender/30 dark:border-stone-700 rounded-xl p-6 backdrop-blur-sm hover:border-liquid-lavender/50 dark:hover:border-stone-600 hover:from-liquid-lavender/20 hover:via-liquid-pink/15 hover:to-liquid-rose/20 transition-colors duration-300"
scale: 1.02,
transition: { duration: 0.4, ease: "easeOut" },
}}
className="relative overflow-hidden bg-gradient-to-br from-liquid-lavender/15 via-liquid-pink/10 to-liquid-rose/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-lavender/30 dark:border-stone-700 rounded-xl p-6 backdrop-blur-sm hover:border-liquid-lavender/50 dark:hover:border-stone-600 hover:from-liquid-lavender/20 hover:via-liquid-pink/15 hover:to-liquid-rose/20 transition-all duration-500 ease-out"
> >
{/* Background Blob Animation */} {/* Background Blob Animation */}
<motion.div <motion.div
@@ -124,9 +120,9 @@ const CurrentlyReading = () => {
{/* Book Cover */} {/* Book Cover */}
{book.image && ( {book.image && (
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }} transition={{ duration: 0.4, delay: 0.1 + index * 0.06, ease: [0.23, 1, 0.32, 1] }}
className="flex-shrink-0" className="flex-shrink-0"
> >
<div className="relative w-24 h-36 sm:w-28 sm:h-40 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600"> <div className="relative w-24 h-36 sm:w-28 sm:h-40 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600">
@@ -162,11 +158,12 @@ const CurrentlyReading = () => {
<span className="font-semibold">{isNaN(book.progress) ? 0 : book.progress}%</span> <span className="font-semibold">{isNaN(book.progress) ? 0 : book.progress}%</span>
</div> </div>
<div className="relative h-2 bg-white/50 dark:bg-stone-700 rounded-full overflow-hidden border border-white/70 dark:border-stone-600"> <div className="relative h-2 bg-white/50 dark:bg-stone-700 rounded-full overflow-hidden border border-white/70 dark:border-stone-600">
{/* scaleX instead of width: stays on the compositor, no layout per frame */}
<motion.div <motion.div
initial={{ width: 0 }} initial={{ scaleX: 0 }}
animate={{ width: `${isNaN(book.progress) ? 0 : book.progress}%` }} animate={{ scaleX: (isNaN(book.progress) ? 0 : book.progress) / 100 }}
transition={{ duration: 1, delay: 0.3 + index * 0.1, ease: "easeOut" }} transition={{ duration: 0.8, delay: 0.2 + index * 0.06, ease: [0.23, 1, 0.32, 1] }}
className="absolute left-0 top-0 h-full bg-gradient-to-r from-liquid-lavender via-liquid-pink to-liquid-rose rounded-full shadow-sm" className="origin-left w-full absolute left-0 top-0 h-full bg-gradient-to-r from-liquid-lavender via-liquid-pink to-liquid-rose rounded-full shadow-sm"
/> />
</div> </div>
</div> </div>
+1 -1
View File
@@ -55,7 +55,7 @@ const Footer = () => {
className="group flex flex-col items-center gap-4 text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-white transition-colors" className="group flex flex-col items-center gap-4 text-stone-600 dark:text-stone-400 hover:text-stone-900 dark:hover:text-white transition-colors"
> >
<span className="text-[10px] font-black uppercase tracking-[0.3em] text-stone-600 dark:text-stone-400" style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>{t("backToTop")}</span> <span className="text-[10px] font-black uppercase tracking-[0.3em] text-stone-600 dark:text-stone-400" style={{ writingMode: 'vertical-rl', transform: 'rotate(180deg)' }}>{t("backToTop")}</span>
<div className="w-12 h-12 rounded-full border border-stone-200 dark:border-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 transition-all shadow-sm"> <div className="w-12 h-12 rounded-full border border-stone-200 dark:border-stone-800 flex items-center justify-center group-hover:bg-stone-900 dark:group-hover:bg-stone-50 group-hover:text-white dark:group-hover:text-stone-900 group-active:scale-[0.97] transition-[background-color,color,transform] duration-200 ease-out shadow-sm">
<ArrowUp size={20} /> <ArrowUp size={20} />
</div> </div>
</button> </button>
+6 -6
View File
@@ -29,7 +29,7 @@ const Header = () => {
{/* Logo Pill */} {/* Logo Pill */}
<Link <Link
href={`/${locale}`} href={`/${locale}`}
className="w-10 h-10 flex items-center justify-center rounded-full bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 transition-transform hover:scale-105 active:scale-95 shadow-lg" className="w-10 h-10 flex items-center justify-center rounded-full bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 transition-transform duration-150 ease-out active:scale-[0.97] shadow-lg"
> >
<span className="font-black text-xs tracking-tighter">dk0</span> <span className="font-black text-xs tracking-tighter">dk0</span>
</Link> </Link>
@@ -40,7 +40,7 @@ const Header = () => {
<Link <Link
key={item.name} key={item.name}
href={item.href} href={item.href}
className="px-5 py-2 text-[10px] font-black uppercase tracking-[0.2em] text-stone-500 hover:text-stone-900 dark:hover:text-stone-100 rounded-full transition-all" className="px-5 py-2 text-[10px] font-black uppercase tracking-[0.2em] text-stone-500 hover:text-stone-900 dark:hover:text-stone-100 rounded-full transition-colors"
> >
{item.name} {item.name}
</Link> </Link>
@@ -62,7 +62,7 @@ const Header = () => {
{/* Mobile Menu Toggle */} {/* Mobile Menu Toggle */}
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="w-8 h-8 flex md:hidden items-center justify-center text-stone-600 dark:text-stone-400 hover:bg-white dark:hover:bg-stone-800 rounded-full transition-colors shadow-sm" className="w-8 h-8 flex md:hidden items-center justify-center text-stone-600 dark:text-stone-400 hover:bg-white dark:hover:bg-stone-800 rounded-full transition-[background-color,transform] duration-150 ease-out active:scale-[0.97] shadow-sm"
> >
{isOpen ? <X size={14} /> : <Menu size={14} />} {isOpen ? <X size={14} /> : <Menu size={14} />}
</button> </button>
@@ -72,10 +72,10 @@ const Header = () => {
{/* Mobile Menu Overlay */} {/* Mobile Menu Overlay */}
<div <div
className={`fixed top-24 left-6 right-6 z-40 bg-white/90 dark:bg-stone-900/95 backdrop-blur-3xl border border-white/40 dark:border-white/10 rounded-[2.5rem] shadow-2xl p-6 md:hidden overflow-hidden transition-all duration-200 ${ className={`fixed top-24 left-6 right-6 z-40 bg-white/90 dark:bg-stone-900/95 backdrop-blur-3xl border border-white/40 dark:border-white/10 rounded-[2.5rem] shadow-2xl p-6 md:hidden overflow-hidden origin-top transition-[opacity,transform] duration-200 ease-out ${
isOpen isOpen
? "opacity-100 translate-y-0 pointer-events-auto" ? "opacity-100 translate-y-0 scale-100 pointer-events-auto"
: "opacity-0 -translate-y-2 pointer-events-none" : "opacity-0 -translate-y-2 scale-[0.97] pointer-events-none"
}`} }`}
> >
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
+34 -21
View File
@@ -50,10 +50,19 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
setScrolled(window.scrollY > 50); setScrolled(window.scrollY > 50);
}; };
window.addEventListener("scroll", handleScroll); window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll); return () => window.removeEventListener("scroll", handleScroll);
}, []); }, []);
useEffect(() => {
if (!isOpen) return;
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") setIsOpen(false);
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [isOpen]);
const navItems = [ const navItems = [
{ name: translations.home, href: `/${locale}` }, { name: translations.home, href: `/${locale}` },
{ name: translations.about, href: isHome ? "#about" : `/${locale}#about` }, { name: translations.about, href: isHome ? "#about" : `/${locale}#about` },
@@ -81,18 +90,18 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
<> <>
<header className="fixed top-6 left-0 right-0 z-50 flex justify-center px-4 pointer-events-none"> <header className="fixed top-6 left-0 right-0 z-50 flex justify-center px-4 pointer-events-none">
<div <div
className={`pointer-events-auto transition-all duration-500 ease-out ${ className={`pointer-events-auto transition-[max-width] duration-500 ease-in-out ${
scrolled ? "w-full max-w-5xl" : "w-full max-w-7xl" scrolled ? "w-full max-w-5xl" : "w-full max-w-7xl"
}`} }`}
> >
<div <div
className={`backdrop-blur-xl transition-all duration-500 flex justify-between items-center ${ className={`backdrop-blur-xl transition-[padding,background-color,box-shadow,border-color] duration-500 ease-in-out flex justify-between items-center ${
scrolled scrolled
? "bg-white/95 border border-stone-200/50 shadow-[0_8px_30px_rgba(0,0,0,0.12)] rounded-full px-6 py-3" ? "bg-white/95 border border-stone-200/50 shadow-[0_8px_30px_rgba(0,0,0,0.12)] rounded-full px-6 py-3"
: "bg-white/85 border border-stone-200/30 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full" : "bg-white/85 border border-stone-200/30 shadow-[0_4px_24px_rgba(0,0,0,0.08)] px-4 py-4 rounded-full"
}`} }`}
> >
<div className="flex items-center space-x-2 hover:scale-105 transition-transform"> <div className="flex items-center space-x-2 active:scale-[0.97] transition-transform duration-150 ease-out">
<Link <Link
href={`/${locale}`} href={`/${locale}`}
className="text-2xl font-black font-sans text-stone-900 tracking-tighter liquid-hover flex items-center" className="text-2xl font-black font-sans text-stone-900 tracking-tighter liquid-hover flex items-center"
@@ -103,7 +112,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
<nav className="hidden md:flex items-center space-x-8"> <nav className="hidden md:flex items-center space-x-8">
{navItems.map((item) => ( {navItems.map((item) => (
<div key={item.name} className="hover:-translate-y-0.5 active:scale-95 transition-all"> <div key={item.name} className="active:scale-[0.97] transition-transform duration-150 ease-out">
<Link <Link
href={item.href} href={item.href}
className="text-stone-700 hover:text-stone-900 font-medium transition-colors relative liquid-hover" className="text-stone-700 hover:text-stone-900 font-medium transition-colors relative liquid-hover"
@@ -117,7 +126,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
<div className="flex items-center space-x-2 ml-4 pl-4 border-l border-stone-300"> <div className="flex items-center space-x-2 ml-4 pl-4 border-l border-stone-300">
<Link <Link
href={enHref} href={enHref}
className={`px-2 py-1 text-sm font-medium rounded transition-all ${ className={`px-2 py-1 text-sm font-medium rounded transition-colors ${
locale === "en" locale === "en"
? "bg-stone-900 text-white" ? "bg-stone-900 text-white"
: "text-stone-600 hover:text-stone-900 hover:bg-stone-100" : "text-stone-600 hover:text-stone-900 hover:bg-stone-100"
@@ -127,7 +136,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
</Link> </Link>
<Link <Link
href={deHref} href={deHref}
className={`px-2 py-1 text-sm font-medium rounded transition-all ${ className={`px-2 py-1 text-sm font-medium rounded transition-colors ${
locale === "de" locale === "de"
? "bg-stone-900 text-white" ? "bg-stone-900 text-white"
: "text-stone-600 hover:text-stone-900 hover:bg-stone-100" : "text-stone-600 hover:text-stone-900 hover:bg-stone-100"
@@ -141,7 +150,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
className="md:hidden p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-all hover:scale-105 active:scale-95" className="md:hidden p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-[background-color,transform] duration-150 ease-out active:scale-[0.97]"
aria-label="Toggle menu" aria-label="Toggle menu"
> >
{isOpen ? <XIcon size={24} /> : <MenuIcon size={24} />} {isOpen ? <XIcon size={24} /> : <MenuIcon size={24} />}
@@ -150,18 +159,23 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
</div> </div>
</header> </header>
{/* Mobile menu overlay */} {/* Mobile menu overlay — always mounted so it can fade out, not just vanish */}
{isOpen && (
<div <div
className="fixed inset-0 bg-stone-900/50 backdrop-blur-sm z-40 md:hidden" aria-hidden="true"
className={`fixed inset-0 bg-stone-900/50 backdrop-blur-sm z-40 md:hidden transition-opacity ${
isOpen ? "opacity-100 duration-300 ease-out" : "opacity-0 pointer-events-none duration-200 ease-out"
}`}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
/> />
)}
{/* Mobile menu panel */} {/* Mobile menu panel — drawer curve in, quicker exit */}
{isOpen && (
<div <div
className="fixed right-0 top-0 h-full w-80 bg-white shadow-2xl z-50 md:hidden overflow-y-auto" inert={!isOpen}
className={`fixed right-0 top-0 h-full w-80 max-w-[85vw] bg-white shadow-2xl z-50 md:hidden overflow-y-auto overscroll-contain transition-[transform,visibility] ${
isOpen
? "translate-x-0 visible duration-500 ease-drawer"
: "translate-x-full invisible duration-200 ease-out"
}`}
> >
<div className="p-6"> <div className="p-6">
<div className="flex justify-between items-center mb-8"> <div className="flex justify-between items-center mb-8">
@@ -174,7 +188,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
</Link> </Link>
<button <button
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className="p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700" className="p-2 rounded-lg bg-stone-100 hover:bg-stone-200 text-stone-700 transition-[background-color,transform] duration-150 ease-out active:scale-[0.97]"
aria-label="Close menu" aria-label="Close menu"
> >
<XIcon size={24} /> <XIcon size={24} />
@@ -187,7 +201,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
key={item.name} key={item.name}
href={item.href} href={item.href}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className="block px-4 py-3 text-stone-700 hover:bg-stone-50 rounded-lg font-medium transition-colors" className="block px-4 py-3 text-stone-700 hover:bg-stone-50 active:bg-stone-100 rounded-lg font-medium transition-colors"
> >
{item.name} {item.name}
</Link> </Link>
@@ -199,7 +213,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
<Link <Link
href={enHref} href={enHref}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-all ${ className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-colors ${
locale === "en" locale === "en"
? "bg-stone-900 text-white" ? "bg-stone-900 text-white"
: "bg-stone-100 text-stone-600 hover:bg-stone-200" : "bg-stone-100 text-stone-600 hover:bg-stone-200"
@@ -210,7 +224,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
<Link <Link
href={deHref} href={deHref}
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-all ${ className={`flex-1 px-4 py-2 text-center font-medium rounded-lg transition-colors ${
locale === "de" locale === "de"
? "bg-stone-900 text-white" ? "bg-stone-900 text-white"
: "bg-stone-100 text-stone-600 hover:bg-stone-200" : "bg-stone-100 text-stone-600 hover:bg-stone-200"
@@ -231,7 +245,7 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
href={link.href} href={link.href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="p-3 rounded-lg bg-stone-100 hover:bg-stone-900 hover:text-white text-stone-700 transition-all" className="p-3 rounded-lg bg-stone-100 hover:bg-stone-900 hover:text-white text-stone-700 transition-[background-color,color,transform] duration-150 ease-out active:scale-[0.97]"
aria-label={link.label} aria-label={link.label}
> >
<Icon size={20} /> <Icon size={20} />
@@ -242,7 +256,6 @@ export default function HeaderClient({ locale, translations }: HeaderClientProps
</div> </div>
</div> </div>
</div> </div>
)}
</> </>
); );
} }
+7 -7
View File
@@ -21,7 +21,7 @@ export default async function Hero({ locale }: HeroProps) {
{/* Left: Text Content */} {/* Left: Text Content */}
<div className="flex-1 text-center xl:text-left space-y-6 sm:space-y-8 md:space-y-10"> <div className="flex-1 text-center xl:text-left space-y-6 sm:space-y-8 md:space-y-10">
<div className="inline-flex items-center gap-2 sm:gap-3 px-4 sm:px-5 py-2 sm:py-2.5 rounded-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 shadow-sm animate-[fadeIn_0.5s_ease-out]"> <div className="inline-flex items-center gap-2 sm:gap-3 px-4 sm:px-5 py-2 sm:py-2.5 rounded-full bg-white dark:bg-stone-900 border border-stone-200 dark:border-stone-800 shadow-sm animate-fade-in">
<span className="w-2 h-2 sm:w-2.5 sm:h-2.5 bg-emerald-500 rounded-full animate-pulse" /> <span className="w-2 h-2 sm:w-2.5 sm:h-2.5 bg-emerald-500 rounded-full animate-pulse" />
<span className="font-mono text-[10px] sm:text-[11px] font-black uppercase tracking-[0.2em] sm:tracking-[0.3em] text-stone-500">{t("badge")}</span> <span className="font-mono text-[10px] sm:text-[11px] font-black uppercase tracking-[0.2em] sm:tracking-[0.3em] text-stone-500">{t("badge")}</span>
</div> </div>
@@ -39,9 +39,9 @@ export default async function Hero({ locale }: HeroProps) {
{t("description")} {t("description")}
</p> </p>
<div className="flex flex-col sm:flex-row items-center gap-4 sm:gap-8 justify-center xl:justify-start pt-2 sm:pt-4 animate-[fadeIn_0.6s_ease-out_0.3s_both]"> <div className="flex flex-col sm:flex-row items-center gap-4 sm:gap-8 justify-center xl:justify-start pt-2 sm:pt-4 animate-fade-in [animation-delay:150ms]">
<a href="#projects" className="group relative px-8 sm:px-12 py-4 sm:py-5 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-2xl sm:rounded-3xl font-black text-xs uppercase tracking-[0.2em] hover:scale-105 active:scale-95 transition-all shadow-2xl"> <a href="#projects" className="group relative overflow-hidden px-8 sm:px-12 py-4 sm:py-5 bg-stone-900 dark:bg-stone-50 text-white dark:text-stone-900 rounded-2xl sm:rounded-3xl font-black text-xs uppercase tracking-[0.2em] active:scale-[0.97] transition-transform duration-150 ease-out shadow-2xl">
<div className="absolute inset-0 bg-white/10 -translate-x-full group-hover:translate-x-full transition-transform duration-1000" /> <div className="absolute inset-0 bg-white/10 -translate-x-full group-hover:translate-x-full transition-transform duration-700 ease-in-out" />
{t("ctaWork")} {t("ctaWork")}
</a> </a>
<a href="#contact" className="font-black text-xs uppercase tracking-[0.2em] text-stone-700 dark:text-stone-300 hover:text-stone-900 dark:hover:text-white transition-colors"> <a href="#contact" className="font-black text-xs uppercase tracking-[0.2em] text-stone-700 dark:text-stone-300 hover:text-stone-900 dark:hover:text-white transition-colors">
@@ -51,7 +51,7 @@ export default async function Hero({ locale }: HeroProps) {
</div> </div>
{/* Right: The Photo */} {/* Right: The Photo */}
<div className="relative w-52 h-52 sm:w-64 sm:h-64 md:w-80 md:h-80 xl:w-[500px] xl:h-[500px] shrink-0 mt-4 sm:mt-8 xl:mt-0 animate-[fadeIn_1s_ease-out]"> <div className="relative w-52 h-52 sm:w-64 sm:h-64 md:w-80 md:h-80 xl:w-[500px] xl:h-[500px] shrink-0 mt-4 sm:mt-8 xl:mt-0 animate-fade-in [animation-delay:100ms] [animation-duration:900ms]">
<div className="absolute inset-0 bg-gradient-to-tr from-liquid-mint to-liquid-purple rounded-[3rem] sm:rounded-[4rem] lg:rounded-[5rem] rotate-12 scale-90 opacity-20 blur-3xl" /> <div className="absolute inset-0 bg-gradient-to-tr from-liquid-mint to-liquid-purple rounded-[3rem] sm:rounded-[4rem] lg:rounded-[5rem] rotate-12 scale-90 opacity-20 blur-3xl" />
<div className="relative w-full h-full rounded-[2.5rem] sm:rounded-[3rem] lg:rounded-[4rem] overflow-hidden border-[12px] sm:border-[16px] lg:border-[24px] border-white dark:border-stone-900 shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] sm:shadow-[0_50px_100px_-20px_rgba(0,0,0,0.4)]" style={{ willChange: "transform" }}> <div className="relative w-full h-full rounded-[2.5rem] sm:rounded-[3rem] lg:rounded-[4rem] overflow-hidden border-[12px] sm:border-[16px] lg:border-[24px] border-white dark:border-stone-900 shadow-[0_30px_60px_-15px_rgba(0,0,0,0.3)] sm:shadow-[0_50px_100px_-20px_rgba(0,0,0,0.4)]" style={{ willChange: "transform" }}>
<Image src="/images/me.jpg" alt="Dennis Konkol" fill className="object-cover" priority fetchPriority="high" sizes="(max-width: 640px) 208px, (max-width: 768px) 256px, (max-width: 1280px) 320px, 500px" /> <Image src="/images/me.jpg" alt="Dennis Konkol" fill className="object-cover" priority fetchPriority="high" sizes="(max-width: 640px) 208px, (max-width: 768px) 256px, (max-width: 1280px) 320px, 500px" />
@@ -65,8 +65,8 @@ export default async function Hero({ locale }: HeroProps) {
</div> </div>
</div> </div>
<div className="absolute bottom-10 left-1/2 -translate-x-1/2 hidden md:flex flex-col items-center gap-4 animate-bounce"> <div className="absolute bottom-10 left-1/2 -translate-x-1/2 hidden md:flex flex-col items-center gap-4" aria-hidden="true">
<div className="w-px h-16 bg-gradient-to-b from-stone-300 dark:from-stone-700 to-transparent" /> <div className="w-px h-16 origin-top animate-scroll-hint bg-gradient-to-b from-stone-300 dark:from-stone-700 to-transparent" />
</div> </div>
</section> </section>
); );
+4 -4
View File
@@ -59,8 +59,8 @@ const Projects = () => {
{t("subtitle")} {t("subtitle")}
</p> </p>
</div> </div>
<Link href={`/${locale}/projects`} className="group flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-all text-xs uppercase tracking-widest"> <Link href={`/${locale}/projects`} className="group flex items-center gap-2 text-stone-900 dark:text-stone-100 font-black border-b-2 border-stone-900 dark:border-stone-100 pb-1 hover:opacity-70 transition-opacity text-xs uppercase tracking-widest">
{t("viewAll")} <ArrowUpRight className="group-hover:-translate-y-1 group-hover:translate-x-1 transition-transform" size={14} /> {t("viewAll")} <ArrowUpRight className="group-hover:-translate-y-0.5 group-hover:translate-x-0.5 transition-transform duration-200 ease-out" size={14} />
</Link> </Link>
</div> </div>
@@ -94,7 +94,7 @@ const Projects = () => {
src={project.imageUrl} src={project.imageUrl}
alt={project.title} alt={project.title}
fill fill
className="object-cover transition-transform duration-700 group-hover:scale-105" className="object-cover transition-transform duration-500 ease-out group-hover:scale-[1.03]"
/> />
) : ( ) : (
<ProjectThumbnail <ProjectThumbnail
@@ -106,7 +106,7 @@ const Projects = () => {
/> />
)} )}
{/* Overlay on Hover */} {/* Overlay on Hover */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-500" /> <div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-300" />
</div> </div>
{/* Text Content */} {/* Text Content */}
+6 -10
View File
@@ -136,15 +136,11 @@ const ReadBooks = () => {
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }} viewport={{ once: true, margin: "-50px" }}
transition={{ transition={{
duration: 0.6, duration: 0.5,
delay: index * 0.1, delay: index * 0.06,
ease: [0.25, 0.1, 0.25, 1], ease: [0.23, 1, 0.32, 1],
}} }}
whileHover={{ className="relative overflow-hidden bg-gradient-to-br from-liquid-mint/15 via-liquid-sky/10 to-liquid-teal/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-mint/30 dark:border-stone-700 rounded-xl p-5 backdrop-blur-sm hover:border-liquid-mint/50 dark:hover:border-stone-600 hover:from-liquid-mint/20 hover:via-liquid-sky/15 hover:to-liquid-teal/20 transition-colors duration-300"
scale: 1.02,
transition: { duration: 0.4, ease: "easeOut" },
}}
className="relative overflow-hidden bg-gradient-to-br from-liquid-mint/15 via-liquid-sky/10 to-liquid-teal/15 dark:from-stone-800 dark:via-stone-800 dark:to-stone-700 border-2 border-liquid-mint/30 dark:border-stone-700 rounded-xl p-5 backdrop-blur-sm hover:border-liquid-mint/50 dark:hover:border-stone-600 hover:from-liquid-mint/20 hover:via-liquid-sky/15 hover:to-liquid-teal/20 transition-all duration-500 ease-out"
> >
{/* Background Blob */} {/* Background Blob */}
<motion.div <motion.div
@@ -165,9 +161,9 @@ const ReadBooks = () => {
{/* Book Cover */} {/* Book Cover */}
{review.book_image && ( {review.book_image && (
<motion.div <motion.div
initial={{ opacity: 0, scale: 0.9 }} initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 + index * 0.1 }} transition={{ duration: 0.4, delay: 0.1 + index * 0.06, ease: [0.23, 1, 0.32, 1] }}
className="flex-shrink-0" className="flex-shrink-0"
> >
<div className="relative w-20 h-[7.5rem] sm:w-24 sm:h-32 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600"> <div className="relative w-20 h-[7.5rem] sm:w-24 sm:h-32 rounded-lg overflow-hidden shadow-lg border-2 border-white/50 dark:border-stone-600">
+6 -3
View File
@@ -18,9 +18,11 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [hasMounted, setHasMounted] = useState(false); const [hasMounted, setHasMounted] = useState(false);
const [reduceMotion, setReduceMotion] = useState(false);
useEffect(() => { useEffect(() => {
setHasMounted(true); setHasMounted(true);
setReduceMotion(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
const el = ref.current; const el = ref.current;
if (!el) return; if (!el) return;
@@ -37,7 +39,8 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc
observer.unobserve(el); observer.unobserve(el);
} }
}, },
{ threshold: 0.1 } // Start a little before the section is fully in view so it never feels late
{ threshold: 0.1, rootMargin: "0px 0px -60px 0px" }
); );
observer.observe(el); observer.observe(el);
@@ -50,8 +53,8 @@ export default function ScrollFadeIn({ children, className = "", delay = 0 }: Sc
className={className} className={className}
style={hasMounted ? { style={hasMounted ? {
opacity: isVisible ? 1 : 0, opacity: isVisible ? 1 : 0,
transform: isVisible ? "translateY(0)" : "translateY(30px)", transform: isVisible || reduceMotion ? "translateY(0)" : "translateY(16px)",
transition: `opacity 0.6s ease ${delay}s, transform 0.6s ease ${delay}s`, transition: `opacity 0.6s var(--ease-out) ${delay}s, transform 0.6s var(--ease-out) ${delay}s`,
} : undefined} } : undefined}
> >
{children} {children}
+1 -1
View File
@@ -24,7 +24,7 @@ export function ThemeToggle() {
return ( return (
<button <button
onClick={cycle} onClick={cycle}
className="p-2 rounded-full bg-stone-100 dark:bg-stone-800 text-stone-800 dark:text-stone-100 hover:bg-stone-200 dark:hover:bg-stone-700 border border-stone-200 dark:border-stone-700 shadow-sm hover:scale-105 active:scale-95 transition-all" className="p-2 rounded-full bg-stone-100 dark:bg-stone-800 text-stone-800 dark:text-stone-100 hover:bg-stone-200 dark:hover:bg-stone-700 border border-stone-200 dark:border-stone-700 shadow-sm active:scale-[0.97] transition-[background-color,transform] duration-150 ease-out"
aria-label={`Current theme: ${theme}. Click to switch.`} aria-label={`Current theme: ${theme}. Click to switch.`}
title={theme === "system" ? "System" : theme === "dark" ? "Dark" : "Light"} title={theme === "system" ? "System" : theme === "dark" ? "Dark" : "Light"}
> >
+26 -3
View File
@@ -25,6 +25,11 @@
--input: #efebe9; --input: #efebe9;
--ring: #a1887f; /* Warm brown ring */ --ring: #a1887f; /* Warm brown ring */
--radius: 1rem; --radius: 1rem;
/* Motion tokens: enter/exit = ease-out, on-screen movement = ease-in-out */
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
} }
.dark { .dark {
@@ -74,6 +79,20 @@ html {
scroll-behavior: smooth; scroll-behavior: smooth;
} }
/* Reduced motion: drop movement, keep colour/opacity feedback */
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
animation-delay: 0ms !important;
}
}
/* Liquid Glass Effects */ /* Liquid Glass Effects */
.glass-panel { .glass-panel {
background: var(--card); background: var(--card);
@@ -92,10 +111,13 @@ html {
box-shadow: box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.06), 0 4px 6px -1px rgba(0, 0, 0, 0.06),
0 2px 4px -1px rgba(0, 0, 0, 0.05); 0 2px 4px -1px rgba(0, 0, 0, 0.05);
transition: all 0.6s cubic-bezier(0.25, 0.1, 0.25, 1); transition:
will-change: transform, box-shadow; transform 250ms var(--ease-out),
box-shadow 250ms var(--ease-out),
border-color 200ms ease;
} }
@media (hover: hover) and (pointer: fine) {
.glass-card:hover { .glass-card:hover {
background: var(--card); background: var(--card);
box-shadow: box-shadow:
@@ -104,6 +126,7 @@ html {
transform: translateY(-4px); transform: translateY(-4px);
border-color: var(--ring); border-color: var(--ring);
} }
}
/* Typography & Headings */ /* Typography & Headings */
h1, h1,
@@ -229,7 +252,7 @@ div {
background: #fffcf5; background: #fffcf5;
border: 1px solid #d7ccc8; border: 1px solid #d7ccc8;
color: #3e2723; color: #3e2723;
transition: all 0.2s ease; transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 1px 2px rgba(62, 39, 35, 0.05); box-shadow: 0 1px 2px rgba(62, 39, 35, 0.05);
} }
+23 -4
View File
@@ -4,6 +4,11 @@ import type { Config } from "tailwindcss";
// tailwind.config.js // tailwind.config.js
export default { export default {
darkMode: "class", // Enables class-based dark mode darkMode: "class", // Enables class-based dark mode
// Touch devices fire :hover on tap and keep it stuck; only apply hover styles
// where a real pointer exists.
future: {
hoverOnlyWhenSupported: true,
},
content: [ content: [
"./app/**/*.{js,ts,jsx,tsx}", "./app/**/*.{js,ts,jsx,tsx}",
"./app/components/**/*.{js,ts,jsx,tsx}", "./app/components/**/*.{js,ts,jsx,tsx}",
@@ -11,19 +16,33 @@ export default {
], ],
theme: { theme: {
extend: { extend: {
// Stronger curves than the CSS defaults. Overriding `out` and `in-out`
// upgrades every existing `ease-out` / `ease-in-out` utility at once.
transitionTimingFunction: {
out: "cubic-bezier(0.23, 1, 0.32, 1)",
"in-out": "cubic-bezier(0.77, 0, 0.175, 1)",
drawer: "cubic-bezier(0.32, 0.72, 0, 1)",
},
keyframes: { keyframes: {
fadeIn: { fadeIn: {
"0%": { opacity: "0", transform: "translateY(10px)" }, "0%": { opacity: "0", transform: "translateY(8px)" },
"100%": { opacity: "1", transform: "translateY(0)" }, "100%": { opacity: "1", transform: "translateY(0)" },
}, },
slideDown: { slideDown: {
"0%": { opacity: "0", transform: "translateY(-20px)" }, "0%": { opacity: "0", transform: "translateY(-12px)" },
"100%": { opacity: "1", transform: "translateY(0)" }, "100%": { opacity: "1", transform: "translateY(0)" },
}, },
scrollHint: {
"0%": { opacity: "0", transform: "scaleY(0.4)" },
"40%": { opacity: "1", transform: "scaleY(1)" },
"100%": { opacity: "0", transform: "scaleY(1)" },
},
}, },
animation: { animation: {
"fade-in": "fadeIn 0.5s ease-out", "fade-in": "fadeIn 600ms cubic-bezier(0.23, 1, 0.32, 1) both",
"slide-down": "slideDown 0.4s ease-out", "slide-down": "slideDown 500ms cubic-bezier(0.23, 1, 0.32, 1) both",
"scroll-hint": "scrollHint 2.4s cubic-bezier(0.77, 0, 0.175, 1) infinite",
"spin-slow": "spin 4s linear infinite",
}, },
colors: { colors: {
background: "var(--background)", background: "var(--background)",