diff --git a/package-lock.json b/package-lock.json index d759509..6b525a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@types/react-dom": "^19.1.7", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^4.7.0", + "agentation": "^3.0.2", "babel-plugin-react-compiler": "1.0.0", "dotenv": "^17.2.2", "eslint": "^9.32.0", @@ -2138,6 +2139,25 @@ "node": ">= 14" } }, + "node_modules/agentation": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/agentation/-/agentation-3.0.2.tgz", + "integrity": "sha512-iGzBxFVTuZEIKzLY6AExSLAQH6i6SwxV4pAu7v7m3X6bInZ7qlZXAwrEqyc4+EfP4gM7z2RXBF6SF4DeH0f2lA==", + "dev": true, + "license": "PolyForm-Shield-1.0.0", + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", diff --git a/package.json b/package.json index f73aed5..f53d08c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@types/react-dom": "^19.1.7", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^4.7.0", + "agentation": "^3.0.2", "babel-plugin-react-compiler": "1.0.0", "dotenv": "^17.2.2", "eslint": "^9.32.0", diff --git a/src/assets/sascha_pixel.svg b/src/assets/sascha_pixel.svg new file mode 100644 index 0000000..eed1d6c --- /dev/null +++ b/src/assets/sascha_pixel.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/sections/AboutSection.tsx b/src/components/sections/AboutSection.tsx index 16f2bf8..9ed04a7 100644 --- a/src/components/sections/AboutSection.tsx +++ b/src/components/sections/AboutSection.tsx @@ -1,8 +1,10 @@ +import { useRef, useState, useEffect } from 'react'; import { Code, Palette, ShieldCheck, Eye } from 'lucide-react'; import { useLanguage } from '../../contexts/LanguageContext'; import FeatureCardsGrid from '../FeatureCardsGrid'; import type { Service } from '../../data/Service'; import saschaImage from '../../assets/sascha.png'; +import saschaPixel from '../../assets/sascha_pixel.svg'; // Define feature card icon and color mappings as constants const FEATURE_CARD_ICONS = [Code, Palette, ShieldCheck, Eye] as const; @@ -39,6 +41,26 @@ export default function AboutSection(props: AboutSectionProps = {}) { })) } = props; + const wrapperRef = useRef(null); + const [fading, setFading] = useState(false); + + useEffect(() => { + const el = wrapperRef.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + const timer = setTimeout(() => setFading(true), 2000); + observer.disconnect(); + return () => clearTimeout(timer); + } + }, + { threshold: 0.4 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + return (
@@ -63,7 +85,7 @@ export default function AboutSection(props: AboutSectionProps = {}) { {/* Personal Photo and Bio */}
-
+
{`Professional +
diff --git a/src/components/sections/HeroSection.tsx b/src/components/sections/HeroSection.tsx index aa18523..b039a97 100644 --- a/src/components/sections/HeroSection.tsx +++ b/src/components/sections/HeroSection.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { Download } from 'lucide-react'; import { useLocation, useNavigate } from 'react-router-dom'; import type { StatItem } from '../../data/StatItem'; @@ -41,6 +42,17 @@ export default function HeroSection(props: HeroSectionProps = {}) { const navigate = useNavigate(); const { announce } = useScreenReaderAnnouncements(); + const [loaded, setLoaded] = useState(false); + useEffect(() => { + if (document.readyState === 'complete') { + setLoaded(true); + } else { + const onLoad = () => setLoaded(true); + window.addEventListener('load', onLoad); + return () => window.removeEventListener('load', onLoad); + } + }, []); + const handleViewWork = () => { announce('Navigating to projects section'); scrollToProjects(location.pathname, navigate); @@ -72,7 +84,7 @@ export default function HeroSection(props: HeroSectionProps = {}) { }; return ( -
+
{/* Skip link for keyboard navigation */}
diff --git a/src/scss/layout/topbar.scss b/src/scss/layout/topbar.scss index 79c0e3b..fb3ee1d 100644 --- a/src/scss/layout/topbar.scss +++ b/src/scss/layout/topbar.scss @@ -33,14 +33,20 @@ &__container { display: flex; align-items: center; - gap: 0.5rem; + // Fluid gap: 0.25rem at 1050px → 0.5rem at 1300px + gap: clamp(0.25rem, -0.8rem + 1.6vw, 0.5rem); &__button { @extend %button-reset; @extend %hover-lift; - padding: 0.6rem 1.2rem; + // Fluid padding: 0.5rem 0.5rem at 1050px → 0.6rem 1.2rem at 1300px + padding: clamp(0.5rem, 0.08rem + 0.64vw, 0.6rem) + clamp(0.5rem, -2.44rem + 4.48vw, 1.2rem); border-radius: 8px; color: var(--color-text-on-dark); + white-space: nowrap; + // Fluid font-size: 0.85rem at 1050px → 1rem at 1300px + font-size: clamp(0.85rem, 0.22rem + 0.96vw, 1rem); transition: color 0.2s ease, box-shadow 0.2s ease, @@ -113,6 +119,7 @@ top: 65px; right: 0; background: var(--color-background); + color: var(--color-text); border: 1px solid var(--color-primary); border-radius: 8px; padding: 0; @@ -172,8 +179,8 @@ } } -// Landing page has 7 menu items — switch to burger at a wider breakpoint -@media (max-width: 1023px) { +// Landing page has 7 menu items — switch to burger at same breakpoint as regular nav +@media (max-width: 1049px) { .navbar--landing .navbar__container__button { display: none; } @@ -185,8 +192,8 @@ } } -// Keep burger visible for landing page up to 1023px -@media (min-width: 769px) and (max-width: 1023px) { +// Keep burger visible for landing page up to 1049px +@media (min-width: 769px) and (max-width: 1049px) { .navbar--landing .navbar__burger-button { display: flex; } diff --git a/src/scss/sections/about-section.scss b/src/scss/sections/about-section.scss index 6d70279..95514a1 100644 --- a/src/scss/sections/about-section.scss +++ b/src/scss/sections/about-section.scss @@ -74,6 +74,50 @@ background: var(--gradient-image-overlay); } + &__pixel-bg { + position: absolute; + inset: 0 0 0 -35px; + background-color: #dbeafe; + opacity: 1; + transition: opacity 1.2s ease-in-out; + will-change: opacity; + pointer-events: none; + + [data-theme='dark'] & { + background-color: #1e3a8a; + } + + &--fade { + opacity: 0; + } + + @media (prefers-reduced-motion: reduce) { + display: none; + } + } + + &__pixel-overlay { + position: absolute; + inset: 0 0 0 -35px; + width: calc(100% + 35px); + height: 100%; + object-fit: cover; + object-position: calc(30%) 50%; + border: 4px solid var(--color-background); + opacity: 1; + transition: opacity 1.2s ease-in-out; + will-change: opacity; + pointer-events: none; + + &--fade { + opacity: 0; + } + + @media (prefers-reduced-motion: reduce) { + display: none; + } + } + &__bio-section { flex: 1; text-align: center; diff --git a/src/scss/sections/hero-section.scss b/src/scss/sections/hero-section.scss index bf55578..9a193cc 100644 --- a/src/scss/sections/hero-section.scss +++ b/src/scss/sections/hero-section.scss @@ -17,6 +17,33 @@ } } +@keyframes hero-fade-up { + from { + opacity: 0; + transform: translate3d(0, 20px, 0); + } + to { + opacity: 1; + transform: translate3d(0, 0, 0); + } +} + +@keyframes hero-bubble-before { + 0% { transform: scale(0.6) translate3d(0, 16px, 0); opacity: 0; } + 40% { transform: scale(1.1) translate3d(0, -5px, 0); opacity: 1; } + 65% { transform: scale(0.96) translate3d(0, 3px, 0); } + 82% { transform: scale(1.03) translate3d(0, -1px, 0); } + 100% { transform: scale(1) translate3d(0, 0, 0); opacity: 1; } +} + +@keyframes hero-bubble-after { + 0% { transform: scale(0.5) translate3d(0, -12px, 0); opacity: 0; } + 45% { transform: scale(1.14) translate3d(0, 4px, 0); opacity: 1; } + 68% { transform: scale(0.94) translate3d(0, -2px, 0); } + 84% { transform: scale(1.04) translate3d(0, 1px, 0); } + 100% { transform: scale(1) translate3d(0, 0, 0); opacity: 1; } +} + .hero-section { @include flex-center; min-height: 100vh; @@ -34,6 +61,7 @@ border-radius: 50%; border: 2px solid rgba(12, 68, 124, 0.15); pointer-events: none; + will-change: transform, opacity; } &::after { @@ -46,6 +74,7 @@ border-radius: 50%; background: rgba(239, 159, 39, 0.08); pointer-events: none; + will-change: transform, opacity; } [data-theme='dark'] & { @@ -76,6 +105,7 @@ letter-spacing: -0.03em; line-height: 1.1; color: $color-heading; + will-change: transform, opacity; } &__description { @@ -85,12 +115,14 @@ max-width: 600px; margin-left: auto; margin-right: auto; + will-change: transform, opacity; } &__buttons { @include flex-center; gap: $spacing-lg; flex-wrap: wrap; + will-change: transform, opacity; } &__button { @@ -148,6 +180,7 @@ text-align: center; padding: $spacing-md; border-radius: $border-radius-md; + will-change: transform, opacity; &--1 { background: var(--stat-primary-bg); @@ -231,3 +264,37 @@ } } } + +// Animations fire only after the page has fully loaded (--loaded class added via JS) +@media (prefers-reduced-motion: no-preference) { + .hero-section--loaded { + &::before { + animation: hero-bubble-before 4s cubic-bezier(0.34, 1.2, 0.64, 1) both 0.2s; + } + + &::after { + animation: hero-bubble-after 4s cubic-bezier(0.34, 1.2, 0.64, 1) both 0.7s; + } + + .hero-section__title { + animation: hero-fade-up 0.6s ease-out both 0.2s; + } + + .hero-section__description { + animation: hero-fade-up 0.6s ease-out both 0.35s; + } + + .hero-section__buttons { + animation: hero-fade-up 0.6s ease-out both 0.5s; + } + + .hero-section__stat-item { + animation: hero-fade-up 0.7s ease-out both; + + &--1 { animation-delay: 0.7s; } + &--2 { animation-delay: 0.85s; } + &--3 { animation-delay: 1s; } + &--4 { animation-delay: 1.15s; } + } + } +} diff --git a/src/scss/variables.scss b/src/scss/variables.scss index 2a5db6e..fd83587 100644 --- a/src/scss/variables.scss +++ b/src/scss/variables.scss @@ -1,6 +1,6 @@ // Breakpoint variables -$mobile-breakpoint: 768px; -$desktop-breakpoint: 769px; +$mobile-breakpoint: 1050px; +$desktop-breakpoint: 1050px; // Color variables $color-primary: #042c53;