+
- {bio.map((paragraph) => ( -

{paragraph}

+ {bio.map((paragraph, index) => ( +

{paragraph}

))}
From bdbce1c3adeabfd5ca397c675ed6c4b25104b0a1 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 26 Mar 2026 21:19:46 +0100 Subject: [PATCH 07/19] feat: enhance text configuration and localization for appointment booking - Added new text configurations for appointment hints and links in TextConfig.ts and contact.ts. - Updated imprint.ts with detailed privacy policy sections regarding online appointment booking, including data handling and legal basis. - Introduced new appointment-related fields in personal.ts for scheduling. - Implemented SEO enhancements across HomePage, ImprintPage, LandingPage, and PrivacyPolicy pages using usePageSeo hook. - Created a prerendering script to generate static HTML for SEO optimization. - Added .htaccess and robots.txt for server configuration and search engine indexing control. - Developed a sitemap.xml for better search engine visibility. - Improved styling for contact section and imprint page to accommodate new appointment features. --- index.html | 76 +- package-lock.json | 1002 +++++++++++++++++ package.json | 4 +- prerender.js | 101 ++ public/.htaccess | 4 + public/robots.txt | 6 + public/sitemap.xml | 18 + src/components/sections/AboutSection.tsx | 1 - src/components/sections/ContactSection.tsx | 16 +- src/config/locales/de/contact.ts | 2 + src/config/locales/de/imprint.ts | 31 + src/config/locales/en/TextConfig.ts | 19 + src/config/locales/en/contact.ts | 2 + src/config/locales/en/imprint.ts | 197 ++-- src/config/personal.ts | 3 + src/hooks/usePageSeo.ts | 56 + src/pages/HomePage.tsx | 7 + src/pages/ImprintPage.tsx | 51 +- src/pages/LandingPage.tsx | 7 + src/pages/PrivacyPolicy.tsx | 23 +- src/scss/sections/certifications-section.scss | 7 - src/scss/sections/contact-section.scss | 46 +- src/scss/sections/imprint-page.scss | 1 - 23 files changed, 1547 insertions(+), 133 deletions(-) create mode 100644 prerender.js create mode 100644 public/.htaccess create mode 100644 public/robots.txt create mode 100644 public/sitemap.xml create mode 100644 src/hooks/usePageSeo.ts diff --git a/index.html b/index.html index 29b5442..b129480 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,84 @@ - Sascha Bach Portfolio + + + Sascha Bach | Freelance Software Developer – Accessible Web Development + + + + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/package-lock.json b/package-lock.json index 2598be0..d759509 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "puppeteer": "^24.40.0", "sass": "^1.90.0", "ssh2-sftp-client": "^12.0.1", "typescript": "~5.8.3", @@ -1326,6 +1327,41 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@puppeteer/browsers/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1641,6 +1677,13 @@ "win32" ] }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1760,6 +1803,17 @@ "@types/react-router": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.48.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.0.tgz", @@ -2074,6 +2128,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2091,6 +2155,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2124,6 +2198,34 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/babel-plugin-react-compiler": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", @@ -2141,6 +2243,103 @@ "dev": true, "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", + "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.11.0.tgz", + "integrity": "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.8.31", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", @@ -2151,6 +2350,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -2220,6 +2429,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2301,6 +2520,45 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2364,6 +2622,33 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cpu-features": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", @@ -2401,6 +2686,16 @@ "dev": true, "license": "MIT" }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2426,6 +2721,21 @@ "dev": true, "license": "MIT" }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2440,6 +2750,13 @@ "node": ">=0.10" } }, + "node_modules/devtools-protocol": { + "version": "0.0.1581282", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", + "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/dotenv": { "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", @@ -2460,6 +2777,43 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -2525,6 +2879,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, "node_modules/eslint": { "version": "9.39.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", @@ -2663,6 +3039,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -2709,6 +3099,37 @@ "node": ">=0.10.0" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2716,6 +3137,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2730,6 +3158,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2820,6 +3258,47 @@ "node": ">=6.9.0" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2880,6 +3359,34 @@ "hermes-estree": "0.25.1" } }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2931,6 +3438,23 @@ "dev": true, "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2941,6 +3465,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -3012,6 +3546,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3063,6 +3604,13 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -3133,6 +3681,13 @@ "node": "*" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3174,6 +3729,16 @@ "dev": true, "license": "MIT" }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/node-addon-api": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", @@ -3189,6 +3754,16 @@ "dev": true, "license": "MIT" }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3239,6 +3814,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3252,6 +3861,25 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3272,6 +3900,13 @@ "node": ">=8" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3332,6 +3967,64 @@ "node": ">= 0.8.0" } }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3342,6 +4035,47 @@ "node": ">=6" } }, + "node_modules/puppeteer": { + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", + "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1581282", + "puppeteer-core": "24.40.0", + "typed-query-selector": "^2.12.1" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.40.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", + "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1581282", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -3440,6 +4174,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3586,6 +4330,58 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3632,6 +4428,18 @@ "url": "https://square.link/u/4g7sPflL" } }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -3642,6 +4450,34 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3668,6 +4504,54 @@ "node": ">=8" } }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3743,6 +4627,13 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -3763,6 +4654,13 @@ "node": ">= 0.8.0" } }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "dev": true, + "license": "MIT" + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -3969,6 +4867,13 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3995,6 +4900,63 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -4002,6 +4964,46 @@ "dev": true, "license": "ISC" }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 6ae12cf..f73aed5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "tsc -b && vite build && node prerender.js", + "build:no-prerender": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview", "deploy": "npm run build && node deploy.js", @@ -30,6 +31,7 @@ "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "puppeteer": "^24.40.0", "sass": "^1.90.0", "ssh2-sftp-client": "^12.0.1", "typescript": "~5.8.3", diff --git a/prerender.js b/prerender.js new file mode 100644 index 0000000..e418b0d --- /dev/null +++ b/prerender.js @@ -0,0 +1,101 @@ +/** + * Post-build prerendering script. + * Starts a local server from dist/, visits each route with Puppeteer, + * and saves the fully-rendered HTML so crawlers see real content. + * + * Usage: node prerender.js (run after `vite build`) + */ + +import { createServer } from 'http'; +import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import puppeteer from 'puppeteer'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DIST = join(__dirname, 'dist'); +const PORT = 4173; + +const ROUTES = ['/', '/technical', '/imprint', '/privacy-policy']; + +/** Minimal static file server that falls back to index.html (SPA). */ +function startServer() { + const mimeTypes = { + '.html': 'text/html', + '.js': 'application/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.svg': 'image/svg+xml', + '.pdf': 'application/pdf', + '.woff2': 'font/woff2', + '.woff': 'font/woff', + }; + + const server = createServer((req, res) => { + let filePath = join(DIST, req.url === '/' ? '/index.html' : req.url); + + if (!existsSync(filePath) || !filePath.includes('.')) { + filePath = join(DIST, 'index.html'); + } + + const ext = '.' + filePath.split('.').pop(); + const contentType = mimeTypes[ext] || 'application/octet-stream'; + + try { + const content = readFileSync(filePath); + res.writeHead(200, { 'Content-Type': contentType }); + res.end(content); + } catch { + res.writeHead(404); + res.end('Not found'); + } + }); + + return new Promise((resolve) => { + server.listen(PORT, () => { + console.log(` Server running on http://localhost:${PORT}`); + resolve(server); + }); + }); +} + +async function prerender() { + console.log('\n🔍 Prerendering routes...\n'); + + const server = await startServer(); + const browser = await puppeteer.launch({ headless: true }); + + for (const route of ROUTES) { + const page = await browser.newPage(); + const url = `http://localhost:${PORT}${route}`; + + console.log(` Rendering ${route} ...`); + await page.goto(url, { waitUntil: 'networkidle0', timeout: 15000 }); + + // Wait a bit for React to finish any async rendering + await new Promise((r) => setTimeout(r, 1500)); + + const html = await page.content(); + await page.close(); + + // Write to dist//index.html + const outDir = route === '/' ? DIST : join(DIST, route); + if (!existsSync(outDir)) { + mkdirSync(outDir, { recursive: true }); + } + const outFile = join(outDir, 'index.html'); + writeFileSync(outFile, html, 'utf-8'); + console.log(` ✓ Saved ${outFile.replace(DIST, 'dist')}`); + } + + await browser.close(); + server.close(); + console.log('\n✅ Prerendering complete!\n'); +} + +prerender().catch((err) => { + console.error('Prerendering failed:', err); + process.exit(1); +}); diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..152dce1 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,4 @@ +Options -MultiViews +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^ index.html [QSA,L] \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..dc58902 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,6 @@ +User-agent: * +Allow: / +Disallow: /imprint +Disallow: /privacy-policy + +Sitemap: https://sascha-bach.de/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..53c760d --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,18 @@ + + + + https://sascha-bach.de/ + + + monthly + 1.0 + + + https://sascha-bach.de/technical + + + monthly + 0.8 + + diff --git a/src/components/sections/AboutSection.tsx b/src/components/sections/AboutSection.tsx index f62c6b3..d111e84 100644 --- a/src/components/sections/AboutSection.tsx +++ b/src/components/sections/AboutSection.tsx @@ -7,7 +7,6 @@ import saschaImage from '../../assets/sascha.png'; // Define feature card icon and color mappings as constants const FEATURE_CARD_ICONS = [Code, Palette, ShieldCheck, Eye] as const; const FEATURE_CARD_COLORS = ['primary', 'secondary', 'tertiary', 'quaternary'] as const; -type FeatureCardColor = typeof FEATURE_CARD_COLORS[number]; interface AboutSectionProps { name?: string; diff --git a/src/components/sections/ContactSection.tsx b/src/components/sections/ContactSection.tsx index e2d39b9..69c36d1 100644 --- a/src/components/sections/ContactSection.tsx +++ b/src/components/sections/ContactSection.tsx @@ -1,5 +1,5 @@ // eslint-disable-next-line @typescript-eslint/no-deprecated -import { Mail, Send, Globe, Linkedin } from 'lucide-react'; +import { Mail, Send, Globe, Linkedin, CalendarClock } from 'lucide-react'; import { personalConfig } from '../../config/personal'; import { useLanguage } from '../../contexts/LanguageContext'; import { useScreenReaderAnnouncements } from '../../hooks/useScreenReaderAnnouncements'; @@ -89,6 +89,20 @@ export default function ContactSection(props: ContactSectionProps = {}) { +
+
+ - - + {showPrimaryButton && ( + + )} + {showSecondaryButton && ( + + )} + {showTertiaryButton && ( + + )} {/* Hidden descriptions for screen readers */} diff --git a/src/config/certifications-config.ts b/src/config/certifications-config.ts index a375dae..4878f2c 100644 --- a/src/config/certifications-config.ts +++ b/src/config/certifications-config.ts @@ -1,4 +1,4 @@ -import { certifications } from './locales/en/certifications'; +import { certifications } from './locales/en/technical/certifications'; import type { Certification } from '../data/Certification'; export const certificationsData: Certification[] = certifications.certificationItems diff --git a/src/config/locales/de/index.ts b/src/config/locales/de/index.ts index f4d8981..e13fd64 100644 --- a/src/config/locales/de/index.ts +++ b/src/config/locales/de/index.ts @@ -1,18 +1,19 @@ import { navigation } from './navigation'; import { themeToggle } from '../../theme-toggle'; import { footer } from './footer'; -import { hero } from './hero'; -import { about } from './about'; -import { landingAboutMe } from './landing-aboutme'; -import { landingReasons } from './landing-reasons'; -import { landingExperience } from './landing-experience'; -import { landingWinnings } from './landing-winnings'; -import { landingProcesses } from './landing-processes'; -import { landingReferences } from './landing-references'; -import { services } from './services'; -import { skills } from './skills'; +import { hero } from './technical/hero'; +import { about } from './technical/about'; +import { landingAboutMe } from './landing/landing-aboutme'; +import { landingHero } from './landing/landing-hero'; +import { landingReasons } from './landing/landing-reasons'; +import { landingExperience } from './landing/landing-experience'; +import { landingWinnings } from './landing/landing-winnings'; +import { landingProcesses } from './landing/landing-processes'; +import { landingReferences } from './landing/landing-references'; +import { services } from './technical/services'; +import { skills } from './technical/skills'; import { projects } from './projects'; -import { certifications } from './certifications'; +import { certifications } from './technical/certifications'; import { contact } from './contact'; import { imprint } from './imprint'; import { privacyPolicy } from './privacy-policy'; @@ -25,6 +26,7 @@ export const de = { hero, about, landingAboutMe, + landingHero, landingReasons, landingExperience, landingWinnings, diff --git a/src/config/locales/de/landing-aboutme.ts b/src/config/locales/de/landing-aboutme.ts deleted file mode 100644 index c131e19..0000000 --- a/src/config/locales/de/landing-aboutme.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const landingAboutMe = { - title: 'Mehr Kunden durch Barrierefreiheit', - subtitle: '', - greeting: 'Hi, ich bin Sascha.', - name: 'Sascha', - bio: [ - 'Ich verhelfe Unternehmen zu Webseiten, die von allen genutzt werden können. Eine Webseite, die für alle funktioniert, erreicht alle.', - ], - featureCards: [] as Array<{ title: string; description: string; }>, -}; diff --git a/src/config/locales/de/landing/landing-aboutme.ts b/src/config/locales/de/landing/landing-aboutme.ts new file mode 100644 index 0000000..ec8e426 --- /dev/null +++ b/src/config/locales/de/landing/landing-aboutme.ts @@ -0,0 +1,17 @@ +export const landingAboutMe = { + title: 'Über mich', + subtitle: '', + greeting: 'Hi, ich bin Sascha.', + name: 'Sascha', + bio: [ + `Seit ich zum ersten Mal mit dem Thema Barrierefreiheit in Berührung gekommen bin, habe ich + für mich einen Mehrwert in der Webentwicklung entdeckt, der mich seitdem nicht mehr loslässt. + Barrierefreiheit ist für mich nicht nur ein technisches Thema, sondern eine Angelegenheit, die mit meinen persönlichen Werten + von Inklusion und Gleichberechtigung übereinstimmt. + Ich helfe Webagenturen und Unternehmen bei der Entwicklung und Umsetzung von Apps und Webseiten mit und ohne Barrierefreiheit.`, + `In meiner Freizeit tanze ich Brasilianischen Zouk und West Coast Swing. Ich beschäftige mich mit alternativen + Beziehungsformen und bin ein großer Fan von Science-Fiction-Filmen. Außerdem interessiere ich mich für Persönlichkeitsentwicklung und + Ethik in der Technologie. Ich bin immer offen für neue Kontakte und spannende Projekte, also zögert nicht, mich zu kontaktieren!`, + ], + featureCards: [] as Array<{ title: string; description: string; }>, +}; diff --git a/src/config/locales/de/landing-experience.ts b/src/config/locales/de/landing/landing-experience.ts similarity index 100% rename from src/config/locales/de/landing-experience.ts rename to src/config/locales/de/landing/landing-experience.ts diff --git a/src/config/locales/de/landing/landing-hero.ts b/src/config/locales/de/landing/landing-hero.ts new file mode 100644 index 0000000..93d234c --- /dev/null +++ b/src/config/locales/de/landing/landing-hero.ts @@ -0,0 +1,5 @@ +export const landingHero = { + title: 'Barrierefreiheit: 20\u00A0% mehr Reichweite, 100\u00A0% mehr Zugang.', + description: 'Ich helfe Ihnen, Ihre Website für jeden zugänglich zu machen – im Einklang mit dem BFSG.', + tertiaryButtonText: 'Kontakt aufnehmen', +}; diff --git a/src/config/locales/de/landing-processes.ts b/src/config/locales/de/landing/landing-processes.ts similarity index 100% rename from src/config/locales/de/landing-processes.ts rename to src/config/locales/de/landing/landing-processes.ts diff --git a/src/config/locales/de/landing-reasons.ts b/src/config/locales/de/landing/landing-reasons.ts similarity index 85% rename from src/config/locales/de/landing-reasons.ts rename to src/config/locales/de/landing/landing-reasons.ts index ccf6735..75cb3c6 100644 --- a/src/config/locales/de/landing-reasons.ts +++ b/src/config/locales/de/landing/landing-reasons.ts @@ -3,11 +3,11 @@ export const landingReasons = { reasons: [ { - stat: '15 %', - text: 'aller Menschen in Deutschland haben eine Beeinträchtigung. Das sind 12 Millionen potenzielle Kunden.', + stat: '20\u00A0%', + text: 'aller Menschen in Deutschland haben eine Beeinträchtigung. Das sind 16 Millionen potenzielle Kunden.', }, { - stat: '80 %', + stat: '80\u00A0%', text: 'dieser Menschen verlassen Webseiten, die nicht für sie funktionieren.', }, { diff --git a/src/config/locales/de/landing-references.ts b/src/config/locales/de/landing/landing-references.ts similarity index 100% rename from src/config/locales/de/landing-references.ts rename to src/config/locales/de/landing/landing-references.ts diff --git a/src/config/locales/de/landing-winnings.ts b/src/config/locales/de/landing/landing-winnings.ts similarity index 100% rename from src/config/locales/de/landing-winnings.ts rename to src/config/locales/de/landing/landing-winnings.ts diff --git a/src/config/locales/de/about.ts b/src/config/locales/de/technical/about.ts similarity index 97% rename from src/config/locales/de/about.ts rename to src/config/locales/de/technical/about.ts index 855d031..bd8f023 100644 --- a/src/config/locales/de/about.ts +++ b/src/config/locales/de/technical/about.ts @@ -6,7 +6,7 @@ export const about = { bio: [ 'Ich habe Informatik an der TU Darmstadt studiert und 2016 meinen Bachelor mit einer Arbeit über Motion Sickness in Virtual Reality abgeschlossen. Seitdem arbeite ich als Software- und Webentwickler mit Schwerpunkt auf Angular und React.', 'In meiner Arbeit habe ich gesehen, dass jedes Unternehmen anders ist, aber viele stehen vor der gleichen Herausforderung: ihre digitale Präsenz zugänglich, modern und zukunftssicher zu halten. Vielen Unternehmen ist bereits bewusst, dass Barrierefreiheit gesetzlich vorgeschrieben ist, aber in der Praxis kommt es vor allem darauf an, dass eine Website für alle Nutzer einfach und angenehm funktioniert.', - 'Meine Spezialisierung liegt in Web Accessibility (WCAG 2.1, BFSG, European Accessibility Act). Ich helfe Unternehmen dabei, ihre Websites und Anwendungen zu aktualisieren, sodass sie konform und gleichzeitig für jeden einfach zu bedienen sind. Was meine Kunden schätzen, ist, dass ich technisches Fachwissen in Angular, React und TypeScript mit einem menschenzentrierten Ansatz verbinde. Denn Barrierefreiheit geht nicht nur um Code, sondern um Menschen.', + 'Meine Spezialisierung liegt in Web Accessibility (WCAG 2.2, BFSG, European Accessibility Act). Ich helfe Unternehmen dabei, ihre Websites und Anwendungen zu aktualisieren, sodass sie konform und gleichzeitig für jeden einfach zu bedienen sind. Was meine Kunden schätzen, ist, dass ich technisches Fachwissen in Angular, React und TypeScript mit einem menschenzentrierten Ansatz verbinde. Denn Barrierefreiheit geht nicht nur um Code, sondern um Menschen.', 'Wenn Sie sich fragen, ob Ihre Website bereits den Standards für Barrierefreiheit entspricht, oder wenn Sie das Gefühl haben, dass es Zeit ist, Ihre Online-Präsenz inklusiver und rechtssicher zu gestalten, würde ich Sie gerne unterstützen. Oft können bereits wenige klare Schritte einen großen Unterschied machen und Ihrer Website helfen, mehr Menschen zu erreichen und sich von der Konkurrenz abzuheben.', 'Lassen Sie uns verbinden und schauen, wie wir Ihre Website barrierefrei, modern und zukunftssicher machen können.', ], diff --git a/src/config/locales/de/certifications.ts b/src/config/locales/de/technical/certifications.ts similarity index 72% rename from src/config/locales/de/certifications.ts rename to src/config/locales/de/technical/certifications.ts index ef95eb3..1e35ce8 100644 --- a/src/config/locales/de/certifications.ts +++ b/src/config/locales/de/technical/certifications.ts @@ -1,4 +1,4 @@ -import { certificationItems } from '../../certifications-data'; +import { certificationItems } from '../../../certifications-data'; export const certifications = { title: 'Zertifizierungen & Erfolge', diff --git a/src/config/locales/de/hero.ts b/src/config/locales/de/technical/hero.ts similarity index 89% rename from src/config/locales/de/hero.ts rename to src/config/locales/de/technical/hero.ts index 1968d7b..4db7850 100644 --- a/src/config/locales/de/hero.ts +++ b/src/config/locales/de/technical/hero.ts @@ -1,5 +1,5 @@ export const hero = { - title: 'Jeder verdient ein reibungsloses Online-Erlebnis', + title: 'Barrierefreie Webentwicklung für alle', description: 'Ich helfe Ihnen, barrierefreie Websites zu erstellen, die jeden Besucher einbeziehen, im Einklang mit dem Barrierefreiheitsstärkungsgesetz.', primaryButtonText: 'Projekte ansehen', diff --git a/src/config/locales/de/services.ts b/src/config/locales/de/technical/services.ts similarity index 100% rename from src/config/locales/de/services.ts rename to src/config/locales/de/technical/services.ts diff --git a/src/config/locales/de/skills.ts b/src/config/locales/de/technical/skills.ts similarity index 100% rename from src/config/locales/de/skills.ts rename to src/config/locales/de/technical/skills.ts diff --git a/src/config/locales/en/TextConfig.ts b/src/config/locales/en/TextConfig.ts index 089bfbb..1894060 100644 --- a/src/config/locales/en/TextConfig.ts +++ b/src/config/locales/en/TextConfig.ts @@ -52,6 +52,13 @@ export interface TextConfig { }>; }; + // Landing Hero Section + landingHero: { + title: string; + description: string; + tertiaryButtonText: string; + }; + // Landing About Me Section landingAboutMe: { title: string; diff --git a/src/config/locales/en/index.ts b/src/config/locales/en/index.ts index 01d84c0..e516834 100644 --- a/src/config/locales/en/index.ts +++ b/src/config/locales/en/index.ts @@ -1,18 +1,19 @@ import { navigation } from './navigation'; import { themeToggle } from '../../theme-toggle'; import { footer } from './footer'; -import { hero } from './hero'; -import { about } from './about'; -import { landingAboutMe } from './landing-aboutme'; -import { landingReasons } from './landing-reasons'; -import { landingExperience } from './landing-experience'; -import { landingWinnings } from './landing-winnings'; -import { landingProcesses } from './landing-processes'; -import { landingReferences } from './landing-references'; -import { services } from './services'; -import { skills } from './skills'; +import { hero } from './technical/hero'; +import { about } from './technical/about'; +import { landingAboutMe } from './landing/landing-aboutme'; +import { landingHero } from './landing/landing-hero'; +import { landingReasons } from './landing/landing-reasons'; +import { landingExperience } from './landing/landing-experience'; +import { landingWinnings } from './landing/landing-winnings'; +import { landingProcesses } from './landing/landing-processes'; +import { landingReferences } from './landing/landing-references'; +import { services } from './technical/services'; +import { skills } from './technical/skills'; import { projects } from './projects'; -import { certifications } from './certifications'; +import { certifications } from './technical/certifications'; import { contact } from './contact'; import { imprint } from './imprint'; import { privacyPolicy } from './privacy-policy'; @@ -29,6 +30,7 @@ export const en: TextConfig = { hero, about, landingAboutMe, + landingHero, landingReasons, landingExperience, landingWinnings, diff --git a/src/config/locales/en/landing-aboutme.ts b/src/config/locales/en/landing/landing-aboutme.ts similarity index 100% rename from src/config/locales/en/landing-aboutme.ts rename to src/config/locales/en/landing/landing-aboutme.ts diff --git a/src/config/locales/en/landing-experience.ts b/src/config/locales/en/landing/landing-experience.ts similarity index 100% rename from src/config/locales/en/landing-experience.ts rename to src/config/locales/en/landing/landing-experience.ts diff --git a/src/config/locales/en/landing/landing-hero.ts b/src/config/locales/en/landing/landing-hero.ts new file mode 100644 index 0000000..c040dad --- /dev/null +++ b/src/config/locales/en/landing/landing-hero.ts @@ -0,0 +1,5 @@ +export const landingHero = { + title: 'Accessible websites that reach more customers and ensure legal compliance', + description: 'I help you make your website accessible to everyone – compliant with accessibility laws.', + tertiaryButtonText: 'Get in Touch', +}; diff --git a/src/config/locales/en/landing-processes.ts b/src/config/locales/en/landing/landing-processes.ts similarity index 100% rename from src/config/locales/en/landing-processes.ts rename to src/config/locales/en/landing/landing-processes.ts diff --git a/src/config/locales/en/landing-reasons.ts b/src/config/locales/en/landing/landing-reasons.ts similarity index 100% rename from src/config/locales/en/landing-reasons.ts rename to src/config/locales/en/landing/landing-reasons.ts diff --git a/src/config/locales/en/landing-references.ts b/src/config/locales/en/landing/landing-references.ts similarity index 100% rename from src/config/locales/en/landing-references.ts rename to src/config/locales/en/landing/landing-references.ts diff --git a/src/config/locales/en/landing-winnings.ts b/src/config/locales/en/landing/landing-winnings.ts similarity index 100% rename from src/config/locales/en/landing-winnings.ts rename to src/config/locales/en/landing/landing-winnings.ts diff --git a/src/config/locales/en/about.ts b/src/config/locales/en/technical/about.ts similarity index 100% rename from src/config/locales/en/about.ts rename to src/config/locales/en/technical/about.ts diff --git a/src/config/locales/en/certifications.ts b/src/config/locales/en/technical/certifications.ts similarity index 71% rename from src/config/locales/en/certifications.ts rename to src/config/locales/en/technical/certifications.ts index 5d91c51..d10a981 100644 --- a/src/config/locales/en/certifications.ts +++ b/src/config/locales/en/technical/certifications.ts @@ -1,4 +1,4 @@ -import { certificationItems } from '../../certifications-data'; +import { certificationItems } from '../../../certifications-data'; export const certifications = { title: 'Certifications & Achievements', diff --git a/src/config/locales/en/hero.ts b/src/config/locales/en/technical/hero.ts similarity index 100% rename from src/config/locales/en/hero.ts rename to src/config/locales/en/technical/hero.ts diff --git a/src/config/locales/en/services.ts b/src/config/locales/en/technical/services.ts similarity index 100% rename from src/config/locales/en/services.ts rename to src/config/locales/en/technical/services.ts diff --git a/src/config/locales/en/skills.ts b/src/config/locales/en/technical/skills.ts similarity index 100% rename from src/config/locales/en/skills.ts rename to src/config/locales/en/technical/skills.ts diff --git a/src/contexts/LanguageContext.tsx b/src/contexts/LanguageContext.tsx index d00e9fe..5571c95 100644 --- a/src/contexts/LanguageContext.tsx +++ b/src/contexts/LanguageContext.tsx @@ -11,46 +11,29 @@ interface LanguageContextType { const LanguageContext = createContext(undefined); -const LANGUAGE_KEY = 'preferred-language'; - -function getBrowserLanguage(): Language { - const browserLang = navigator.language.toLowerCase(); - if (browserLang.startsWith('en')) { - return 'en'; - } - return 'de'; // Default to German for all other languages -} - -function getSavedLanguage(): Language | null { - const saved = localStorage.getItem(LANGUAGE_KEY); - if (saved === 'en' || saved === 'de') { - return saved; - } +/** Derive the language from the URL pathname prefix (/en/… or /de/…). */ +export function getLanguageFromPath(pathname: string): Language | null { + if (pathname.startsWith('/en')) return 'en'; + if (pathname.startsWith('/de')) return 'de'; return null; } -export function LanguageProvider({ children }: { children: ReactNode; }) { - const [language, setLanguageState] = useState(() => { - // First check localStorage, then browser language, then default to German - return getSavedLanguage() || getBrowserLanguage(); - }); +export function LanguageProvider({ initialLanguage = 'de', children }: Readonly<{ initialLanguage?: Language; children: ReactNode; }>) { + const [language, setLanguage] = useState(initialLanguage); const texts = language === 'de' ? de : en; - const setLanguage = (lang: Language) => { - setLanguageState(lang); - localStorage.setItem(LANGUAGE_KEY, lang); - // Update HTML lang attribute for accessibility and SEO + const handleSetLanguage = (lang: Language) => { + setLanguage(lang); document.documentElement.lang = lang; }; useEffect(() => { - // Set initial lang attribute document.documentElement.lang = language; }, [language]); return ( - + {children} ); diff --git a/src/hooks/usePageSeo.ts b/src/hooks/usePageSeo.ts index bd4ed13..4986e84 100644 --- a/src/hooks/usePageSeo.ts +++ b/src/hooks/usePageSeo.ts @@ -5,11 +5,12 @@ interface SeoProps { description: string; canonical?: string; noIndex?: boolean; + image?: string; } const BASE_URL = 'https://sascha-bach.de'; -export function usePageSeo({ title, description, canonical, noIndex }: SeoProps) { +export function usePageSeo({ title, description, canonical, noIndex, image }: SeoProps) { useEffect(() => { document.title = title; @@ -30,6 +31,12 @@ export function usePageSeo({ title, description, canonical, noIndex }: SeoProps) setMeta('twitter:title', title); setMeta('twitter:description', description); + if (image) { + const imageUrl = image.startsWith('http') ? image : `${BASE_URL}${image}`; + setMeta('og:image', imageUrl, true); + setMeta('twitter:image', imageUrl); + } + if (canonical) { setMeta('og:url', `${BASE_URL}${canonical}`, true); diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 5d3d9b2..3e3746e 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -6,12 +6,18 @@ import CertificationsSection from '../components/sections/CertificationsSection' import ProjectsSection from '../components/sections/ProjectsSection'; import ContactSection from '../components/sections/ContactSection'; import { usePageSeo } from '../hooks/usePageSeo'; +import { useLanguage } from '../contexts/LanguageContext'; export default function HomePage() { + const { language } = useLanguage(); usePageSeo({ - title: 'Sascha Bach | Technical Portfolio – Skills, Services & Projects', - description: 'Explore the technical portfolio of Sascha Bach: services, skills, certifications, and projects in React, TypeScript, and accessible web development.', - canonical: '/technical', + title: language === 'de' + ? 'Sascha Bach | Technisches Portfolio – Skills, Services & Projekte' + : 'Sascha Bach | Technical Portfolio – Skills, Services & Projects', + description: language === 'de' + ? 'Das technische Portfolio von Sascha Bach: Services, Fähigkeiten, Zertifizierungen und Projekte in React, TypeScript und barrierefreier Webentwicklung.' + : 'Explore the technical portfolio of Sascha Bach: services, skills, certifications, and projects in React, TypeScript, and accessible web development.', + canonical: `/${language}/technical`, }); return ( diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index e9a7c1c..169b49a 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -1,3 +1,4 @@ +import HeroSection from '../components/sections/HeroSection'; import LandingAboutMeSection from '../components/landing/LandingAboutMeSection'; import ReasonsSection from '../components/landing/ReasonsSection'; import ExperienceSection from '../components/landing/ExperienceSection'; @@ -7,16 +8,31 @@ import ProjectsSection from '../components/sections/ProjectsSection'; import ReferencesSection from '../components/landing/ReferencesSection'; import ContactSection from '../components/sections/ContactSection'; import { usePageSeo } from '../hooks/usePageSeo'; +import { useLanguage } from '../contexts/LanguageContext'; export default function LandingPage() { + const { language, texts } = useLanguage(); + const t = texts.landingHero; usePageSeo({ - title: 'Sascha Bach | Freelance Software Developer – Accessible Web Development', - description: 'Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act.', - canonical: '/', + title: language === 'de' + ? 'Sascha Bach | Freelance Softwareentwickler – Barrierefreie Webentwicklung' + : 'Sascha Bach | Freelance Software Developer – Accessible Web Development', + description: language === 'de' + ? 'Freelance Softwareentwickler in Deutschland spezialisiert auf barrierefreie Webentwicklung. Ich baue inklusive Websites konform zum BFSG mit React, TypeScript und modernen Frameworks.' + : 'Freelance software developer in Germany specializing in accessible web development. Building inclusive websites compliant with the Accessibility Act.', + canonical: `/${language}/`, }); return ( <> + diff --git a/src/scss/sections/hero-section.scss b/src/scss/sections/hero-section.scss index f5f0c2c..852bff4 100644 --- a/src/scss/sections/hero-section.scss +++ b/src/scss/sections/hero-section.scss @@ -47,7 +47,7 @@ } &__container { - max-width: 800px; + max-width: 900px; width: 100%; text-align: center; @include flex-center; diff --git a/src/scss/sections/landing-hero-section.scss b/src/scss/sections/landing-hero-section.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/scrollUtils.ts b/src/utils/scrollUtils.ts index 6e2faff..54aa508 100644 --- a/src/utils/scrollUtils.ts +++ b/src/utils/scrollUtils.ts @@ -3,13 +3,30 @@ import type { NavigateFunction } from 'react-router-dom'; // Offset to account for sticky navbar height (65px) plus some padding const SCROLL_OFFSET = 65; +function isScrollablePath(pathname: string): boolean { + return ( + pathname === '/de/' || + pathname === '/en/' || + pathname === '/de/technical' || + pathname === '/en/technical' || + // legacy fallbacks + pathname === '/' || + pathname === '/technical' + ); +} + +/** Returns /de/technical or /en/technical depending on the current path prefix. */ +function getTechnicalPath(currentPath: string): string { + if (currentPath.startsWith('/en')) return '/en/technical'; + return '/de/technical'; +} + export function scrollToSection( sectionId: string, currentPath: string, navigate?: NavigateFunction ): void { - const isScrollablePage = currentPath === '/technical' || currentPath === '/'; - if (isScrollablePage) { + if (isScrollablePath(currentPath)) { const element = document.getElementById(sectionId); if (element) { const elementPosition = element.getBoundingClientRect().top; @@ -21,11 +38,9 @@ export function scrollToSection( }); } } else if (navigate) { - // If we're on another page, navigate to homepage with hash - navigate(`/technical#${sectionId}`); + navigate(`${getTechnicalPath(currentPath)}#${sectionId}`); } else { - // Fallback: direct navigation without router - globalThis.location.href = `/technical#${sectionId}`; + globalThis.location.href = `${getTechnicalPath(currentPath)}#${sectionId}`; } } diff --git a/vercel.json b/vercel.json index 3cdefcc..ce49b4c 100644 --- a/vercel.json +++ b/vercel.json @@ -3,13 +3,13 @@ "outputDirectory": "dist", "framework": "vite", "rewrites": [ - { - "source": "/privacy-policy", - "destination": "/privacy-policy.html" - }, - { - "source": "/(.*)", - "destination": "/index.html" - } + { "source": "/", "destination": "/de/" }, + { "source": "/technical", "destination": "/de/technical" }, + { "source": "/de/", "destination": "/de/index.html" }, + { "source": "/de/technical", "destination": "/de/technical/index.html" }, + { "source": "/en/", "destination": "/en/index.html" }, + { "source": "/en/technical", "destination": "/en/technical/index.html" }, + { "source": "/privacy-policy", "destination": "/privacy-policy.html" }, + { "source": "/(.*)", "destination": "/index.html" } ] } From ed093e8027315476efb621b777e834d94decb755 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 16 Apr 2026 21:19:03 +0200 Subject: [PATCH 12/19] Refactor theme styles for improved accessibility and consistency - Updated light and dark themes across contact, hero, projects, services, skills SCSS files to enhance color contrast for better readability. - Replaced gradients with solid colors where necessary to ensure AAA compliance. - Adjusted button and text colors for improved visibility and user experience. - Introduced a new logo SVG asset for branding consistency. - Modified global color variables to align with the new design scheme. --- src/assets/logo.svg | 14 + src/components/layout/Navigation.tsx | 3 +- src/config/locales/de/navigation.ts | 1 + src/config/locales/en/TextConfig.ts | 1 + src/config/locales/en/navigation.ts | 3 +- src/hooks/useScrollReveal.ts | 52 +--- src/scss/App.scss | 340 ++++++++++++++++------ src/scss/components/back-to-top.scss | 2 +- src/scss/globals.scss | 30 +- src/scss/language-toggle.scss | 19 +- src/scss/layout/footer.scss | 34 ++- src/scss/layout/topbar.scss | 42 ++- src/scss/sections/contact-section.scss | 150 +++++++--- src/scss/sections/experience-section.scss | 2 +- src/scss/sections/hero-section.scss | 58 ++-- src/scss/sections/imprint-page.scss | 2 +- src/scss/sections/reasons-section.scss | 12 +- src/scss/sections/references-section.scss | 2 +- src/scss/sections/skills-section.scss | 39 +-- src/scss/themes/_about.scss | 160 ++++------ src/scss/themes/_base.scss | 84 +++--- src/scss/themes/_certifications.scss | 79 ++--- src/scss/themes/_contact.scss | 195 +++++-------- src/scss/themes/_hero.scss | 103 ++----- src/scss/themes/_projects.scss | 97 ++---- src/scss/themes/_services.scss | 115 ++++---- src/scss/themes/_skills.scss | 306 +++++-------------- src/scss/variables.scss | 21 +- 28 files changed, 916 insertions(+), 1050 deletions(-) create mode 100644 src/assets/logo.svg diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..91a52fb --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + Sascha Bach + BARRIEREFREIE WEBENTWICKLUNG + diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index e7e6b45..cace1ed 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -4,6 +4,7 @@ import ThemeToggle from '../ThemeToggle'; import LanguageToggle from '../LanguageToggle'; import MobileMenu from './MobileMenu'; import { personalConfig } from '../../config/personal'; +import logoUrl from '../../assets/logo.svg'; import { useLanguage } from '../../contexts/LanguageContext'; import { scrollToSection } from '../../utils/scrollUtils'; import type { Theme } from '../../data/types'; @@ -74,7 +75,7 @@ export default function Navbar({ window.scrollTo({ top: 0, behavior: 'smooth' }); }} > - {personalConfig.name} + {texts.navigation.logoAlt}
{menuItems.map((item) => ( diff --git a/src/config/locales/de/navigation.ts b/src/config/locales/de/navigation.ts index 7544e09..6fdb667 100644 --- a/src/config/locales/de/navigation.ts +++ b/src/config/locales/de/navigation.ts @@ -17,4 +17,5 @@ export const navigation = { { section: 'Contact', label: 'Kontakt' }, ], mobileMenuAriaLabel: 'Menü öffnen', + logoAlt: 'Sascha Bach – Barrierefreie Webentwicklung', }; diff --git a/src/config/locales/en/TextConfig.ts b/src/config/locales/en/TextConfig.ts index 1894060..b81ec89 100644 --- a/src/config/locales/en/TextConfig.ts +++ b/src/config/locales/en/TextConfig.ts @@ -6,6 +6,7 @@ export interface TextConfig { menuItems: MenuItem[]; landingMenuItems: MenuItem[]; mobileMenuAriaLabel: string; + logoAlt: string; }; // Theme Toggle diff --git a/src/config/locales/en/navigation.ts b/src/config/locales/en/navigation.ts index e7c2cb8..cc50b97 100644 --- a/src/config/locales/en/navigation.ts +++ b/src/config/locales/en/navigation.ts @@ -16,5 +16,6 @@ export const navigation = { { section: 'References', label: 'References' }, { section: 'Contact', label: 'Contact' }, ], - mobileMenuAriaLabel: 'Menü öffnen', + mobileMenuAriaLabel: 'Open menu', + logoAlt: 'Sascha Bach – Accessible Web Development', }; diff --git a/src/hooks/useScrollReveal.ts b/src/hooks/useScrollReveal.ts index aba4dd8..bc761c3 100644 --- a/src/hooks/useScrollReveal.ts +++ b/src/hooks/useScrollReveal.ts @@ -5,9 +5,8 @@ import { useEffect, useRef } from 'react'; * `.is-visible` when they enter the viewport, triggering the CSS transition * defined in globals.scss. * - * The observer only starts after the first user interaction (scroll, keydown, - * or pointer event), so elements already in the viewport on load stay hidden - * until the user actually engages with the page. + * The observer starts immediately so that elements already in the viewport + * become visible right away — including after language-change remounts. */ export function useScrollReveal() { const ref = useRef(null); @@ -16,42 +15,23 @@ export function useScrollReveal() { const container = ref.current; if (!container) return; - let observer: IntersectionObserver | null = null; - - const startObserving = () => { - if (observer) return; // already started - - observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - entry.target.classList.add('is-visible'); - observer!.unobserve(entry.target); - } - }); - }, - { threshold: 0.12 } - ); - - const items = container.querySelectorAll('.reveal-item'); - items.forEach((el) => observer!.observe(el)); - - // Clean up interaction listeners once started - INTERACTION_EVENTS.forEach((event) => - window.removeEventListener(event, startObserving, { capture: true }) - ); - }; - - const INTERACTION_EVENTS = ['scroll', 'keydown', 'pointerdown', 'touchstart'] as const; - INTERACTION_EVENTS.forEach((event) => - window.addEventListener(event, startObserving, { once: false, capture: true, passive: true }) + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('is-visible'); + observer.unobserve(entry.target); + } + }); + }, + { threshold: 0.12 } ); + const items = container.querySelectorAll('.reveal-item'); + items.forEach((el) => observer.observe(el)); + return () => { - observer?.disconnect(); - INTERACTION_EVENTS.forEach((event) => - window.removeEventListener(event, startObserving, { capture: true }) - ); + observer.disconnect(); }; }, []); diff --git a/src/scss/App.scss b/src/scss/App.scss index 95cc589..43e303b 100644 --- a/src/scss/App.scss +++ b/src/scss/App.scss @@ -12,8 +12,7 @@ // Component styles @use 'components/back-to-top'; -// Import Geist font from Google Fonts or local files -@import url('https://fonts.googleapis.com/css2?family=Geist:wght@100;200;300;400;500;600;700;800;900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Comfortaa:wght@600&family=Quicksand:wght@400;500&display=swap'); // CSS Reset - Add this * { @@ -69,115 +68,292 @@ --radius-lg: 0.75rem; --radius-xl: 1rem; - // Shadows (light) - --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05); - --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06); - --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.08); - --shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.12); + // Shadows (light) – brand-tinted + --shadow-xs: 0 1px 2px rgba(4, 44, 83, 0.05); + --shadow-sm: 0 2px 8px rgba(4, 44, 83, 0.08); + --shadow-md: 0 4px 16px rgba(4, 44, 83, 0.08); + --shadow-hover: 0 8px 24px rgba(4, 44, 83, 0.12); // Transitions --transition-fast: 150ms ease; --transition-base: 200ms ease; --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); - // Landing section background gradients (light) – vivid & saturated - --bg-reasons: linear-gradient(160deg, #dbeafe 0%, #ede9fe 100%); - --bg-winnings: linear-gradient(160deg, #ede9fe 0%, #e0e7ff 100%); - --bg-experience: linear-gradient(160deg, #e0f2fe 0%, #ede9fe 100%); - --bg-processes: linear-gradient(160deg, #e0e7ff 0%, #dbeafe 100%); - --bg-references: linear-gradient(160deg, #ede9fe 0%, #dbeafe 100%); + // Brand accent & text-on-dark + --color-accent-text: #412402; + --color-accent-deco: #ef9f27; + --color-text-on-dark: #e6f1fb; + --font-headline: 'Comfortaa', sans-serif; + --font-body: 'Quicksand', sans-serif; + --contact-button-bg: #042c53; + --contact-button-text: #e6f1fb; + --contact-button-hover-bg: #0c447c; + + // Landing section backgrounds (light) – flat brand + --bg-reasons: #ffffff; + --bg-winnings: #f1efe8; + --bg-experience: #ffffff; + --bg-processes: #f1efe8; + --bg-references: #ffffff; // Reused section backgrounds (light) - --about-background: linear-gradient(160deg, #f0f9ff 0%, #dbeafe 100%); - --projects-background: linear-gradient(160deg, #dbeafe 0%, #ede9fe 100%); - --contact-background: linear-gradient(160deg, #ede9fe 0%, #e0f2fe 100%); + --about-background: #ffffff; + --projects-background: #ffffff; + --contact-background: #f1efe8; // Tech section backgrounds (light) - --hero-background: linear-gradient( - 160deg, - #f8faff 0%, - #f1f3ff 50%, - #f5f0ff 100% - ); - --services-background: linear-gradient(160deg, #ede9fe 0%, #dbeafe 100%); - --skills-background: linear-gradient(160deg, #dbeafe 0%, #e0f2fe 100%); - --certifications-background: linear-gradient( - 160deg, - #e0e7ff 0%, - #ede9fe 100% - ); + --hero-background: #f1efe8; + --services-background: #f1efe8; + --skills-background: #ffffff; + --certifications-background: #f1efe8; - // Glassmorphism card tokens (light) - --card-glass-bg: rgba(255, 255, 255, 0.65); - --card-glass-border: rgba(255, 255, 255, 0.9); + // Card tokens (light) + --card-glass-bg: #ffffff; + --card-glass-border: rgba(4, 44, 83, 0.12); - // Glow shadows (light) + // Functional shadows --shadow-glow: - 0 0 0 1px rgba(99, 102, 241, 0.12), 0 8px 24px rgba(99, 102, 241, 0.1); + 0 0 0 1px rgba(4, 44, 83, 0.1), 0 8px 24px rgba(4, 44, 83, 0.08); --shadow-glow-hover: - 0 0 0 1px rgba(99, 102, 241, 0.28), 0 16px 48px rgba(99, 102, 241, 0.2); + 0 0 0 1px rgba(4, 44, 83, 0.2), 0 16px 48px rgba(4, 44, 83, 0.15); } // Dark theme + shadow overrides [data-theme='dark'] { @include t.theme-vars(t.$dark-theme); - --shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.35); + // Core colors + --color-primary: #e6f1fb; + --color-secondary: #b5d4f4; + --color-accent-text: #fac775; + --color-accent-deco: #ef9f27; + --color-background: #021829; + --color-surface: #042c53; + --color-text: #e6f1fb; + --color-text-muted: #b5d4f4; + --color-text-on-dark: #e6f1fb; + --color-border: rgba(230, 241, 251, 0.12); + --color-focus-ring: #ef9f27; + + // Section backgrounds + --hero-background: #021829; + --about-background: #042c53; + --services-background: #021829; + --skills-background: #042c53; + --certifications-background: #021829; + --projects-background: #042c53; + --contact-background: #021829; + --bg-reasons: #042c53; + --bg-winnings: #021829; + --bg-experience: #042c53; + --bg-processes: #021829; + --bg-references: #042c53; + + // Cards + shadows + --card-glass-bg: rgba(12, 68, 124, 0.5); + --card-glass-border: rgba(230, 241, 251, 0.1); + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4); + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4); --shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.5); - - // Landing section backgrounds (dark) – cinematic deep - --bg-reasons: linear-gradient(160deg, #0f172a 0%, #1a0f2e 100%); - --bg-winnings: linear-gradient(160deg, #1a0f2e 0%, #0d1a2e 100%); - --bg-experience: linear-gradient(160deg, #0d1a2e 0%, #0f172a 100%); - --bg-processes: linear-gradient(160deg, #0f172a 0%, #190b2e 100%); - --bg-references: linear-gradient(160deg, #190b2e 0%, #0f172a 100%); - - // Reused section backgrounds (dark) - --about-background: linear-gradient(160deg, #111827 0%, #0f172a 100%); - --projects-background: linear-gradient(160deg, #0f172a 0%, #1a0f2e 100%); - --contact-background: linear-gradient(160deg, #1a0f2e 0%, #0d1a2e 100%); - - // Tech section backgrounds (dark) – full cinematic - --hero-background: linear-gradient( - 160deg, - #06080f 0%, - #0c1220 50%, - #120720 100% - ); - --services-background: linear-gradient(160deg, #190b2e 0%, #0f172a 100%); - --skills-background: linear-gradient(160deg, #0f172a 0%, #0d1a2e 100%); - --certifications-background: linear-gradient( - 160deg, - #0d1a2e 0%, - #190b2e 100% - ); - - // Glassmorphism card tokens (dark) - --card-glass-bg: rgba(139, 92, 246, 0.06); - --card-glass-border: rgba(139, 92, 246, 0.22); - - // Glow shadows (dark) --shadow-glow: - 0 0 0 1px rgba(139, 92, 246, 0.2), 0 8px 32px rgba(139, 92, 246, 0.18); + 0 0 0 1px rgba(239, 159, 39, 0.15), 0 8px 24px rgba(0, 0, 0, 0.25); --shadow-glow-hover: - 0 0 0 1px rgba(139, 92, 246, 0.45), 0 16px 56px rgba(139, 92, 246, 0.32); + 0 0 0 1px rgba(239, 159, 39, 0.3), 0 16px 48px rgba(0, 0, 0, 0.4); + + // Buttons + --contact-button-bg: #ef9f27; + --contact-button-text: #042c53; + --contact-button-hover-bg: #fac775; + + // Form inputs + --contact-input-bg: #042c53; + --contact-input-border: rgba(230, 241, 251, 0.2); + --contact-input-focus-ring: rgba(239, 159, 39, 0.3); + + // Stat cards + --stat-primary-bg: rgba(12, 68, 124, 0.4); + --stat-primary-value: #e6f1fb; + --stat-primary-label: #b5d4f4; + --stat-secondary-bg: rgba(12, 68, 124, 0.4); + --stat-secondary-value: #e6f1fb; + --stat-secondary-label: #b5d4f4; + --stat-tertiary-bg: rgba(12, 68, 124, 0.4); + --stat-tertiary-value: #e6f1fb; + --stat-tertiary-label: #b5d4f4; + --stat-quaternary-bg: rgba(12, 68, 124, 0.4); + --stat-quaternary-value: #e6f1fb; + --stat-quaternary-label: #b5d4f4; + + // Service cards + --service-card-primary-bg: rgba(12, 68, 124, 0.5); + --service-card-secondary-bg: rgba(4, 44, 83, 0.8); + --service-card-tertiary-bg: rgba(12, 68, 124, 0.5); + --service-card-quaternary-bg: rgba(4, 44, 83, 0.8); + --service-card-quinary-bg: rgba(12, 68, 124, 0.5); + --service-card-senary-bg: rgba(4, 44, 83, 0.8); + --service-icon-primary: #ef9f27; + --service-icon-secondary: #ef9f27; + --service-icon-tertiary: #ef9f27; + --service-icon-quaternary: #ef9f27; + --service-icon-quinary: #ef9f27; + --service-icon-senary: #ef9f27; + --service-title-primary: #e6f1fb; + --service-title-secondary: #e6f1fb; + --service-title-tertiary: #e6f1fb; + --service-title-quaternary: #e6f1fb; + --service-title-quinary: #e6f1fb; + --service-title-senary: #e6f1fb; + --service-description-primary: #b5d4f4; + --service-description-secondary: #b5d4f4; + --service-description-tertiary: #b5d4f4; + --service-description-quaternary: #b5d4f4; + --service-description-quinary: #b5d4f4; + --service-description-senary: #b5d4f4; + + // Skills + --skills-category-bg: rgba(12, 68, 124, 0.4); + --skills-category-border: rgba(230, 241, 251, 0.1); + --skills-skill-name-color: #e6f1fb; + --skills-skill-level-color: #b5d4f4; + --skills-skill-percentage-color: #b5d4f4; + --skills-progress-bg: rgba(230, 241, 251, 0.1); + --skills-category-bg-primary: rgba(12, 68, 124, 0.5); + --skills-category-bg-secondary: rgba(12, 68, 124, 0.5); + --skills-category-bg-tertiary: rgba(12, 68, 124, 0.5); + --skills-category-bg-quaternary: rgba(12, 68, 124, 0.5); + --skills-category-bg-quinary: rgba(12, 68, 124, 0.5); + --skills-category-bg-senary: rgba(12, 68, 124, 0.5); + --skills-progress-primary: #ef9f27; + --skills-progress-secondary: #ef9f27; + --skills-progress-tertiary: #ef9f27; + --skills-progress-quaternary: #ef9f27; + --skills-progress-quinary: #ef9f27; + --skills-progress-senary: #ef9f27; + + // Certifications + --certifications-card-background: #042c53; + --certifications-badge-background: rgba(239, 159, 39, 0.15); + --certifications-badge-border: rgba(239, 159, 39, 0.3); + --color-certifications-badge-text: #fac775; + + // Projects + --projects-card-background: #042c53; + --projects-card-border: rgba(239, 159, 39, 0.15); + --projects-button-primary-bg: #ef9f27; + --projects-button-primary-text: #042c53; + --projects-button-primary-border: rgba(239, 159, 39, 0.3); + --projects-button-primary-hover-bg: #fac775; + --projects-button-secondary-bg: #0c447c; + --projects-button-secondary-text: #e6f1fb; + --projects-button-secondary-border: rgba(230, 241, 251, 0.15); + --projects-button-secondary-hover-bg: #185fa5; + --projects-tech-badge-bg: rgba(12, 68, 124, 0.6); + --projects-tech-badge-text: #e6f1fb; + --projects-tech-badge-border: rgba(239, 159, 39, 0.2); + + // Contact + --contact-social-bg: #042c53; + --contact-social-text: #e6f1fb; + --contact-social-border: rgba(239, 159, 39, 0.3); + --contact-social-hover-bg: #0c447c; + --contact-social-hover-border: #ef9f27; + --contact-status-success-bg: rgba(4, 44, 83, 0.5); + --contact-status-success-border: #ef9f27; + --contact-status-success-text: #fac775; + + // Page backgrounds + --bg-primary: #021829; + --bg-secondary: #042c53; + --border-color: rgba(230, 241, 251, 0.12); + + // Skill badges + --skill-badge-primary-bg: rgba(12, 68, 124, 0.5); + --skill-badge-primary-text: #e6f1fb; + --skill-badge-secondary-bg: rgba(12, 68, 124, 0.5); + --skill-badge-secondary-text: #e6f1fb; + --skill-badge-tertiary-bg: rgba(12, 68, 124, 0.5); + --skill-badge-tertiary-text: #e6f1fb; + --skill-badge-quaternary-bg: rgba(65, 36, 2, 0.4); + --skill-badge-quaternary-text: #fac775; + + // Feature cards + --feature-card-primary-bg: rgba(12, 68, 124, 0.4); + --feature-card-secondary-bg: rgba(4, 44, 83, 0.6); + --feature-card-tertiary-bg: rgba(12, 68, 124, 0.4); + --feature-icon-primary: #ef9f27; + --feature-icon-secondary: #ef9f27; + --feature-icon-tertiary: #ef9f27; + --feature-title-primary: #e6f1fb; + --feature-title-secondary: #e6f1fb; + --feature-title-tertiary: #e6f1fb; + --feature-description-primary: #b5d4f4; + --feature-description-secondary: #b5d4f4; + --feature-description-tertiary: #b5d4f4; + + // Remove gradients + --gradient-primary: #ef9f27; +} + +// Dark mode: override hardcoded SASS-compiled colors on section headings +[data-theme='dark'] { + h1, + h2, + h3, + h4 { + color: #e6f1fb; + } + + .about-section__greeting { + color: #e6f1fb; + } + + .skills-section__category-title { + color: #e6f1fb; + } + + // Border-left accent colors + .reasons-section__item, + .references-section__item, + .experience-section__paragraph--outro, + .reasons-section__outro { + border-left-color: #ef9f27; + } + + .winnings-section__item, + .processes-section__item { + border-left-color: #b5d4f4; + } + + // Stat numbers in reasons section + .reasons-section__stat { + color: #ef9f27; + } + + // Navbar deepens in dark mode + .navbar { + background: #021829; + border-bottom: 1px solid rgba(230, 241, 251, 0.08); + } } body { - font-family: - 'Geist', - system-ui, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - 'Roboto', - sans-serif; + font-family: 'Quicksand', sans-serif; + font-weight: 400; + font-size: 1rem; background-color: var(--color-background); color: var(--color-text); - margin: 0; // Explicitly remove body margin - padding: 0; // Explicitly remove body padding + margin: 0; + padding: 0; +} + +h1, +h2, +h3, +h4 { + font-family: 'Comfortaa', sans-serif; + font-weight: 600; + color: var(--color-primary); } section { diff --git a/src/scss/components/back-to-top.scss b/src/scss/components/back-to-top.scss index 6036915..ebdbaab 100644 --- a/src/scss/components/back-to-top.scss +++ b/src/scss/components/back-to-top.scss @@ -9,7 +9,7 @@ height: 3rem; border-radius: 50%; background: var(--color-primary); - color: #000000; + color: var(--color-text-on-dark); // #E6F1FB – white on dark navy border: none; cursor: pointer; display: flex; diff --git a/src/scss/globals.scss b/src/scss/globals.scss index af950de..32e6d0b 100644 --- a/src/scss/globals.scss +++ b/src/scss/globals.scss @@ -5,21 +5,25 @@ // Global CSS custom properties for accessibility :root { - --color-primary: #2563eb; - --color-secondary: #60a5fa; - --color-focus-ring: #2563eb; - --color-text-aaa-light: #111827; - --color-text-aaa-dark: #ffffff; - --box-shadow-hover: rgba(0, 0, 0, 0.15); + --color-primary: #042c53; + --color-secondary: #0c447c; + --color-accent-text: #412402; + --color-accent-deco: #ef9f27; + --color-text-on-dark: #e6f1fb; + --color-focus-ring: #042c53; + --color-text-aaa-light: #1a1a1a; + --color-text-aaa-dark: #e6f1fb; + --box-shadow-hover: rgba(4, 44, 83, 0.15); + --font-headline: 'Comfortaa', sans-serif; + --font-body: 'Quicksand', sans-serif; + --contact-button-bg: #042c53; + --contact-button-text: #e6f1fb; + --contact-button-hover-bg: #0c447c; } -@media (prefers-color-scheme: dark) { - :root { - --color-primary: #60a5fa; - --color-secondary: #93c5fd; - --color-focus-ring: #60a5fa; - } -} +// Dark mode is controlled via [data-theme='dark'] attribute on . +// OS-level prefers-color-scheme is intentionally NOT used here to avoid +// --color-primary flipping to #E6F1FB, which breaks the always-dark navbar. // Screen Reader Only Classes - Central Definition .sr-only { diff --git a/src/scss/language-toggle.scss b/src/scss/language-toggle.scss index 91ef1d8..ce5ffba 100644 --- a/src/scss/language-toggle.scss +++ b/src/scss/language-toggle.scss @@ -5,7 +5,7 @@ display: flex; gap: 0.25rem; background: transparent; - border: 1px solid var(--color-text-muted); + border: 1px solid rgba(230, 241, 251, 0.35); // light on always-dark navbar border-radius: $border-radius-sm; padding: 0.25rem; @@ -16,33 +16,32 @@ border: none; border-radius: 0.25rem; background: transparent; - color: var(--color-text); + color: var(--color-text-on-dark); // #E6F1FB – 10:1 on #042C53 navbar cursor: pointer; transition: all 0.2s ease; - @extend %hover-lift; &:hover { - color: var(--color-text); + background: rgba(230, 241, 251, 0.1); + color: var(--color-text-on-dark); } &:focus { - outline: 2px solid var(--color-primary); + outline: 2px solid var(--color-accent-deco); // ochre – always visible on dark outline-offset: 2px; } &:active { - box-shadow: 0 2px 4px var(--box-shadow-active); transform: translateY(0); } &--active { - background: var(--color-primary); - color: #000000; + background: var(--color-text-on-dark); // #E6F1FB bg → 10:1 contrast + color: #042c53; // night-blue text on light bg → AAA font-weight: 600; &:hover { - background: var(--color-primary); - color: #000000; + background: var(--color-text-on-dark); + color: #042c53; } } } diff --git a/src/scss/layout/footer.scss b/src/scss/layout/footer.scss index 7cc16fb..60d5c20 100644 --- a/src/scss/layout/footer.scss +++ b/src/scss/layout/footer.scss @@ -2,18 +2,17 @@ @use '../variables'; .footer { - background-color: var(--color-background-muted); - margin-top: auto; // Push footer to bottom if using flexbox layout + background-color: var(--color-surface); + border-top: 1px solid var(--color-border); + margin-top: auto; &__container { margin: 0 auto; - padding: 0 1rem; + padding: 2rem 1rem 1.5rem; } &__separator { - height: 1px; - background-color: var(--color-border); - margin-bottom: 2rem; + display: none; // border-top on .footer replaces this } &__content { @@ -38,7 +37,7 @@ } &__link { - color: var(--color-text-muted); + color: var(--color-text); font-size: 0.875rem; text-decoration: none; padding: 0.5rem; @@ -46,21 +45,22 @@ transition: all 0.2s ease-in-out; &:hover { - color: var(--color-text); - background-color: var(--color-background-hover); + color: var(--color-primary); + text-decoration: underline; } &:focus { - outline: 2px solid var(--color-primary); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } } &__copyright { - color: var(--color-text-muted); + color: var(--color-text); font-size: 0.875rem; text-align: center; margin: 0; + opacity: 0.7; } &__social { @@ -75,20 +75,22 @@ width: 2.5rem; height: 2.5rem; background: transparent; - border: none; + border: 1px solid var(--color-border); border-radius: 0.375rem; - color: var(--color-text-muted); + color: var(--color-text); cursor: pointer; transition: all 0.2s ease-in-out; + text-decoration: none; &:hover { - background-color: var(--color-background-hover); - color: var(--color-text); + background-color: var(--color-primary); + color: var(--color-text-on-dark); + border-color: var(--color-primary); transform: translateY(-1px); } &:focus { - outline: 2px solid var(--color-primary); + outline: 2px solid var(--color-focus-ring); outline-offset: 2px; } diff --git a/src/scss/layout/topbar.scss b/src/scss/layout/topbar.scss index 378ed23..79c0e3b 100644 --- a/src/scss/layout/topbar.scss +++ b/src/scss/layout/topbar.scss @@ -4,21 +4,32 @@ @include globals.flex-center(); justify-content: space-between; padding: 1em; - background: var(--color-background); - color: var(--color-text); + background: #042c53; // always night-blue – independent of --color-primary cascade + color: var(--color-text-on-dark); height: 65px; position: sticky; top: 0; z-index: 100; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 8px rgba(4, 44, 83, 0.2); &__name { font-weight: bold; font-size: 1.25rem; - color: var(--color-text); + color: var(--color-text-on-dark); text-decoration: none; } + &__logo { + height: 65px; + max-height: calc(65px - 0.5rem); // stay within navbar height minus padding + width: auto; + display: block; + + @media (max-width: 768px) { + height: 48px; + } + } + &__container { display: flex; align-items: center; @@ -29,18 +40,31 @@ @extend %hover-lift; padding: 0.6rem 1.2rem; border-radius: 8px; + color: var(--color-text-on-dark); transition: + color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; + &:hover { + color: var(--color-accent-deco); + box-shadow: none; + transform: none; + } + &:active { - box-shadow: 0 2px 4px var(--box-shadow-active); + box-shadow: none; transform: translateY(0); } &--active { - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); - background: var(--box-shadow-hover, rgba(0, 0, 0, 0.05)); + color: var( + --color-text-on-dark + ); // #E6F1FB – 10:1 contrast, same as default + box-shadow: none; + background: rgba(230, 241, 251, 0.12); + border-bottom: 2px solid var(--color-accent-deco); // ochre underline as accent indicator + border-radius: 8px 8px 4px 4px; } } } @@ -60,14 +84,14 @@ display: block; height: 3px; width: 100%; - background-color: var(--color-text); + background-color: var(--color-text-on-dark); // light on dark navy bg border-radius: 2px; transition: all 0.3s ease; margin: 1px 0; } &:hover &__item { - background-color: var(--color-primary); + background-color: var(--color-accent-deco); // ochre accent on hover } &.active { diff --git a/src/scss/sections/contact-section.scss b/src/scss/sections/contact-section.scss index 4e82256..507fb4b 100644 --- a/src/scss/sections/contact-section.scss +++ b/src/scss/sections/contact-section.scss @@ -85,8 +85,10 @@ &__email-icon { width: 2rem; height: 2rem; - color: var(--primary-color); - background: rgba(147, 51, 234, 0.1); + color: var( + --color-primary + ); // #042C53 light / #E6F1FB dark – high contrast in both + background: rgba(4, 44, 83, 0.08); border-radius: 0.75rem; padding: 0.5rem; flex-shrink: 0; @@ -225,7 +227,11 @@ &__status-text { font-size: 0.875rem; font-weight: 500; - color: #22c55e; + color: #065f46; // dark emerald – 7.4:1 on white; light-mode accessible + + [data-theme='dark'] & { + color: #34d399; // light mint – high contrast on dark bg + } } &__quick-facts { @@ -351,17 +357,10 @@ outline-offset: 2px; } - // Brand-specific icon colors with better contrast - &--email .contact-section__social-icon { - color: var(--social-icon-email, #2563eb); // Use theme variable - } - - &--github .contact-section__social-icon { - color: var(--social-icon-github, #1f2937); // Use theme variable - } - - &--linkedin .contact-section__social-icon { - color: var(--social-icon-linkedin, #0d67b5); // Use theme variable + // Icons inherit color from parent button's var(--contact-social-text) + // which is #042C53 (light) / #E6F1FB (dark) – both high contrast + .contact-section__social-icon { + color: currentColor; } } @@ -578,7 +577,7 @@ &__email-card-icon { width: 3rem; height: 3rem; - color: var(--primary-color); + color: var(--color-primary); // #042C53 light / #E6F1FB dark – high contrast margin: 0 auto 1rem; display: block; } @@ -599,39 +598,19 @@ margin: var(--card-padding-sm) 0; text-decoration: none; color: var(--contact-button-text); - box-shadow: - 0 4px 14px 0 rgba(37, 99, 235, 0.25), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); + box-shadow: none; width: 100%; position: relative; overflow: hidden; &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.2), - transparent - ); - transition: left 0.5s ease; + display: none; } &:hover { background: var(--contact-button-hover-bg); transform: translateY(-2px); - box-shadow: - 0 8px 25px 0 rgba(37, 99, 235, 0.35), - 0 4px 6px -2px rgba(0, 0, 0, 0.1); - - &::before { - left: 100%; - } + box-shadow: none; } &:active { @@ -661,7 +640,7 @@ line-height: 1.5; strong { - color: var(--primary-color); + color: var(--color-primary); font-weight: 600; } } @@ -753,6 +732,99 @@ } } +// ── Dark mode explicit overrides ───────────────────────────────────────────── +// CSS vars can silently fall back to light values in certain cascade scenarios. +// These scoped rules guarantee correct dark appearance regardless. +[data-theme='dark'] { + .contact-section { + // Card / form backgrounds + &__connect-card, + &__social-card, + &__info-card, + &__email-card, + &__form-card { + background: #042c53; + border-color: rgba(230, 241, 251, 0.12); + } + + // Email button – ochre fill, navy text + &__email-button { + background: #ef9f27; + color: #042c53; + + &:hover { + background: #fac775; + color: #042c53; + } + } + + // Form submit button + &__form-button { + background: #ef9f27; + color: #042c53; + + &:hover:not(:disabled) { + background: #fac775; + } + } + + // Social list icons (Globe, Linkedin) and links + &__social-icon { + color: #e6f1fb; + } + + &__detail-item { + color: #e6f1fb; + } + + &__detail-link { + color: #e6f1fb; + + &:hover { + text-decoration-color: #e6f1fb; + } + } + + // Appointment hint block + &__appointment-hint { + background: rgba(12, 68, 124, 0.5); + border-color: rgba(230, 241, 251, 0.12); + } + + &__appointment-icon, + &__appointment-link, + &__appointment-text { + color: #e6f1fb; + } + + // Email icon in header + &__email-icon { + color: #ef9f27; + background: rgba(239, 159, 39, 0.15); + } + + // Titles within cards (compiled from $color-heading) + &__email-title, + &__connect-title, + &__social-title, + &__form-title { + color: #e6f1fb; + } + + // Inputs + &__form-input, + &__form-textarea { + background: #021829; + color: #e6f1fb; + border-color: rgba(230, 241, 251, 0.2); + + &::placeholder { + color: #b5d4f4; + } + } + } +} + // High contrast mode support @media (prefers-contrast: high) { .contact-section__social-button { diff --git a/src/scss/sections/experience-section.scss b/src/scss/sections/experience-section.scss index fb72c87..1ce35b7 100644 --- a/src/scss/sections/experience-section.scss +++ b/src/scss/sections/experience-section.scss @@ -68,7 +68,7 @@ padding: var(--space-5) var(--space-6); border-radius: var(--radius-xl); border: 1px solid var(--card-glass-border); - border-left: 4px solid var(--color-primary); + border-left: 4px solid var(--color-secondary); box-shadow: var(--shadow-glow); } } diff --git a/src/scss/sections/hero-section.scss b/src/scss/sections/hero-section.scss index 852bff4..bf55578 100644 --- a/src/scss/sections/hero-section.scss +++ b/src/scss/sections/hero-section.scss @@ -23,26 +23,38 @@ background: var(--hero-background); position: relative; - // Subtle radial glow at center for cinematic depth + // Mid-Century geometric circle decoration &::before { content: ''; position: absolute; - inset: 0; - background: radial-gradient( - ellipse 80% 60% at 50% 40%, - rgba(99, 102, 241, 0.12) 0%, - transparent 70% - ); + top: -80px; + right: -80px; + width: 300px; + height: 300px; + border-radius: 50%; + border: 2px solid rgba(12, 68, 124, 0.15); + pointer-events: none; + } + + &::after { + content: ''; + position: absolute; + bottom: 40px; + left: -60px; + width: 180px; + height: 180px; + border-radius: 50%; + background: rgba(239, 159, 39, 0.08); pointer-events: none; } [data-theme='dark'] & { &::before { - background: radial-gradient( - ellipse 80% 60% at 50% 40%, - rgba(139, 92, 246, 0.22) 0%, - transparent 70% - ); + border-color: rgba(230, 241, 251, 0.1); + } + + &::after { + background: rgba(239, 159, 39, 0.05); } } @@ -86,27 +98,31 @@ &--primary, &--tertiary { - background: var(--gradient-primary); - color: var(--color-background); + background: var(--color-primary); + color: var(--color-text-on-dark); + font-family: var(--font-headline); + font-weight: 600; + border-radius: 8px; &:hover { + background: var(--color-secondary); + box-shadow: none; + filter: none; transform: translateY(-1px); - box-shadow: var(--box-shadow-sm); - filter: brightness(1.1); } } &--secondary { background: transparent; - color: var(--color-text); - border: 2px solid $color-primary; + color: var(--color-primary); + border: 2px solid var(--color-primary); padding: calc(#{$spacing-sm} - 2px) $spacing-xl; &:hover { - background: $color-primary; - color: var(--color-background); + background: var(--color-primary); + color: var(--color-text-on-dark); transform: translateY(-1px); - box-shadow: var(--box-shadow-sm); + box-shadow: none; } } } diff --git a/src/scss/sections/imprint-page.scss b/src/scss/sections/imprint-page.scss index 69f5153..2a22af4 100644 --- a/src/scss/sections/imprint-page.scss +++ b/src/scss/sections/imprint-page.scss @@ -149,7 +149,7 @@ margin-bottom: 1rem; padding: 1rem; background: var(--bg-primary); - border-left: 4px solid var(--color-primary); + border-left: 4px solid var(--color-secondary); border-radius: 0 0.5rem 0.5rem 0; font-weight: 500; text-decoration: underline; diff --git a/src/scss/sections/reasons-section.scss b/src/scss/sections/reasons-section.scss index c72d177..4b5d5b0 100644 --- a/src/scss/sections/reasons-section.scss +++ b/src/scss/sections/reasons-section.scss @@ -80,7 +80,7 @@ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--card-glass-border); - border-left: 3px solid var(--color-primary); + border-left: 3px solid var(--color-secondary); border-radius: var(--radius-lg); padding: var(--space-4) var(--space-5); box-shadow: var(--shadow-sm); @@ -102,10 +102,16 @@ &__stat { font-size: var(--font-size-lg); font-weight: 800; - color: var(--color-primary); + color: $color-heading; // compiled literal – immune to CSS var cascade issues line-height: var(--leading-normal); white-space: nowrap; letter-spacing: var(--tracking-tight); + + [data-theme='dark'] & { + color: var( + --color-text-on-dark + ); // #E6F1FB – always defined in globals, never overridden + } } &__text { @@ -127,7 +133,7 @@ padding: var(--space-5) var(--space-6); border-radius: var(--radius-xl); border: 1px solid var(--card-glass-border); - border-left: 4px solid var(--color-primary); + border-left: 4px solid var(--color-secondary); box-shadow: var(--shadow-glow); } } diff --git a/src/scss/sections/references-section.scss b/src/scss/sections/references-section.scss index a7e3a6a..a0c67b5 100644 --- a/src/scss/sections/references-section.scss +++ b/src/scss/sections/references-section.scss @@ -58,7 +58,7 @@ backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); border: 1px solid var(--card-glass-border); - border-left: 3px solid var(--color-primary); + border-left: 3px solid var(--color-secondary); border-radius: var(--radius-lg); padding: var(--space-6); box-shadow: var(--shadow-sm); diff --git a/src/scss/sections/skills-section.scss b/src/scss/sections/skills-section.scss index 84155f5..8f8acc2 100644 --- a/src/scss/sections/skills-section.scss +++ b/src/scss/sections/skills-section.scss @@ -358,67 +358,34 @@ transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1); position: relative; + // Shimmer disabled for flat design &::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient( - 90deg, - transparent, - rgba(255, 255, 255, 0.4), - transparent - ); - transform: translateX(-100%); - animation: shimmer 2s infinite; + display: none; } - // Semantic hierarchy classes with enhanced gradients + // Semantic hierarchy classes — flat brand colors via theme vars &--primary { background: var(--skills-progress-primary); - box-shadow: 0 0 8px rgba(59, 130, 246, 0.3); } &--secondary { background: var(--skills-progress-secondary); - box-shadow: 0 0 8px rgba(16, 185, 129, 0.3); } &--tertiary { background: var(--skills-progress-tertiary); - box-shadow: 0 0 8px rgba(139, 92, 246, 0.3); } &--quaternary { background: var(--skills-progress-quaternary); - box-shadow: 0 0 8px rgba(6, 182, 212, 0.3); } &--quinary { background: var(--skills-progress-quinary); - box-shadow: 0 0 8px rgba(245, 158, 11, 0.3); } &--senary { background: var(--skills-progress-senary); - box-shadow: 0 0 8px rgba(239, 68, 68, 0.3); } } } - -// Enhanced shimmer animation -@keyframes shimmer { - 0% { - transform: translateX(-100%); - opacity: 0; - } - 50% { - opacity: 1; - } - 100% { - transform: translateX(100%); - opacity: 0; - } -} diff --git a/src/scss/themes/_about.scss b/src/scss/themes/_about.scss index 59a314e..3aa3f4a 100644 --- a/src/scss/themes/_about.scss +++ b/src/scss/themes/_about.scss @@ -64,119 +64,63 @@ $text-aaa-light: #111827; // Near black for AAA contrast on light backgrounds $text-aaa-dark: #ffffff; // Pure white for AAA contrast on dark backgrounds $about-light-theme: ( - // About section colors - about-background: - linear-gradient(135deg, $about-bg-light-start 0%, $about-bg-light-end 100%), - gradient-about-title: linear-gradient(90deg, $green-primary, $teal-primary), - about-greeting-color: $green-secondary, - gradient-image-overlay: - linear-gradient( - 45deg, - rgba($blue-primary, $alpha-overlay), - rgba($purple-primary, $alpha-overlay) - ), - // Skill badge colors - FIXED for AAA contrast - skill-badge-primary-bg: $blue-lightest, - skill-badge-primary-text: $text-aaa-light, - // Changed from $blue-tertiary for AAA contrast - skill-badge-secondary-bg: $green-ultralight, - skill-badge-secondary-text: $text-aaa-light, - // Changed from $green-tertiary for AAA contrast - skill-badge-tertiary-bg: $purple-lightest, - skill-badge-tertiary-text: $text-aaa-light, - // Changed from $orange-tertiary for AAA contrast - skill-badge-quaternary-bg: $orange-lighter, - skill-badge-quaternary-text: $text-aaa-light, + about-background: #ffffff, + gradient-about-title: #042c53, + about-greeting-color: #042c53, + gradient-image-overlay: rgba(4, 44, 83, 0.1), + // Skill badge colors + skill-badge-primary-bg: #e6f1fb, + skill-badge-primary-text: #042c53, + skill-badge-secondary-bg: #f1efe8, + skill-badge-secondary-text: #042c53, + skill-badge-tertiary-bg: #e6f1fb, + skill-badge-tertiary-text: #042c53, + skill-badge-quaternary-bg: #faeeda, + skill-badge-quaternary-text: #412402, - // Changed from $orange-secondary for AAA contrast - // Feature card colors - FIXED for AAA contrast - feature-card-primary-bg: - linear-gradient(135deg, $blue-lightest, $blue-ultralight), - feature-card-secondary-bg: - linear-gradient(135deg, $purple-lightest, $purple-ultralight), - feature-card-tertiary-bg: - linear-gradient(135deg, $teal-lightest, $teal-lighter), - feature-icon-primary: $blue-primary, - feature-icon-secondary: $purple-primary, - feature-icon-tertiary: $teal-primary, + // Feature card colors + feature-card-primary-bg: #f1efe8, + feature-card-secondary-bg: #ffffff, + feature-card-tertiary-bg: #f1efe8, + feature-icon-primary: #042c53, + feature-icon-secondary: #0c447c, + feature-icon-tertiary: #ef9f27, - // Text colors - FIXED for AAA contrast - feature-title-primary: $text-aaa-light, - // Changed from $blue-tertiary for AAA contrast - feature-title-secondary: $text-aaa-light, - // Changed from $purple-tertiary for AAA contrast - feature-title-tertiary: $text-aaa-light, - - // Changed from $teal-tertiary for AAA contrast - feature-description-primary: $text-aaa-light, - // Changed from $blue-secondary for AAA contrast - feature-description-secondary: $text-aaa-light, - // Changed from $purple-secondary for AAA contrast - feature-description-tertiary: $text-aaa-light - // Changed from $teal-dark for AAA contrast + feature-title-primary: #042c53, + feature-title-secondary: #042c53, + feature-title-tertiary: #042c53, + feature-description-primary: #1a1a1a, + feature-description-secondary: #1a1a1a, + feature-description-tertiary: #1a1a1a, ); $about-dark-theme: ( - // About section colors - about-background: - linear-gradient(135deg, $about-bg-dark-start 0%, $about-bg-dark-end 100%), - gradient-about-title: linear-gradient(90deg, $green-light, $teal-secondary), - about-greeting-color: $green-lighter, - gradient-image-overlay: - linear-gradient( - 45deg, - rgba($blue-primary, $alpha-overlay), - rgba($purple-primary, $alpha-overlay) - ), - // Skill badge colors - FIXED for AAA contrast - skill-badge-primary-bg: rgba($blue-dark, $alpha-bg-low), - skill-badge-primary-text: $text-aaa-dark, - // Changed from $blue-lighter for AAA contrast - skill-badge-secondary-bg: rgba($green-dark, $alpha-bg-low), - skill-badge-secondary-text: $text-aaa-dark, - // Changed from $green-lightest for AAA contrast - skill-badge-tertiary-bg: rgba($purple-dark, $alpha-bg-low), - skill-badge-tertiary-text: $text-aaa-dark, - // Changed from $purple-lighter for AAA contrast - skill-badge-quaternary-bg: rgba($orange-darker, $alpha-bg-low), - skill-badge-quaternary-text: $text-aaa-dark, + about-background: #0c447c, + gradient-about-title: #e6f1fb, + about-greeting-color: #e6f1fb, + gradient-image-overlay: rgba(230, 241, 251, 0.1), + // Skill badge colors (dark) + skill-badge-primary-bg: rgba(230, 241, 251, 0.15), + skill-badge-primary-text: #e6f1fb, + skill-badge-secondary-bg: rgba(230, 241, 251, 0.1), + skill-badge-secondary-text: #e6f1fb, + skill-badge-tertiary-bg: rgba(230, 241, 251, 0.15), + skill-badge-tertiary-text: #e6f1fb, + skill-badge-quaternary-bg: rgba(239, 159, 39, 0.15), + skill-badge-quaternary-text: #ef9f27, - // Changed from $orange-light for AAA contrast - // Feature card colors - Enhanced for better contrast - feature-card-primary-bg: - linear-gradient( - 135deg, - rgba($blue-dark, $alpha-bg-low), - rgba($blue-darker, $alpha-bg-low) - ), - feature-card-secondary-bg: - linear-gradient( - 135deg, - rgba($purple-dark, $alpha-bg-low), - rgba($purple-darker, $alpha-bg-low) - ), - feature-card-tertiary-bg: - linear-gradient( - 135deg, - rgba($teal-dark, $alpha-bg-low), - rgba($teal-darker, $alpha-bg-low) - ), - feature-icon-primary: $blue-light, - feature-icon-secondary: $purple-light, - feature-icon-tertiary: $teal-secondary, + // Feature card colors (dark) + feature-card-primary-bg: rgba(12, 68, 124, 0.8), + feature-card-secondary-bg: rgba(4, 44, 83, 0.9), + feature-card-tertiary-bg: rgba(12, 68, 124, 0.8), + feature-icon-primary: #ef9f27, + feature-icon-secondary: #ef9f27, + feature-icon-tertiary: #ef9f27, - // Text colors - FIXED for AAA contrast - feature-title-primary: $text-aaa-dark, - // Changed from $blue-lighter for AAA contrast - feature-title-secondary: $text-aaa-dark, - // Changed from $purple-lighter for AAA contrast - feature-title-tertiary: $text-aaa-dark, - - // Changed from $teal-light for AAA contrast - feature-description-primary: $text-aaa-dark, - // Changed from $blue-lightest for AAA contrast - feature-description-secondary: $text-aaa-dark, - // Changed from $purple-lightest for AAA contrast - feature-description-tertiary: $text-aaa-dark - // Changed from $teal-lightest for AAA contrast + feature-title-primary: #e6f1fb, + feature-title-secondary: #e6f1fb, + feature-title-tertiary: #e6f1fb, + feature-description-primary: #b5d4f4, + feature-description-secondary: #b5d4f4, + feature-description-tertiary: #b5d4f4, ); diff --git a/src/scss/themes/_base.scss b/src/scss/themes/_base.scss index d8690d7..458bd40 100644 --- a/src/scss/themes/_base.scss +++ b/src/scss/themes/_base.scss @@ -3,13 +3,13 @@ // Base colors and variables that are used across sections $base-light-theme: ( - primary: #800000, - background: #ffffff, - text: #000000, - text-muted: #2b2b2b, - active-box-shadow: rgba(0, 0, 0, 0.6), - hover-box-shadow: rgba(0, 0, 0, 0.9), - gradient-primary: $gradient-primary, + primary: #042c53, + background: #f1efe8, + text: #1a1a1a, + text-muted: #412402, + active-box-shadow: rgba(4, 44, 83, 0.2), + hover-box-shadow: rgba(4, 44, 83, 0.15), + gradient-primary: #042c53, gradient-text: $gradient-text-light, // Shadow variables @@ -36,10 +36,10 @@ $base-light-theme: ( // Form and text variables form-background: #ffffff, - form-border: #e5e7eb, - text-primary: #111827, - text-secondary: #484c56, - primary-color: #9333ea, + form-border: rgba(4, 44, 83, 0.15), + text-primary: #1a1a1a, + text-secondary: #412402, + primary-color: #042c53, // Contact status colors (light theme) contact-status-success-bg: #d1fae5, @@ -50,25 +50,24 @@ $base-light-theme: ( contact-status-error-text: #991b1b, // Additional missing variables - box-shadow-hover: rgba(0, 0, 0, 0.15), + box-shadow-hover: rgba(4, 44, 83, 0.15), bg-primary: #ffffff, - bg-secondary: #f9fafb, - border-color: #e5e7eb, - + bg-secondary: #f1efe8, + border-color: rgba(4, 44, 83, 0.15), // Design system semantic tokens - surface: #f8fafc, - border: #e5e7eb, - secondary: #2563eb, + surface: #ffffff, + border: rgba(4, 44, 83, 0.15), + secondary: #0c447c, ); $base-dark-theme: ( - primary: #9333ea, - background: #1f2937, - text: #ffffff, - text-muted: #ffffff, - active-box-shadow: rgba(255, 255, 255, 0.1), - hover-box-shadow: rgba(255, 255, 255, 0.15), - gradient-primary: $gradient-primary, + primary: #e6f1fb, + background: #021829, + text: #e6f1fb, + text-muted: #b5d4f4, + active-box-shadow: rgba(230, 241, 251, 0.1), + hover-box-shadow: rgba(230, 241, 251, 0.15), + gradient-primary: #ef9f27, gradient-text: $gradient-text-dark, // Shadow variables @@ -93,30 +92,27 @@ $base-dark-theme: ( transition-fast: #{$transition-fast}, // Form and text variables (dark theme) - form-background: #374151, - form-border: #4b5563, - text-primary: #f9fafb, - text-secondary: #9ca3af, - primary-color: #9333ea, + form-background: #042c53, + form-border: rgba(230, 241, 251, 0.15), + text-primary: #e6f1fb, + text-secondary: #b5d4f4, + primary-color: #ef9f27, // Contact status colors (dark theme) - contact-status-success-bg: #064e3b, - contact-status-success-border: #059669, - contact-status-success-text: #a7f3d0, - contact-status-error-bg: #7f1d1d, + contact-status-success-bg: rgba(4, 44, 83, 0.5), + contact-status-success-border: #ef9f27, + contact-status-success-text: #fac775, + contact-status-error-bg: rgba(127, 29, 29, 0.3), contact-status-error-border: #dc2626, contact-status-error-text: #fecaca, // Additional missing variables (dark theme) - box-shadow-hover: rgba(255, 255, 255, 0.15), - bg-primary: #1f2937, - bg-secondary: #374151, - border-color: #4b5563, - + box-shadow-hover: rgba(239, 159, 39, 0.15), + bg-primary: #021829, + bg-secondary: #042c53, + border-color: rgba(230, 241, 251, 0.12), // Design system semantic tokens - // surface is a subtle step above background — not a stark jump. - // Kept close to background so dark sections feel like one canvas. - surface: #232f3e, - border: #374151, - secondary: #60a5fa, + surface: #042c53, + border: rgba(230, 241, 251, 0.12), + secondary: #b5d4f4, ); diff --git a/src/scss/themes/_certifications.scss b/src/scss/themes/_certifications.scss index 24cacf4..816367c 100644 --- a/src/scss/themes/_certifications.scss +++ b/src/scss/themes/_certifications.scss @@ -47,74 +47,39 @@ $text-aaa-dark: #ffffff; // Pure white for AAA contrast on dark backgrounds $certifications-light-theme: ( // Certifications section background - 'certifications-background': - linear-gradient(135deg, $cert-bg-light-start 0%, $cert-bg-light-end 100%), - 'gradient-certifications-title': - linear-gradient(to right, $purple-primary, $pink-primary), - 'color-certifications-primary': $purple-primary, + 'certifications-background': #f1efe8, + 'gradient-certifications-title': #042c53, + 'color-certifications-primary': #ef9f27, - // Card styling - 'certifications-card-background': - linear-gradient(135deg, $white 0%, $gray-50 100%), + 'certifications-card-background': #ffffff, 'certifications-card-shadow': $shadow-card-light, 'certifications-card-shadow-hover': $shadow-card-light-hover, - 'certifications-card-overlay': - linear-gradient( - 135deg, - rgba($purple-primary, $alpha-overlay-light) 0%, - rgba($pink-primary, $alpha-overlay-light) 100% - ), - // Badge styling - FIXED for AAA contrast - 'certifications-badge-background': - linear-gradient(to right, $purple-lightest, $pink-lightest), - 'certifications-badge-border': rgba($purple-primary, $alpha-border-light), - 'color-certifications-badge-text': $text-aaa-light, + 'certifications-card-overlay': rgba(4, 44, 83, 0.04), + 'certifications-badge-background': #e6f1fb, + 'certifications-badge-border': rgba(4, 44, 83, 0.15), + 'color-certifications-badge-text': #042c53, - // Changed from $purple-secondary for AAA contrast - // Text styling for AAA compliance - 'certifications-card-title': $text-aaa-light, - 'certifications-card-description': $text-aaa-light, - 'certifications-card-date': $text-aaa-light, - // Medium gray for secondary text + 'certifications-card-title': #042c53, + 'certifications-card-description': #1a1a1a, + 'certifications-card-date': #412402 ); $certifications-dark-theme: ( - // Certifications section background (dark purple/pink gradient) - 'certifications-background': - linear-gradient(135deg, $cert-bg-dark-start 0%, $cert-bg-dark-end 100%), - // Certifications title gradient (brighter purple to pink for dark mode) - 'gradient-certifications-title': - linear-gradient(to right, $purple-light, $pink-light), - // Primary color for icons (lighter purple) - 'color-certifications-primary': $purple-light, + 'certifications-background': #042c53, + 'gradient-certifications-title': #e6f1fb, + 'color-certifications-primary': #ef9f27, - // Card styling (dark theme) - 'certifications-card-background': - linear-gradient(135deg, $gray-900 0%, $gray-800 100%), + 'certifications-card-background': rgba(12, 68, 124, 0.6), 'certifications-card-shadow': $shadow-card-dark, 'certifications-card-shadow-hover': $shadow-card-dark-hover, - 'certifications-card-overlay': - linear-gradient( - 135deg, - rgba($purple-light, $alpha-overlay-dark) 0%, - rgba($pink-light, $alpha-overlay-dark) 100% - ), - // Badge styling (dark theme) - FIXED for AAA contrast - 'certifications-badge-background': - linear-gradient( - to right, - rgba($purple-dark, $alpha-border-dark), - rgba($pink-dark, $alpha-border-dark) - ), - 'certifications-badge-border': rgba($purple-light, $alpha-border-dark), - 'color-certifications-badge-text': $text-aaa-dark, + 'certifications-card-overlay': rgba(239, 159, 39, 0.04), + 'certifications-badge-background': rgba(239, 159, 39, 0.15), + 'certifications-badge-border': rgba(239, 159, 39, 0.3), + 'color-certifications-badge-text': #ef9f27, - // Changed from $purple-lighter for AAA contrast - // Text styling for AAA compliance - 'certifications-card-title': $text-aaa-dark, - 'certifications-card-description': $text-aaa-dark, - 'certifications-card-date': #d1d5db, - // Light gray for secondary text in dark mode + 'certifications-card-title': #e6f1fb, + 'certifications-card-description': #b5d4f4, + 'certifications-card-date': #b5d4f4, ); // Card layout styling diff --git a/src/scss/themes/_contact.scss b/src/scss/themes/_contact.scss index 8a7adc3..0df3373 100644 --- a/src/scss/themes/_contact.scss +++ b/src/scss/themes/_contact.scss @@ -72,148 +72,87 @@ $text-aaa-dark: #ffffff; // Pure white for AAA contrast on dark backgrounds $contact-light-theme: ( // Contact section background (light indigo/purple tones) - 'contact-background': - linear-gradient( - 135deg, - $contact-bg-light-start 0%, - $contact-bg-light-end 100% - ), - // Contact title gradient (indigo to purple) - 'gradient-contact-title': - linear-gradient(to right, $indigo-primary, $indigo-secondary), - // Primary color for icons (indigo) - 'color-contact-primary': $indigo-primary, + 'contact-background': #f1efe8, + 'gradient-contact-title': #042c53, + 'color-contact-primary': #042c53, - // Form styling - warm cream/peach to complement cool blue background - 'contact-form-bg': - linear-gradient(135deg, $orange-lightest 0%, $orange-lighter 100%), - 'contact-form-border': rgba($orange-primary, $alpha-border-light), + 'contact-form-bg': #ffffff, + 'contact-form-border': rgba(4, 44, 83, 0.15), 'contact-form-shadow': $shadow-light, - // Input styling - warm background to complement form - 'contact-input-bg': $amber-primary, - 'contact-input-border': rgba($orange-primary, $alpha-border-medium), - 'contact-input-focus-ring': rgba($orange-primary, $alpha-border-light), - 'contact-input-text': $text-aaa-light, - // Added for AAA compliance - 'contact-input-placeholder': $text-input-placeholder-light, + 'contact-input-bg': #f1efe8, + 'contact-input-border': rgba(4, 44, 83, 0.2), + 'contact-input-focus-ring': rgba(4, 44, 83, 0.3), + 'contact-input-text': #1a1a1a, + 'contact-input-placeholder': #412402, - // Added for form accessibility - // Button styling - improved contrast and readability - 'contact-button-bg': - linear-gradient(135deg, $blue-primary 0%, $blue-secondary 100%), - 'contact-button-text': $text-aaa-dark, - // Changed to ensure AAA contrast - 'contact-button-hover-bg': - linear-gradient(135deg, $blue-dark 0%, $blue-darker 100%), - // Social button styling - FIXED for AAA contrast - 'contact-social-bg': $orange-lighter, - 'contact-social-text': $text-aaa-light, - // Changed from $orange-dark for AAA contrast - 'contact-social-border': $orange-primary, - 'contact-social-hover-bg': $orange-light, - 'contact-social-hover-border': $orange-secondary, + 'contact-button-bg': #042c53, + 'contact-button-text': #e6f1fb, + 'contact-button-hover-bg': #0c447c, - // Status message styling (light theme) - improved readability - 'contact-status-success-bg': $green-lightest, - 'contact-status-success-border': $green-primary, - 'contact-status-success-text': $green-secondary, - 'contact-status-error-bg': $red-lightest, - 'contact-status-error-border': $red-primary, - 'contact-status-error-text': $red-secondary, + 'contact-social-bg': #f1efe8, + 'contact-social-text': #042c53, + 'contact-social-border': rgba(4, 44, 83, 0.15), + 'contact-social-hover-bg': #e6f1fb, + 'contact-social-hover-border': #0c447c, - // Text colors for AAA compliance - 'color-text': $text-aaa-light, - // Near black for AAA contrast - 'color-text-muted': $gray-neutral-light, - // Gray for secondary text - 'primary-color': $indigo-primary, - // Indigo primary - 'text-primary': $text-aaa-light, - // Main text color - 'text-secondary': $gray-neutral-light, + 'contact-status-success-bg': #d1fae5, + 'contact-status-success-border': #10b981, + 'contact-status-success-text': #065f46, + 'contact-status-error-bg': #fee2e2, + 'contact-status-error-border': #ef4444, + 'contact-status-error-text': #991b1b, - // Secondary text color - // Enhanced social button colors for AAA compliance - 'social-icon-email': $text-aaa-light, - // Dark for better contrast - 'social-icon-github': $text-aaa-light, - // Dark for better contrast - 'social-icon-linkedin': $text-aaa-light, - // Dark for better contrast + 'color-text': #1a1a1a, + 'color-text-muted': #412402, + 'primary-color': #042c53, + 'text-primary': #1a1a1a, + 'text-secondary': #412402, + + 'social-icon-email': #042c53, + 'social-icon-github': #042c53, + 'social-icon-linkedin': #042c53 ); $contact-dark-theme: ( - // Contact section background (dark indigo/purple tones) - 'contact-background': - linear-gradient( - 135deg, - rgba($indigo-light, $alpha-border-light) 0%, - rgba($purple-primary, $alpha-border-light) 100% - ), - // Contact title gradient (brighter indigo to purple for dark mode) - 'gradient-contact-title': - linear-gradient(to right, $indigo-light, $purple-primary), - // Primary color for icons (lighter indigo) - 'color-contact-primary': $indigo-light, + 'contact-background': #021829, + 'gradient-contact-title': #e6f1fb, + 'color-contact-primary': #ef9f27, - // Form styling (dark theme) - warm dark tones to complement cool purple/indigo - 'contact-form-bg': - linear-gradient(135deg, $orange-darkest 0%, $orange-darker 100%), - 'contact-form-border': rgba($orange-primary, $alpha-border-medium), + 'contact-form-bg': #042c53, + 'contact-form-border': rgba(230, 241, 251, 0.12), 'contact-form-shadow': $shadow-dark, - // Input styling (dark theme) - FIXED for AAA contrast - 'contact-input-bg': $gray-neutral-dark, - // Changed to neutral dark gray - 'contact-input-border': rgba($orange-primary, $alpha-border-heavy), - 'contact-input-focus-ring': rgba($orange-primary, $alpha-border-medium), - 'contact-input-text': $text-aaa-dark, - // White text for AAA contrast - 'contact-input-placeholder': $gray-neutral-lighter, + 'contact-input-bg': #042c53, + 'contact-input-border': rgba(230, 241, 251, 0.2), + 'contact-input-focus-ring': rgba(239, 159, 39, 0.3), + 'contact-input-text': #e6f1fb, + 'contact-input-placeholder': #b5d4f4, - // Light gray for placeholder - // Button styling (dark theme) - improved contrast - 'contact-button-bg': - linear-gradient(135deg, $blue-light 0%, $blue-primary 100%), - 'contact-button-text': $text-aaa-dark, - 'contact-button-hover-bg': - linear-gradient(135deg, $blue-lighter 0%, $blue-light 100%), - // Social button styling (dark theme) - FIXED for AAA contrast - 'contact-social-bg': $gray-neutral-medium, - // Changed to neutral dark background - 'contact-social-text': $text-aaa-dark, - // White text for AAA contrast - 'contact-social-border': $orange-secondary, - 'contact-social-hover-bg': $gray-neutral-dark, - // Slightly lighter on hover - 'contact-social-hover-border': $orange-primary, + 'contact-button-bg': #ef9f27, + 'contact-button-text': #042c53, + 'contact-button-hover-bg': #fac775, - // Status message styling (dark theme) - improved readability - 'contact-status-success-bg': rgba($green-primary, $alpha-low), - 'contact-status-success-border': $green-light, - 'contact-status-success-text': $green-lighter, - 'contact-status-error-bg': rgba($red-light, $alpha-low), - 'contact-status-error-border': $red-light, - 'contact-status-error-text': $red-lighter, + 'contact-social-bg': #042c53, + 'contact-social-text': #e6f1fb, + 'contact-social-border': rgba(239, 159, 39, 0.3), + 'contact-social-hover-bg': #0c447c, + 'contact-social-hover-border': #ef9f27, - // Text colors for AAA compliance - ALL WHITE in dark mode - 'color-text': $text-aaa-dark, - // White for AAA contrast on dark - 'color-text-muted': $text-aaa-dark, - // Changed to white for consistency - 'primary-color': $indigo-light, - // Lighter indigo for dark mode - 'text-primary': $text-aaa-dark, - // Main text color - 'text-secondary': $text-aaa-dark, - // Changed to white for consistency - // Secondary text color - // Enhanced social button colors for AAA compliance - 'social-icon-email': $text-aaa-dark, - // White for better contrast - 'social-icon-github': $text-aaa-dark, - // White for better contrast - 'social-icon-linkedin': $text-aaa-dark, - // White for better contrast + 'contact-status-success-bg': rgba(4, 44, 83, 0.5), + 'contact-status-success-border': #ef9f27, + 'contact-status-success-text': #fac775, + 'contact-status-error-bg': rgba(127, 29, 29, 0.3), + 'contact-status-error-border': #dc2626, + 'contact-status-error-text': #fecaca, + + 'color-text': #e6f1fb, + 'color-text-muted': #b5d4f4, + 'primary-color': #ef9f27, + 'text-primary': #e6f1fb, + 'text-secondary': #b5d4f4, + + 'social-icon-email': #e6f1fb, + 'social-icon-github': #e6f1fb, + 'social-icon-linkedin': #e6f1fb, ); diff --git a/src/scss/themes/_hero.scss b/src/scss/themes/_hero.scss index f4bad6d..08efca3 100644 --- a/src/scss/themes/_hero.scss +++ b/src/scss/themes/_hero.scss @@ -45,88 +45,41 @@ $text-aaa-light: #111827; // Near black for AAA contrast on light backgrounds $text-aaa-dark: #ffffff; // Pure white for AAA contrast on dark backgrounds $hero-light-theme: ( - hero-background: - linear-gradient(135deg, $hero-bg-light-start 0%, $hero-bg-light-end 100%), - // Stat colors - FIXED for AAA contrast - stat-primary-bg: linear-gradient(135deg, $blue-lighter, $blue-lightest), - stat-primary-value: $text-aaa-light, - // Changed from $blue-primary for AAA contrast - stat-primary-label: $text-aaa-light, + hero-background: #f1efe8, + // Stat card backgrounds – flat brand tones + stat-primary-bg: #e6f1fb, + stat-primary-value: #042c53, + stat-primary-label: #042c53, - // Changed from $blue-secondary for AAA contrast - stat-secondary-bg: linear-gradient(135deg, $green-lighter, $green-lightest), - stat-secondary-value: $text-aaa-light, - // Changed from $green-primary for AAA contrast - stat-secondary-label: $text-aaa-light, + stat-secondary-bg: #f1efe8, + stat-secondary-value: #042c53, + stat-secondary-label: #042c53, - // Changed from $green-secondary for AAA contrast - stat-tertiary-bg: linear-gradient(135deg, $teal-lighter, $teal-lightest), - stat-tertiary-value: $text-aaa-light, - // Changed from $teal-primary for AAA contrast - stat-tertiary-label: $text-aaa-light, + stat-tertiary-bg: #e6f1fb, + stat-tertiary-value: #042c53, + stat-tertiary-label: #042c53, - // Changed from $teal-secondary for AAA contrast - stat-quaternary-bg: linear-gradient( - 135deg, - $purple-lighter, - $purple-lightest - ), - stat-quaternary-value: $text-aaa-light, - // Changed from $purple-primary for AAA contrast - stat-quaternary-label: $text-aaa-light, - // Changed from $purple-secondary for AAA contrast + stat-quaternary-bg: #faeeda, + stat-quaternary-value: #412402, + stat-quaternary-label: #412402, ); $hero-dark-theme: ( - hero-background: - linear-gradient( - 135deg, - rgba($blue-dark, $alpha-bg-low) 0%, - rgba($purple-dark, $alpha-bg-low) 50%, - rgba($purple-darker, $alpha-bg-low) 100% - ), - // Stat colors - Enhanced contrast for AAA compliance - stat-primary-bg: - linear-gradient( - 135deg, - rgba($blue-dark, $alpha-bg-low), - rgba($blue-darker, $alpha-bg-low) - ), - stat-primary-value: $text-aaa-dark, - // Changed from $blue-primary to white for AAA contrast - stat-primary-label: $text-aaa-dark, + hero-background: #042c53, + // Stat card backgrounds – flat brand tones for dark + stat-primary-bg: rgba(12, 68, 124, 0.6), + stat-primary-value: #e6f1fb, + stat-primary-label: #b5d4f4, - // Changed from $blue-light to white for better contrast - stat-secondary-bg: - linear-gradient( - 135deg, - rgba($green-dark, $alpha-bg-low), - rgba($green-darker, $alpha-bg-low) - ), - stat-secondary-value: $text-aaa-dark, - // Changed from $green-primary to white for AAA contrast - stat-secondary-label: $text-aaa-dark, + stat-secondary-bg: rgba(12, 68, 124, 0.4), + stat-secondary-value: #e6f1fb, + stat-secondary-label: #b5d4f4, - // Changed from $green-light to white for better contrast - stat-tertiary-bg: - linear-gradient( - 135deg, - rgba($teal-dark, $alpha-bg-low), - rgba($teal-darker, $alpha-bg-low) - ), - stat-tertiary-value: $text-aaa-dark, - // Changed from $teal-primary to white for AAA contrast - stat-tertiary-label: $text-aaa-dark, + stat-tertiary-bg: rgba(12, 68, 124, 0.6), + stat-tertiary-value: #e6f1fb, + stat-tertiary-label: #b5d4f4, - // Changed from $teal-light to white for better contrast - stat-quaternary-bg: - linear-gradient( - 135deg, - rgba($purple-dark, $alpha-bg-low), - rgba($purple-darker, $alpha-bg-low) - ), - stat-quaternary-value: $text-aaa-dark, - // Changed from $purple-primary to white for AAA contrast - stat-quaternary-label: $text-aaa-dark, - // Changed from $purple-light to white for better contrast + stat-quaternary-bg: rgba(239, 159, 39, 0.15), + stat-quaternary-value: #ef9f27, + stat-quaternary-label: #e6f1fb, ); diff --git a/src/scss/themes/_projects.scss b/src/scss/themes/_projects.scss index 85b53da..0186094 100644 --- a/src/scss/themes/_projects.scss +++ b/src/scss/themes/_projects.scss @@ -57,76 +57,43 @@ $text-aaa-dark: $white; // #ffffff - ensures AAA contrast on dark backgrounds $projects-light-theme: ( // Projects section background - 'projects-background': - linear-gradient(135deg, $bg-light-start 0%, $bg-light-end 100%), - // Projects title gradient - 'gradient-projects-title': - linear-gradient(to right, $orange-primary, $red-primary), - // Card styling - 'projects-card-background': - linear-gradient(135deg, $card-light-start 0%, $card-light-end 100%), - 'projects-card-border': rgba($orange-primary, $alpha-border-light), + 'projects-background': #ffffff, + 'gradient-projects-title': #042c53, + 'projects-card-background': #ffffff, + 'projects-card-border': rgba(4, 44, 83, 0.12), 'projects-card-shadow': $shadow-light, 'projects-card-shadow-hover': $shadow-light-hover, - // Overlay styling - 'projects-overlay-background': rgba($black, $alpha-overlay-light), - // Button styling - 'projects-button-primary-bg': - linear-gradient(135deg, $orange-primary 0%, $red-primary 100%), - 'projects-button-primary-text': $text-aaa-dark, - // Changed to ensure AAA contrast - 'projects-button-primary-border': rgba($orange-primary, $alpha-border-heavy), - 'projects-button-primary-hover-bg': - linear-gradient(135deg, $red-primary 0%, $red-secondary 100%), - 'projects-button-secondary-bg': $white, - 'projects-button-secondary-text': $text-aaa-light, - // Changed from $gray-800 for AAA compliance - 'projects-button-secondary-border': rgba($gray-300, $alpha-border-medium), - 'projects-button-secondary-hover-bg': $gray-50, - // Changed from $white for subtle hover effect - // Tech badge styling - FIXED for AAA contrast - 'projects-tech-badge-bg': $gray-100, - // Changed to solid light gray instead of gradient - 'projects-tech-badge-text': $text-aaa-light, - // Changed to dark text for AAA contrast - 'projects-tech-badge-border': rgba($orange-primary, $alpha-border-medium) + 'projects-overlay-background': rgba(0, 0, 0, 0.6), + 'projects-button-primary-bg': #042c53, + 'projects-button-primary-text': #e6f1fb, + 'projects-button-primary-border': rgba(4, 44, 83, 0.3), + 'projects-button-primary-hover-bg': #0c447c, + 'projects-button-secondary-bg': #ffffff, + 'projects-button-secondary-text': #042c53, + 'projects-button-secondary-border': rgba(4, 44, 83, 0.2), + 'projects-button-secondary-hover-bg': #f1efe8, + 'projects-tech-badge-bg': #f1efe8, + 'projects-tech-badge-text': #042c53, + 'projects-tech-badge-border': rgba(4, 44, 83, 0.15) ); $projects-dark-theme: ( - // Projects section background - using variables instead of hardcoded rgba - 'projects-background': - linear-gradient( - 135deg, - rgba($brown-primary, $alpha-border-medium) 0%, - rgba($brown-secondary, $alpha-border-medium) 100% - ), - // Projects title gradient - 'gradient-projects-title': - linear-gradient(to right, $orange-secondary, $red-light), - // Card styling - 'projects-card-background': - linear-gradient(135deg, $gray-900 0%, $gray-800 100%), - 'projects-card-border': rgba($orange-secondary, $alpha-border-medium), + 'projects-background': #0c447c, + 'gradient-projects-title': #e6f1fb, + 'projects-card-background': rgba(12, 68, 124, 0.6), + 'projects-card-border': rgba(230, 241, 251, 0.12), 'projects-card-shadow': $shadow-dark, 'projects-card-shadow-hover': $shadow-dark-hover, - // Overlay styling - 'projects-overlay-background': rgba($black, $alpha-overlay-dark), - // Button styling - 'projects-button-primary-bg': - linear-gradient(135deg, $orange-primary 0%, $red-primary 100%), - 'projects-button-primary-text': $text-aaa-dark, - 'projects-button-primary-border': rgba($orange-primary, $alpha-border-heavy), - 'projects-button-primary-hover-bg': - linear-gradient(135deg, $orange-secondary 0%, $red-light 100%), - 'projects-button-secondary-bg': rgba($gray-800, $alpha-bg-light), - 'projects-button-secondary-text': $text-aaa-dark, - // Changed from $gray-200 for AAA compliance - 'projects-button-secondary-border': rgba($gray-600, $alpha-border-heavy), - 'projects-button-secondary-hover-bg': $gray-700, - // Tech badge styling - FIXED for AAA contrast - 'projects-tech-badge-bg': $gray-800, - // Changed to solid dark background - 'projects-tech-badge-text': $text-aaa-dark, - // White text for AAA contrast - 'projects-tech-badge-border': rgba($orange-secondary, $alpha-border-heavy) + 'projects-overlay-background': rgba(0, 0, 0, 0.7), + 'projects-button-primary-bg': #ef9f27, + 'projects-button-primary-text': #042c53, + 'projects-button-primary-border': rgba(239, 159, 39, 0.3), + 'projects-button-primary-hover-bg': #ba7517, + 'projects-button-secondary-bg': rgba(12, 68, 124, 0.8), + 'projects-button-secondary-text': #e6f1fb, + 'projects-button-secondary-border': rgba(230, 241, 251, 0.3), + 'projects-button-secondary-hover-bg': rgba(4, 44, 83, 0.9), + 'projects-tech-badge-bg': rgba(4, 44, 83, 0.8), + 'projects-tech-badge-text': #e6f1fb, + 'projects-tech-badge-border': rgba(230, 241, 251, 0.2), ); diff --git a/src/scss/themes/_services.scss b/src/scss/themes/_services.scss index 3f76b04..2f57746 100644 --- a/src/scss/themes/_services.scss +++ b/src/scss/themes/_services.scss @@ -148,77 +148,66 @@ $dark-senary-title: #fca5a5; // Medium red // ================================ $services-light-theme: ( - // Section backgrounds - services-background: $light-services-bg, - gradient-services-title: $light-title-gradient, + services-background: #f1efe8, + gradient-services-title: #042c53, - // Card backgrounds - service-card-primary-bg: $light-card-primary-bg, - service-card-secondary-bg: $light-card-secondary-bg, - service-card-tertiary-bg: $light-card-tertiary-bg, - service-card-quaternary-bg: $light-card-quaternary-bg, - service-card-quinary-bg: $light-card-quinary-bg, - service-card-senary-bg: $light-card-senary-bg, + service-card-primary-bg: #ffffff, + service-card-secondary-bg: #f1efe8, + service-card-tertiary-bg: #ffffff, + service-card-quaternary-bg: #f1efe8, + service-card-quinary-bg: #ffffff, + service-card-senary-bg: #f1efe8, - // Icons - using same color for consistency - service-icon-primary: $light-text-primary, - service-icon-secondary: $light-text-primary, - service-icon-tertiary: $light-text-primary, - service-icon-quaternary: $light-text-primary, - service-icon-quinary: $light-text-primary, - service-icon-senary: $light-text-primary, + service-icon-primary: #ef9f27, + service-icon-secondary: #ef9f27, + service-icon-tertiary: #ef9f27, + service-icon-quaternary: #ef9f27, + service-icon-quinary: #ef9f27, + service-icon-senary: #ef9f27, - // Titles - using same color as icons - service-title-primary: $light-text-primary, - service-title-secondary: $light-text-primary, - service-title-tertiary: $light-text-primary, - service-title-quaternary: $light-text-primary, - service-title-quinary: $light-text-primary, - service-title-senary: $light-text-primary, + service-title-primary: #042c53, + service-title-secondary: #042c53, + service-title-tertiary: #042c53, + service-title-quaternary: #042c53, + service-title-quinary: #042c53, + service-title-senary: #042c53, - // Descriptions - using same color as icons and titles - service-description-primary: $light-text-primary, - service-description-secondary: $light-text-primary, - service-description-tertiary: $light-text-primary, - service-description-quaternary: $light-text-primary, - service-description-quinary: $light-text-primary, - service-description-senary: $light-text-primary + service-description-primary: #1a1a1a, + service-description-secondary: #1a1a1a, + service-description-tertiary: #1a1a1a, + service-description-quaternary: #1a1a1a, + service-description-quinary: #1a1a1a, + service-description-senary: #1a1a1a, ); $services-dark-theme: ( - // Section backgrounds - services-background: $dark-services-bg, - gradient-services-title: $dark-title-gradient, + services-background: #042c53, + gradient-services-title: #e6f1fb, - // Card backgrounds - service-card-primary-bg: $dark-card-primary-bg, - service-card-secondary-bg: $dark-card-secondary-bg, - service-card-tertiary-bg: $dark-card-tertiary-bg, - service-card-quaternary-bg: $dark-card-quaternary-bg, - service-card-quinary-bg: $dark-card-quinary-bg, - service-card-senary-bg: $dark-card-senary-bg, + service-card-primary-bg: rgba(12, 68, 124, 0.6), + service-card-secondary-bg: rgba(4, 44, 83, 0.8), + service-card-tertiary-bg: rgba(12, 68, 124, 0.6), + service-card-quaternary-bg: rgba(4, 44, 83, 0.8), + service-card-quinary-bg: rgba(12, 68, 124, 0.6), + service-card-senary-bg: rgba(4, 44, 83, 0.8), + service-icon-primary: #ef9f27, + service-icon-secondary: #ef9f27, + service-icon-tertiary: #ef9f27, + service-icon-quaternary: #ef9f27, + service-icon-quinary: #ef9f27, + service-icon-senary: #ef9f27, - // Icons - all using pure white - service-icon-primary: #ffffff, - service-icon-secondary: #ffffff, - service-icon-tertiary: #ffffff, - service-icon-quaternary: #ffffff, - service-icon-quinary: #ffffff, - service-icon-senary: #ffffff, + service-title-primary: #e6f1fb, + service-title-secondary: #e6f1fb, + service-title-tertiary: #e6f1fb, + service-title-quaternary: #e6f1fb, + service-title-quinary: #e6f1fb, + service-title-senary: #e6f1fb, - // Titles - all using pure white - service-title-primary: #ffffff, - service-title-secondary: #ffffff, - service-title-tertiary: #ffffff, - service-title-quaternary: #ffffff, - service-title-quinary: #ffffff, - service-title-senary: #ffffff, - - // Descriptions - all using pure white - service-description-primary: #ffffff, - service-description-secondary: #ffffff, - service-description-tertiary: #ffffff, - service-description-quaternary: #ffffff, - service-description-quinary: #ffffff, - service-description-senary: #ffffff + service-description-primary: #b5d4f4, + service-description-secondary: #b5d4f4, + service-description-tertiary: #b5d4f4, + service-description-quaternary: #b5d4f4, + service-description-quinary: #b5d4f4, + service-description-senary: #b5d4f4, ); diff --git a/src/scss/themes/_skills.scss b/src/scss/themes/_skills.scss index c34f5f8..ceb0189 100644 --- a/src/scss/themes/_skills.scss +++ b/src/scss/themes/_skills.scss @@ -52,245 +52,99 @@ $alpha-bg-card-strong: 0.95; $skills-light-theme: ( // Skills section colors - skills-background: linear-gradient(135deg, #e0e7ef 0%, #bae6fd 100%), - skills-background-pattern: - radial-gradient(circle at 25% 25%, $blue-primary, transparent 50%), - gradient-skills-title: - linear-gradient(90deg, $blue-tertiary, $blue-primary, $green-primary), - skills-category-bg: rgba($slate-50, $alpha-bg), - skills-category-border: rgba($slate-400, $alpha-medium), - skills-category-title-color: $slate-800, - skills-skill-name-color: $slate-700, - skills-skill-level-color: $slate-900, - skills-skill-percentage-color: $slate-600, - skills-progress-bg: rgba($slate-200, $alpha-bg-card), - // Card 1 - Web Frameworks (Blue theme) - skills-category-bg-primary: - linear-gradient( - 135deg, - rgba(239, 246, 255, $alpha-bg-card-alt), - rgba(219, 234, 254, $alpha-bg-card-strong) - ), - skills-category-border-primary: rgba($blue-primary, $alpha-medium), - skills-gradient-primary: - linear-gradient( - 135deg, - rgba($blue-primary, $alpha-light), - rgba($blue-secondary, 0.05) - ), - skills-accent-primary: $blue-primary, + skills-background: #ffffff, + skills-background-pattern: none, + gradient-skills-title: #042c53, + skills-category-bg: #ffffff, + skills-category-border: rgba(4, 44, 83, 0.12), + skills-category-title-color: #042c53, + skills-skill-name-color: #1a1a1a, + skills-skill-level-color: #1a1a1a, + skills-skill-percentage-color: #412402, + skills-progress-bg: rgba(4, 44, 83, 0.06), + skills-category-bg-primary: #ffffff, + skills-category-border-primary: rgba(4, 44, 83, 0.12), + skills-gradient-primary: transparent, + skills-accent-primary: #042c53, - // Card 2 - Styling & Design (Green theme) - skills-category-bg-secondary: - linear-gradient( - 135deg, - rgba(236, 253, 245, $alpha-bg-card-alt), - rgba(209, 250, 229, $alpha-bg-card-strong) - ), - skills-category-border-secondary: rgba($green-primary, $alpha-medium), - skills-gradient-secondary: - linear-gradient( - 135deg, - rgba($green-primary, $alpha-light), - rgba($green-secondary, 0.05) - ), - skills-accent-secondary: $green-primary, + skills-category-bg-secondary: #f1efe8, + skills-category-border-secondary: rgba(4, 44, 83, 0.12), + skills-gradient-secondary: transparent, + skills-accent-secondary: #042c53, - // Card 3 - Backend Development (Purple theme) - skills-category-bg-tertiary: - linear-gradient( - 135deg, - rgba(245, 243, 255, $alpha-bg-card-alt), - rgba(237, 233, 254, $alpha-bg-card-strong) - ), - skills-category-border-tertiary: rgba($purple-primary, $alpha-medium), - skills-gradient-tertiary: - linear-gradient( - 135deg, - rgba($purple-primary, $alpha-light), - rgba($purple-secondary, 0.05) - ), - skills-accent-tertiary: $purple-primary, + skills-category-bg-tertiary: #ffffff, + skills-category-border-tertiary: rgba(4, 44, 83, 0.12), + skills-gradient-tertiary: transparent, + skills-accent-tertiary: #042c53, - // Card 4 - Development Tools (Teal theme) - skills-category-bg-quaternary: - linear-gradient( - 135deg, - rgba(240, 253, 250, $alpha-bg-card-alt), - rgba(204, 251, 241, $alpha-bg-card-strong) - ), - skills-category-border-quaternary: rgba($teal-primary, $alpha-medium), - skills-gradient-quaternary: - linear-gradient( - 135deg, - rgba($teal-primary, $alpha-light), - rgba($teal-secondary, 0.05) - ), - skills-accent-quaternary: $teal-primary, + skills-category-bg-quaternary: #f1efe8, + skills-category-border-quaternary: rgba(4, 44, 83, 0.12), + skills-gradient-quaternary: transparent, + skills-accent-quaternary: #042c53, - // Card 5 - Testing & Quality (Orange theme) - skills-category-bg-quinary: - linear-gradient( - 135deg, - rgba(255, 251, 235, $alpha-bg-card-alt), - rgba(254, 243, 199, $alpha-bg-card-strong) - ), - skills-category-border-quinary: rgba($orange-primary, $alpha-medium), - skills-gradient-quinary: - linear-gradient( - 135deg, - rgba($orange-primary, $alpha-light), - rgba($orange-secondary, 0.05) - ), - skills-accent-quinary: $orange-primary, + skills-category-bg-quinary: #ffffff, + skills-category-border-quinary: rgba(4, 44, 83, 0.12), + skills-gradient-quinary: transparent, + skills-accent-quinary: #042c53, - // Card 6 - AI-Tools (Indigo theme) - skills-category-bg-senary: - linear-gradient( - 135deg, - rgba(238, 242, 255, $alpha-bg-card-alt), - rgba(224, 231, 255, $alpha-bg-card-strong) - ), - skills-category-border-senary: rgba($indigo-primary, $alpha-medium), - skills-gradient-senary: - linear-gradient( - 135deg, - rgba($indigo-primary, $alpha-light), - rgba($indigo-secondary, 0.05) - ), - skills-accent-senary: $indigo-primary, + skills-category-bg-senary: #f1efe8, + skills-category-border-senary: rgba(4, 44, 83, 0.12), + skills-gradient-senary: transparent, + skills-accent-senary: #042c53, - // Progress bar gradients - skills-progress-primary: - linear-gradient(90deg, $blue-primary, $blue-secondary, $blue-tertiary), - skills-progress-secondary: - linear-gradient(90deg, $green-primary, $green-secondary, $green-tertiary), - skills-progress-tertiary: - linear-gradient(90deg, $purple-primary, $purple-tertiary, $purple-secondary), - skills-progress-quaternary: - linear-gradient(90deg, $teal-primary, $teal-secondary, $teal-tertiary), - skills-progress-quinary: - linear-gradient(90deg, $orange-primary, $orange-secondary, $orange-tertiary), - skills-progress-senary: - linear-gradient(90deg, $indigo-primary, $indigo-secondary, $indigo-tertiary) + skills-progress-primary: #042c53, + skills-progress-secondary: #042c53, + skills-progress-tertiary: #042c53, + skills-progress-quaternary: #042c53, + skills-progress-quinary: #042c53, + skills-progress-senary: #042c53 ); $skills-dark-theme: ( - // Skills section colors - skills-background: linear-gradient(135deg, $slate-800 0%, #0ea5e9 100%), - skills-background-pattern: - radial-gradient(circle at 25% 25%, $blue-tertiary, transparent 50%), - gradient-skills-title: - linear-gradient(90deg, $blue-light, $green-light, $purple-light), - skills-category-bg: rgba($slate-800, $alpha-bg-card), - skills-category-border: rgba($slate-600, $alpha-heavy), - skills-category-title-color: #ffffff, - skills-skill-name-color: #ffffff, - skills-skill-level-color: #ffffff, - skills-skill-percentage-color: #ffffff, - skills-progress-bg: rgba($slate-700, $alpha-bg-card), - // Dark theme card backgrounds - skills-category-bg-primary: - linear-gradient( - 135deg, - rgba(30, 58, 138, 0.4), - rgba(29, 78, 216, $alpha-heavy) - ), - skills-category-border-primary: rgba($blue-light, $alpha-heavy), - skills-gradient-primary: - linear-gradient( - 135deg, - rgba($blue-light, $alpha-light), - rgba($blue-primary, 0.05) - ), - skills-accent-primary: $blue-light, + skills-background: #0c447c, + skills-background-pattern: none, + gradient-skills-title: #e6f1fb, + skills-category-bg: rgba(12, 68, 124, 0.6), + skills-category-border: rgba(230, 241, 251, 0.12), + skills-category-title-color: #e6f1fb, + skills-skill-name-color: #e6f1fb, + skills-skill-level-color: #e6f1fb, + skills-skill-percentage-color: #b5d4f4, + skills-progress-bg: rgba(230, 241, 251, 0.1), + skills-category-bg-primary: rgba(12, 68, 124, 0.6), + skills-category-border-primary: rgba(230, 241, 251, 0.12), + skills-gradient-primary: transparent, + skills-accent-primary: #ef9f27, - skills-category-bg-secondary: - linear-gradient( - 135deg, - rgba(6, 95, 70, 0.4), - rgba(4, 120, 87, $alpha-heavy) - ), - skills-category-border-secondary: rgba($green-light, $alpha-heavy), - skills-gradient-secondary: - linear-gradient( - 135deg, - rgba($green-light, $alpha-light), - rgba($green-primary, 0.05) - ), - skills-accent-secondary: $green-light, + skills-category-bg-secondary: rgba(4, 44, 83, 0.8), + skills-category-border-secondary: rgba(230, 241, 251, 0.12), + skills-gradient-secondary: transparent, + skills-accent-secondary: #ef9f27, - skills-category-bg-tertiary: - linear-gradient( - 135deg, - rgba(88, 28, 135, 0.4), - rgba(109, 40, 217, $alpha-heavy) - ), - skills-category-border-tertiary: rgba($purple-light, $alpha-heavy), - skills-gradient-tertiary: - linear-gradient( - 135deg, - rgba($purple-light, $alpha-light), - rgba($purple-primary, 0.05) - ), - skills-accent-tertiary: $purple-light, + skills-category-bg-tertiary: rgba(12, 68, 124, 0.6), + skills-category-border-tertiary: rgba(230, 241, 251, 0.12), + skills-gradient-tertiary: transparent, + skills-accent-tertiary: #ef9f27, - skills-category-bg-quaternary: - linear-gradient( - 135deg, - rgba(15, 118, 110, 0.4), - rgba(17, 94, 89, $alpha-heavy) - ), - skills-category-border-quaternary: rgba($teal-light, $alpha-heavy), - skills-gradient-quaternary: - linear-gradient( - 135deg, - rgba($teal-light, $alpha-light), - rgba($teal-primary, 0.05) - ), - skills-accent-quaternary: $teal-light, + skills-category-bg-quaternary: rgba(4, 44, 83, 0.8), + skills-category-border-quaternary: rgba(230, 241, 251, 0.12), + skills-gradient-quaternary: transparent, + skills-accent-quaternary: #ef9f27, - skills-category-bg-quinary: - linear-gradient( - 135deg, - rgba(180, 83, 9, 0.4), - rgba(217, 119, 6, $alpha-heavy) - ), - skills-category-border-quinary: rgba($orange-light, $alpha-heavy), - skills-gradient-quinary: - linear-gradient( - 135deg, - rgba($orange-light, $alpha-light), - rgba($orange-primary, 0.05) - ), - skills-accent-quinary: $orange-light, + skills-category-bg-quinary: rgba(12, 68, 124, 0.6), + skills-category-border-quinary: rgba(230, 241, 251, 0.12), + skills-gradient-quinary: transparent, + skills-accent-quinary: #ef9f27, - skills-category-bg-senary: - linear-gradient( - 135deg, - rgba(67, 56, 202, 0.4), - rgba(79, 70, 229, $alpha-heavy) - ), - skills-category-border-senary: rgba($indigo-light, $alpha-heavy), - skills-gradient-senary: - linear-gradient( - 135deg, - rgba($indigo-light, $alpha-light), - rgba($indigo-primary, 0.05) - ), - skills-accent-senary: $indigo-light, + skills-category-bg-senary: rgba(4, 44, 83, 0.8), + skills-category-border-senary: rgba(230, 241, 251, 0.12), + skills-gradient-senary: transparent, + skills-accent-senary: #ef9f27, - // Progress bars for dark theme - skills-progress-primary: - linear-gradient(90deg, $blue-light, $blue-primary, $blue-secondary), - skills-progress-secondary: - linear-gradient(90deg, $green-light, $green-primary, $green-secondary), - skills-progress-tertiary: - linear-gradient(90deg, $purple-light, $purple-primary, $purple-tertiary), - skills-progress-quaternary: - linear-gradient(90deg, $teal-light, $teal-primary, $teal-secondary), - skills-progress-quinary: - linear-gradient(90deg, $orange-light, $orange-primary, $orange-secondary), - skills-progress-senary: - linear-gradient(90deg, $indigo-light, $indigo-primary, $indigo-secondary) + skills-progress-primary: #ef9f27, + skills-progress-secondary: #ef9f27, + skills-progress-tertiary: #ef9f27, + skills-progress-quaternary: #ef9f27, + skills-progress-quinary: #ef9f27, + skills-progress-senary: #ef9f27, ); diff --git a/src/scss/variables.scss b/src/scss/variables.scss index 70a43f1..2a5db6e 100644 --- a/src/scss/variables.scss +++ b/src/scss/variables.scss @@ -3,9 +3,9 @@ $mobile-breakpoint: 768px; $desktop-breakpoint: 769px; // Color variables -$color-primary: #9333ea; -$color-secondary: #2563eb; -$color-heading: #0f4d46; +$color-primary: #042c53; +$color-secondary: #0c447c; +$color-heading: #042c53; // Shadow variables $shadow-sm: 0 4px 12px; @@ -63,22 +63,17 @@ $shadow-button: 0 2px 4px -1px rgba(0, 0, 0, 0.06); // Gradient variables -$gradient-primary: linear-gradient( - 90deg, - $color-secondary 0%, - $color-primary 100% -); +$gradient-primary: #042c53; -// Light theme gradient (keep original) +// Light theme gradient $gradient-text-light: linear-gradient( to right, $color-secondary, - $color-primary, - #0d9488 + $color-primary ); -// Dark theme gradient (white to light purple for hero title) -$gradient-text-dark: linear-gradient(to right, #ffffff, #c4b5fd, #a78bfa); +// Dark theme gradient +$gradient-text-dark: linear-gradient(to right, #e6f1fb, #b5d4f4); // Default gradient for backwards compatibility $gradient-text: $gradient-text-light; From 63002b70b34a47fe26318aaa13072c50a230ee29 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 16 Apr 2026 21:20:28 +0200 Subject: [PATCH 13/19] refactor: remove unused personalConfig import from Navigation component --- src/components/layout/Navigation.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/layout/Navigation.tsx b/src/components/layout/Navigation.tsx index cace1ed..5013be2 100644 --- a/src/components/layout/Navigation.tsx +++ b/src/components/layout/Navigation.tsx @@ -3,7 +3,6 @@ import { Link, useLocation, useNavigate } from 'react-router-dom'; import ThemeToggle from '../ThemeToggle'; import LanguageToggle from '../LanguageToggle'; import MobileMenu from './MobileMenu'; -import { personalConfig } from '../../config/personal'; import logoUrl from '../../assets/logo.svg'; import { useLanguage } from '../../contexts/LanguageContext'; import { scrollToSection } from '../../utils/scrollUtils'; From dfe535eb1bba95623bab4e74ed0d7ac81e07c421 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 22 Apr 2026 13:13:32 +0200 Subject: [PATCH 14/19] feat: enhance German localization for landing sections with updated descriptions and new legal compliance information --- src/config/locales/de/landing/landing-experience.ts | 2 +- src/config/locales/de/landing/landing-hero.ts | 2 +- src/config/locales/de/landing/landing-reasons.ts | 4 ++++ src/config/locales/de/landing/landing-winnings.ts | 5 +++-- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/config/locales/de/landing/landing-experience.ts b/src/config/locales/de/landing/landing-experience.ts index 241f13f..6d31a7a 100644 --- a/src/config/locales/de/landing/landing-experience.ts +++ b/src/config/locales/de/landing/landing-experience.ts @@ -3,7 +3,7 @@ export const landingExperience = { paragraphs: [ 'Ich habe Unternehmen wie deins geholfen – von kleinen Läden bis hin zu Dienstleistern – ihre Webseiten barrierefrei zu machen.', 'Bei ORWO habe ich einen Fotobuch-Designer entwickelt, der für tausende Kunden funktionierte, auch für Menschen mit Sehbeeinträchtigungen. Ich habe ihn nicht nur gut aussehen lassen, ich habe ihn für alle zum Laufen gebracht.', - 'Diese Sorgfalt bringe ich jetzt zu deiner Webseite, egal ob Bäckerei, Therapeut, Handwerker oder Kiosk.', + 'Diese Sorgfalt bringe ich jetzt zu deiner Webseite, egal ob Bäckerei, Therapeut, Handwerker oder Online-Shop.', 'Barrierefreiheit bedeutet Respekt und mehr Reichweite für dich und deine Produkte.', ], }; diff --git a/src/config/locales/de/landing/landing-hero.ts b/src/config/locales/de/landing/landing-hero.ts index 93d234c..136a2a0 100644 --- a/src/config/locales/de/landing/landing-hero.ts +++ b/src/config/locales/de/landing/landing-hero.ts @@ -1,5 +1,5 @@ export const landingHero = { title: 'Barrierefreiheit: 20\u00A0% mehr Reichweite, 100\u00A0% mehr Zugang.', - description: 'Ich helfe Ihnen, Ihre Website für jeden zugänglich zu machen – im Einklang mit dem BFSG.', + description: 'Das BFSG gilt seit Juni 2025. Ich mache deine Website barrierefrei und rechtssicher – bevor Haftungsrisiken entstehen.', tertiaryButtonText: 'Kontakt aufnehmen', }; diff --git a/src/config/locales/de/landing/landing-reasons.ts b/src/config/locales/de/landing/landing-reasons.ts index 75cb3c6..c146816 100644 --- a/src/config/locales/de/landing/landing-reasons.ts +++ b/src/config/locales/de/landing/landing-reasons.ts @@ -10,6 +10,10 @@ export const landingReasons = { stat: '80\u00A0%', text: 'dieser Menschen verlassen Webseiten, die nicht für sie funktionieren.', }, + { + stat: 'BFSG', + text: 'Seit Juni 2025 sind viele Unternehmen gesetzlich verpflichtet. Wer nicht handelt, riskiert Abmahnungen und Haftung.', + }, { stat: 'SEO', text: 'Google belohnt barrierefreie Webseiten mit besseren Rankings, mehr Traffic und mehr Umsatz.', diff --git a/src/config/locales/de/landing/landing-winnings.ts b/src/config/locales/de/landing/landing-winnings.ts index ecd44f3..29fdc39 100644 --- a/src/config/locales/de/landing/landing-winnings.ts +++ b/src/config/locales/de/landing/landing-winnings.ts @@ -4,9 +4,10 @@ export const landingWinnings = { items: [ { text: 'Eine Webseite, die für alle funktioniert, egal wie sie surfen.' }, { text: 'Mehr Besucher, denn Google liebt barrierefreie Seiten.' }, - { text: 'Mehr Umsatz, denn Menschen kaufen, wenn sie sich eingeschlossen fühlen.' }, + { text: 'Mehr Umsatz, denn Menschen kaufen, wenn sie sich einbezogen fühlen.' }, { text: 'Mehr Vertrauen, denn deine Kunden fühlen sich respektiert.' }, - { text: 'Entspannung, denn ich kümmere mich um alles, vom Testen bis zur rechtlichen Konformität.' }, + { text: 'Rechtssicherheit, denn ich stelle sicher, dass deine Webseite die BFSG-Anforderungen erfüllt.' }, + { text: 'Entspannung, denn ich kümmere mich um alles, vom Testen bis zur Konformitätsprüfung.' }, { text: 'Kein Tech-Stress, denn ich erkläre alles in einfacher Sprache, ohne Fachjargon.' }, ], outro: 'Ich stelle sicher, dass ich dein Problem verstehe und meine Lösung passt.', From 8deb151241c5ee1d60cf2052986815f5911840bb Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 30 Apr 2026 13:29:08 +0200 Subject: [PATCH 15/19] Refactor code structure for improved readability and maintainability --- package-lock.json | 20 +++++++ package.json | 1 + src/assets/sascha_pixel.svg | 3 ++ src/components/sections/AboutSection.tsx | 34 +++++++++++- src/components/sections/HeroSection.tsx | 14 ++++- src/scss/layout/topbar.scss | 19 ++++--- src/scss/sections/about-section.scss | 44 ++++++++++++++++ src/scss/sections/hero-section.scss | 67 ++++++++++++++++++++++++ src/scss/variables.scss | 4 +- 9 files changed, 196 insertions(+), 10 deletions(-) create mode 100644 src/assets/sascha_pixel.svg 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; From b28a26b7e92153a64e9a8e9f902e0a33d9ec48d6 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 7 May 2026 14:38:39 +0200 Subject: [PATCH 16/19] feat: add VAT ID and update privacy policy dates in imprint sections for localization Co-authored-by: Copilot --- src/config/locales/de/imprint.ts | 3 ++- src/config/locales/en/TextConfig.ts | 2 ++ src/config/locales/en/imprint.ts | 3 ++- src/pages/ImprintPage.tsx | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/config/locales/de/imprint.ts b/src/config/locales/de/imprint.ts index 3ad5bc1..d51b83a 100644 --- a/src/config/locales/de/imprint.ts +++ b/src/config/locales/de/imprint.ts @@ -2,6 +2,7 @@ export const imprint = { title: 'Impressum und Datenschutz', subtitle: 'Rechtliche Informationen', companyInfoTitle: 'Angaben gemäß § 5 DDG', + umsatzsteuerID: 'Umsatzsteuer-ID: DE316934637', contactTitle: 'Kontakt', responsibilityTitle: 'Vertretungsberechtigt', disclaimerTitle: 'Haftungsausschluss', @@ -170,7 +171,7 @@ export const imprint = { 'Wir treffen im Übrigen technische und organisatorische Sicherheitsmaßnahmen nach dem Stand der Technik, um die Vorschriften der Datenschutzgesetze einzuhalten und Ihre Daten gegen zufällige oder vorsätzliche Manipulationen, teilweisen oder vollständigen Verlust, Zerstörung oder gegen den unbefugten Zugriff Dritter zu schützen.', changesTitle: 'Aktualität und Änderung dieser Datenschutzerklärung', changesText: - 'Diese Datenschutzerklärung ist aktuell gültig und hat den Stand September 2025. Aufgrund geänderter gesetzlicher bzw. behördlicher Vorgaben kann es notwendig werden, diese Datenschutzerklärung anzupassen.', + 'Diese Datenschutzerklärung ist aktuell gültig und hat den Stand Mai 2026. Aufgrund geänderter gesetzlicher bzw. behördlicher Vorgaben kann es notwendig werden, diese Datenschutzerklärung anzupassen.', disclaimer: 'Diese Datenschutzerklärung wurde mit Hilfe des Datenschutz-Generators von SOS Recht erstellt. SOS Recht ist ein Angebot der Mueller.legal Rechtsanwälte Partnerschaft mit Sitz in Berlin.', }, diff --git a/src/config/locales/en/TextConfig.ts b/src/config/locales/en/TextConfig.ts index b81ec89..2a70e68 100644 --- a/src/config/locales/en/TextConfig.ts +++ b/src/config/locales/en/TextConfig.ts @@ -188,6 +188,8 @@ export interface TextConfig { // Imprint Page imprint: { + umsatzsteuerID: string; + vatID: string; title: string; subtitle: string; companyInfoTitle: string; diff --git a/src/config/locales/en/imprint.ts b/src/config/locales/en/imprint.ts index 25a5bd5..a2727cf 100644 --- a/src/config/locales/en/imprint.ts +++ b/src/config/locales/en/imprint.ts @@ -2,6 +2,7 @@ export const imprint = { title: 'Imprint and Privacy Policy', subtitle: 'Legal Information', companyInfoTitle: 'Information pursuant to § 5 DDG', + vatID: 'VAT ID: DE316934637', contactTitle: 'Contact', responsibilityTitle: 'Authorized Representative', disclaimerTitle: 'Disclaimer', @@ -170,7 +171,7 @@ export const imprint = { 'We also take technical and organizational security measures in accordance with the state of the art to comply with data protection laws and to protect your data against accidental or intentional manipulation, partial or complete loss, destruction, or unauthorized access by third parties.', changesTitle: 'Currency and amendments to this privacy policy', changesText: - 'This privacy policy is currently valid and was last updated in September 2025. Due to changes in legislation or official requirements, it may become necessary to amend this privacy policy.', + 'This privacy policy is currently valid and was last updated in May 2026. Due to changes in legislation or official requirements, it may become necessary to amend this privacy policy.', disclaimer: 'This privacy policy was created with the help of the data protection generator by SOS Recht. SOS Recht is an offering of Mueller.legal Attorneys at Law, based in Berlin.', }, diff --git a/src/pages/ImprintPage.tsx b/src/pages/ImprintPage.tsx index b572d1f..d173402 100644 --- a/src/pages/ImprintPage.tsx +++ b/src/pages/ImprintPage.tsx @@ -30,6 +30,7 @@ export default function ImprintPage() {

{texts.address.street}

{texts.address.city}

{texts.address.country}

+

{texts.umsatzsteuerID || texts.vatID}

From e7fa5dcb8df3c285ac9f3935dbe52cc896d50474 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 7 May 2026 14:40:22 +0200 Subject: [PATCH 17/19] feat: add VAT ID to imprint sections for German and English localization Co-authored-by: Copilot --- src/config/locales/de/imprint.ts | 1 + src/config/locales/en/imprint.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/config/locales/de/imprint.ts b/src/config/locales/de/imprint.ts index d51b83a..9e1edd3 100644 --- a/src/config/locales/de/imprint.ts +++ b/src/config/locales/de/imprint.ts @@ -3,6 +3,7 @@ export const imprint = { subtitle: 'Rechtliche Informationen', companyInfoTitle: 'Angaben gemäß § 5 DDG', umsatzsteuerID: 'Umsatzsteuer-ID: DE316934637', + vatID: 'VAT ID: DE316934637', contactTitle: 'Kontakt', responsibilityTitle: 'Vertretungsberechtigt', disclaimerTitle: 'Haftungsausschluss', diff --git a/src/config/locales/en/imprint.ts b/src/config/locales/en/imprint.ts index a2727cf..ef09d58 100644 --- a/src/config/locales/en/imprint.ts +++ b/src/config/locales/en/imprint.ts @@ -2,6 +2,7 @@ export const imprint = { title: 'Imprint and Privacy Policy', subtitle: 'Legal Information', companyInfoTitle: 'Information pursuant to § 5 DDG', + umsatzsteuerID: 'Umsatzsteuer-ID: DE316934637', vatID: 'VAT ID: DE316934637', contactTitle: 'Contact', responsibilityTitle: 'Authorized Representative', From 051d614364ddcddb87584cd8e4cf5a5ddd715858 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 20 May 2026 13:26:09 +0200 Subject: [PATCH 18/19] feat: add business registration information to imprint sections for German and English localization --- src/config/locales/de/imprint.ts | 7 +++++++ src/config/locales/en/imprint.ts | 7 +++++++ src/pages/ImprintPage.tsx | 10 ++++++++++ 3 files changed, 24 insertions(+) diff --git a/src/config/locales/de/imprint.ts b/src/config/locales/de/imprint.ts index 9e1edd3..900304e 100644 --- a/src/config/locales/de/imprint.ts +++ b/src/config/locales/de/imprint.ts @@ -4,6 +4,13 @@ export const imprint = { companyInfoTitle: 'Angaben gemäß § 5 DDG', umsatzsteuerID: 'Umsatzsteuer-ID: DE316934637', vatID: 'VAT ID: DE316934637', + businessRegistrationTitle: 'Gewerbeanmeldung', + businessRegistration: { + title: 'Gewerbeerlaubnis nach § 34c GewO', + dateIssued: '18.03.2026', + issuedBy: 'Bezirksamt Tempelhof-Schöneberg von Berlin', + authority: 'Ordnungsamt', + }, contactTitle: 'Kontakt', responsibilityTitle: 'Vertretungsberechtigt', disclaimerTitle: 'Haftungsausschluss', diff --git a/src/config/locales/en/imprint.ts b/src/config/locales/en/imprint.ts index ef09d58..5b4d396 100644 --- a/src/config/locales/en/imprint.ts +++ b/src/config/locales/en/imprint.ts @@ -4,6 +4,13 @@ export const imprint = { companyInfoTitle: 'Information pursuant to § 5 DDG', umsatzsteuerID: 'Umsatzsteuer-ID: DE316934637', vatID: 'VAT ID: DE316934637', + businessRegistrationTitle: 'Business Registration', + businessRegistration: { + title: 'Business License pursuant to § 34c GewO', + dateIssued: '18.03.2026', + issuedBy: 'District Office Tempelhof-Schöneberg of Berlin', + authority: 'Regulatory Authority', + }, contactTitle: 'Contact', responsibilityTitle: 'Authorized Representative', disclaimerTitle: 'Disclaimer', diff --git a/src/pages/ImprintPage.tsx b/src/pages/ImprintPage.tsx index d173402..5ec569f 100644 --- a/src/pages/ImprintPage.tsx +++ b/src/pages/ImprintPage.tsx @@ -34,6 +34,16 @@ export default function ImprintPage() {
+
+

{texts.businessRegistrationTitle}

+
+

{texts.businessRegistration.title}

+

Datum der Erteilung: {texts.businessRegistration.dateIssued}

+

Erteilt von: {texts.businessRegistration.issuedBy}

+

{texts.businessRegistration.authority}

+
+
+

{texts.contactTitle}

From 4190be4d15be6bacaa1415563dacefced5ec5f34 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 20 May 2026 13:32:32 +0200 Subject: [PATCH 19/19] feat: add business registration section to TextConfig interface --- src/config/locales/en/TextConfig.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/config/locales/en/TextConfig.ts b/src/config/locales/en/TextConfig.ts index 2a70e68..de84c09 100644 --- a/src/config/locales/en/TextConfig.ts +++ b/src/config/locales/en/TextConfig.ts @@ -193,6 +193,13 @@ export interface TextConfig { title: string; subtitle: string; companyInfoTitle: string; + businessRegistrationTitle: string; + businessRegistration: { + title: string; + dateIssued: string; + issuedBy: string; + authority: string; + }; contactTitle: string; responsibilityTitle: string; disclaimerTitle: string;