From cda1da02a36be661779c817383052c3da5f8912a Mon Sep 17 00:00:00 2001 From: Havoc Date: Fri, 28 Aug 2026 18:10:06 +0300 Subject: [PATCH 1/9] redesign site with refined dark theme and electric lime accent Full visual overhaul on the SvelteKit site, no content or data changes: - New design system: dark ink palette (#060907) with electric lime accent, Space Grotesk display type, Inter body, Fira Mono for labels - Self-hosted fonts via @fontsource, replacing Google Fonts CDN - Replace FontAwesome kit CDN with unplugin-icons tree-shaken inline SVGs, eliminating all external icon and font requests - Shared components: PageHero, SectionHeading, Card, Tag, StatBlock, SocialLinks; scroll-reveal action with IntersectionObserver and staggered delays, respects prefers-reduced-motion - Rebuilt every page: home, intro, experience, skills, achievements, certificates, contact, ideal-role, download; print page separators swapped for dot and hyphen characters - Certificates page keeps all 13 entries, skill filter, both modals, and the badge marquee (CSS animation, pauses on hover) - Dark-only design, theme toggle and light tokens removed - Performance: hackathon image 1.9MB PNG to 215KB WebP, profile photo 392KB to 35KB WebP, unreferenced static assets removed - Remove superseded global SCSS files; layout CSS fixes sidebar offset so it no longer overlaps page content Co-Authored-By: Claude --- web/package-lock.json | 300 +- web/package.json | 7 +- web/src/app.css | 175 +- web/src/app.html | 17 +- web/src/components/Card.svelte | 57 + web/src/components/Language.svelte | 13 +- web/src/components/PageHero.svelte | 69 + web/src/components/SectionHeading.svelte | 70 + web/src/components/SocialLinks.svelte | 90 + web/src/components/StatBlock.svelte | 41 + web/src/components/Tag.svelte | 30 + web/src/icons.d.ts | 1 + web/src/lib/actions/reveal.ts | 40 + web/src/lib/config/certificates.ts | 2 +- web/src/lib/stores/theme.ts | 38 - web/src/routes/+layout.svelte | 956 ++++-- web/src/routes/+page.svelte | 811 +++-- web/src/routes/achievements/+page.svelte | 1174 ++++---- web/src/routes/certificates/+page.svelte | 2664 +++++++---------- web/src/routes/contact/+page.svelte | 688 ++--- web/src/routes/download/+page.svelte | 100 +- web/src/routes/experience/+page.svelte | 657 ++-- web/src/routes/ideal-role/+page.svelte | 503 ++-- web/src/routes/intro/+page.svelte | 551 ++-- web/src/routes/intro/+page.svelte.new | 390 --- web/src/routes/print/+page.svelte | 16 +- web/src/routes/skills/+page.svelte | 991 +++--- web/src/styles/link.scss | 148 - web/src/styles/other-pages.scss | 158 - web/src/styles/page-global.scss | 668 ----- web/src/styles/resume-main.scss | 917 ------ web/src/styles/variables.scss | 221 +- web/static/assets/Hackathon-win-pictures.png | Bin 1916619 -> 0 bytes web/static/assets/Hackathon-win-pictures.webp | Bin 0 -> 215644 bytes web/static/assets/image.png | Bin 242846 -> 0 bytes web/static/assets/wmremove-transformed.png | Bin 256723 -> 0 bytes web/static/favicon_new.png | Bin 319 -> 0 bytes web/static/profile-picture.jpg | Bin 397503 -> 0 bytes web/static/profile-picture.webp | Bin 0 -> 35026 bytes web/svelte.config.js | 5 +- web/vite.config.ts | 10 +- 41 files changed, 5230 insertions(+), 7348 deletions(-) create mode 100644 web/src/components/Card.svelte create mode 100644 web/src/components/PageHero.svelte create mode 100644 web/src/components/SectionHeading.svelte create mode 100644 web/src/components/SocialLinks.svelte create mode 100644 web/src/components/StatBlock.svelte create mode 100644 web/src/components/Tag.svelte create mode 100644 web/src/icons.d.ts create mode 100644 web/src/lib/actions/reveal.ts delete mode 100644 web/src/lib/stores/theme.ts delete mode 100644 web/src/routes/intro/+page.svelte.new delete mode 100644 web/src/styles/link.scss delete mode 100644 web/src/styles/other-pages.scss delete mode 100644 web/src/styles/page-global.scss delete mode 100644 web/src/styles/resume-main.scss delete mode 100644 web/static/assets/Hackathon-win-pictures.png create mode 100644 web/static/assets/Hackathon-win-pictures.webp delete mode 100644 web/static/assets/image.png delete mode 100644 web/static/assets/wmremove-transformed.png delete mode 100644 web/static/favicon_new.png delete mode 100644 web/static/profile-picture.jpg create mode 100644 web/static/profile-picture.webp diff --git a/web/package-lock.json b/web/package-lock.json index e29555b..11e7d9a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,6 +8,9 @@ "name": "web", "version": "0.0.1", "dependencies": { + "@fontsource/fira-mono": "^5.3.0", + "@fontsource/inter": "^5.3.0", + "@fontsource/space-grotesk": "^5.3.0", "@sparticuz/chromium": "^126.0.0", "@vercel/speed-insights": "^1.3.1", "bufferutil": "^4.1.0", @@ -15,7 +18,8 @@ "utf-8-validate": "^6.0.6" }, "devDependencies": { - "@fontsource/fira-mono": "^5.1.0", + "@iconify-json/fa6-brands": "^1.2.6", + "@iconify-json/fa6-solid": "^1.2.4", "@neoconfetti/svelte": "^2.2.1", "@sveltejs/adapter-auto": "3.3.1", "@sveltejs/adapter-vercel": "6.0.0", @@ -38,6 +42,7 @@ "svelte-check": "^4.4.6", "typescript": "^5.7.3", "typescript-eslint": "^8.58.0", + "unplugin-icons": "^23.0.1", "vite": "^5.4.21" }, "engines": { @@ -58,6 +63,20 @@ "node": ">=6.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -658,10 +677,27 @@ } }, "node_modules/@fontsource/fira-mono": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-5.2.7.tgz", - "integrity": "sha512-wYrAn6i3nH6luqQBZxtWUpl4UTUvs9AEbEeZxksPMwIqyjRRaxHTNW3c2VfM50gabS2IS7pT8lVWS2USB4ukYA==", - "dev": true, + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/fira-mono/-/fira-mono-5.3.0.tgz", + "integrity": "sha512-FZzRwKhpoXQU1uhEGKUg5AHUUdfiaTXxGCdBBeGHUmcZaqEDEmndSVvpXHkSViH25f/UiWfhcWTib+6A8o4qeQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/space-grotesk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource/space-grotesk/-/space-grotesk-5.3.0.tgz", + "integrity": "sha512-ksnGizDPXIDuvqcTYTSrmZ+evx9sDlS8rp7+42BQ7wU+spt3twEoXfbJz672C+5CLg6VeUQwRy5RXshWb67LcQ==", "license": "OFL-1.1", "funding": { "url": "https://github.com/sponsors/ayuhito" @@ -719,6 +755,45 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify-json/fa6-brands": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@iconify-json/fa6-brands/-/fa6-brands-1.2.6.tgz", + "integrity": "sha512-twL3X4KWcxAhbc1vz/mIDsVr+CAItk1/EIfxKUVQtpv6O4eydk5KNYqTZWdvJNHGInUgd6vKg21aWfVgb5DXEg==", + "dev": true, + "license": "CC-BY-4.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/fa6-solid": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@iconify-json/fa6-solid/-/fa6-solid-1.2.4.tgz", + "integrity": "sha512-LmDNNdJVyvF5mPm1yxWvL8KjCc/E8LzoqnF1LNTVpyY2ZJRUlGOWuPIThdbuFBF2IovgttkIyumhyqfmlHdwKg==", + "dev": true, + "license": "CC-BY-4.0", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -846,6 +921,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2153,9 +2239,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "devOptional": true, "license": "MIT", "bin": { @@ -2618,6 +2704,13 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "dev": true, + "license": "MIT" + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -3119,6 +3212,13 @@ "bare-events": "^2.7.0" } }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "dev": true, + "license": "MIT" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -3701,6 +3801,24 @@ "node": ">=10" } }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -3810,6 +3928,38 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -3926,6 +4076,20 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/ometa": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/ometa/-/ometa-0.2.2.tgz", @@ -4033,6 +4197,13 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4083,6 +4254,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -4131,6 +4309,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -4365,6 +4555,23 @@ "node": ">=18" } }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -4916,6 +5123,16 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -5026,6 +5243,13 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, "node_modules/unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", @@ -5043,6 +5267,59 @@ "devOptional": true, "license": "MIT" }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-icons": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/unplugin-icons/-/unplugin-icons-23.0.1.tgz", + "integrity": "sha512-rv0XEJepajKzDLvRUWASM8K+8+/CCfZn2jtogXqg6RIp7kpatRc/aFrVJn8ANQA09e++lPEEv9yX8cC9enc+QQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/utils": "^3.1.0", + "local-pkg": "^1.1.2", + "obug": "^2.1.1", + "unplugin": "^2.3.11" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@svgr/core": ">=7.0.0", + "@svgx/core": "^1.0.1", + "@vue/compiler-sfc": "^3.0.2", + "svelte": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@svgr/core": { + "optional": true + }, + "@svgx/core": { + "optional": true + }, + "@vue/compiler-sfc": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5562,6 +5839,13 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/web/package.json b/web/package.json index d9f2ebf..5a36fa3 100644 --- a/web/package.json +++ b/web/package.json @@ -15,7 +15,8 @@ "format": "prettier --write ." }, "devDependencies": { - "@fontsource/fira-mono": "^5.1.0", + "@iconify-json/fa6-brands": "^1.2.6", + "@iconify-json/fa6-solid": "^1.2.4", "@neoconfetti/svelte": "^2.2.1", "@sveltejs/adapter-auto": "3.3.1", "@sveltejs/adapter-vercel": "6.0.0", @@ -38,11 +39,15 @@ "svelte-check": "^4.4.6", "typescript": "^5.7.3", "typescript-eslint": "^8.58.0", + "unplugin-icons": "^23.0.1", "vite": "^5.4.21" }, "type": "module", "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "dependencies": { + "@fontsource/fira-mono": "^5.3.0", + "@fontsource/inter": "^5.3.0", + "@fontsource/space-grotesk": "^5.3.0", "@sparticuz/chromium": "^126.0.0", "@vercel/speed-insights": "^1.3.1", "bufferutil": "^4.1.0", diff --git a/web/src/app.css b/web/src/app.css index cc34e61..174cc17 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,6 +1,9 @@ -@import '@fontsource/fira-mono'; +/* Fonts are self-hosted via @fontsource and imported in +layout.svelte: + zero external font requests, tree-shaken woff2 subsets. */ -/* Global Styles - Enhanced with Modern Typography & Layout */ +/* ========================================================================== + Base styles, typography, and shared utilities + ========================================================================== */ *, *::before, *::after { @@ -15,6 +18,7 @@ html { -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; scroll-behavior: smooth; + color-scheme: dark; } body { @@ -31,28 +35,30 @@ body { /* Typography */ h1, h2, h3, h4, h5, h6 { + font-family: var(--font-display); font-weight: 600; line-height: var(--line-height-tight); + letter-spacing: var(--tracking-display); color: var(--text-color); margin: 0; } h1 { - font-size: clamp(var(--font-size-2xl), 5vw, var(--font-size-4xl)); + font-size: clamp(var(--font-size-3xl), 6vw, var(--font-size-6xl)); font-weight: 700; } h2 { - font-size: clamp(var(--font-size-xl), 4vw, var(--font-size-3xl)); - font-weight: 600; + font-size: clamp(var(--font-size-2xl), 4vw, var(--font-size-4xl)); + font-weight: 700; } h3 { - font-size: clamp(var(--font-size-lg), 3vw, var(--font-size-2xl)); + font-size: clamp(var(--font-size-xl), 3vw, var(--font-size-2xl)); } h4 { - font-size: clamp(var(--font-size-base), 2.5vw, var(--font-size-xl)); + font-size: clamp(var(--font-size-lg), 2.5vw, var(--font-size-xl)); } h5 { @@ -78,20 +84,31 @@ em, i { font-style: italic; } +/* Mono label utility: small uppercase eyebrows and terminal-style tags */ +.label, +.eyebrow { + font-family: var(--font-mono); + font-size: var(--font-size-xs); + font-weight: 500; + letter-spacing: var(--tracking-label); + text-transform: uppercase; + color: var(--accent); +} + /* Links */ a { - color: var(--primary); + color: var(--accent); text-decoration: none; transition: color var(--transition-fast); cursor: pointer; } a:hover { - color: var(--primary-hover); + color: var(--accent-dim); } a:focus-visible { - outline: 2px solid var(--primary); + outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--radius-sm); } @@ -113,10 +130,10 @@ pre { } code { - background: var(--surface); + background: var(--accent-transparent); padding: 0.125rem 0.375rem; border-radius: var(--radius-sm); - color: var(--primary); + color: var(--accent); } pre code { @@ -125,7 +142,7 @@ pre code { color: inherit; } -/* Form Elements */ +/* Form elements */ input, textarea, select, @@ -150,7 +167,7 @@ button:disabled { } button:focus-visible { - outline: 2px solid var(--primary); + outline: 2px solid var(--accent); outline-offset: 2px; } @@ -168,8 +185,8 @@ input:focus, textarea:focus, select:focus { outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 3px var(--primary-transparent); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-transparent); } /* Lists */ @@ -179,48 +196,41 @@ ul, ol { /* Selection */ ::selection { - background: var(--primary); + background: var(--accent); color: var(--background); } ::-moz-selection { - background: var(--primary); + background: var(--accent); color: var(--background); } /* Scrollbar */ ::-webkit-scrollbar { - width: 12px; - height: 12px; + width: 10px; + height: 10px; } ::-webkit-scrollbar-track { - background: var(--surface); - border-radius: var(--radius-md); + background: var(--background); } ::-webkit-scrollbar-thumb { - background: var(--surface-elevated); - border-radius: var(--radius-md); - border: 2px solid var(--surface); -} - -::-webkit-scrollbar-thumb:hover { background: var(--surface-hover); + border-radius: var(--radius-full); + border: 2px solid var(--background); } -/* Utilities */ -.text-center { - text-align: center; -} - -.text-left { - text-align: left; +::-webkit-scrollbar-thumb:hover { + background: var(--text-color-muted); } -.text-right { - text-align: right; -} +/* ========================================================================== + Utilities + ========================================================================== */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } .text-column { display: flex; @@ -243,26 +253,83 @@ ul, ol { border-width: 0; } -/* Responsive Images */ +/* Subtle engineering grid backdrop for hero zones. CSS-only, no image. */ +.grid-backdrop { + position: absolute; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(var(--border-color) 1px, transparent 1px), + linear-gradient(90deg, var(--border-color) 1px, transparent 1px); + background-size: 44px 44px; + -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, black 30%, transparent 75%); + mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, black 30%, transparent 75%); +} + +/* Soft lime glow placed behind hero content */ +.hero-glow { + position: absolute; + top: -20%; + left: 50%; + width: min(700px, 90vw); + height: min(500px, 70vh); + transform: translateX(-50%); + pointer-events: none; + background: radial-gradient(ellipse at center, var(--accent-glow) 0%, transparent 65%); + filter: blur(8px); +} + +/* ========================================================================== + Scroll reveal - paired with the reveal action (lib/actions/reveal.ts) + ========================================================================== */ +[data-reveal] { + opacity: 0; + transform: translateY(18px); + transition: + opacity 600ms cubic-bezier(0.22, 1, 0.36, 1), + transform 600ms cubic-bezier(0.22, 1, 0.36, 1); + transition-delay: var(--reveal-delay, 0ms); +} + +[data-reveal][data-revealed='true'] { + opacity: 1; + transform: none; +} + +/* ========================================================================== + Motion preferences + ========================================================================== */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + [data-reveal] { + opacity: 1; + transform: none; + transition: none; + } + + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* Responsive images */ img { max-width: 100%; height: auto; display: block; } -/* Focus Visible Polyfill */ -.js-focus-visible :focus:not(.focus-visible) { - outline: none; -} - -/* Loading States */ +/* Loading states */ @keyframes shimmer { - 0% { - background-position: -1000px 0; - } - 100% { - background-position: 1000px 0; - } + 0% { background-position: -1000px 0; } + 100% { background-position: 1000px 0; } } .loading { @@ -277,17 +344,17 @@ img { background-size: 1000px 100%; } -/* Print Styles */ +/* Print styles */ @media print { body { background: white; color: black; } - + a { text-decoration: underline; } - + .no-print { display: none !important; } diff --git a/web/src/app.html b/web/src/app.html index 77e8ff2..b213a10 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -5,14 +5,13 @@ %sveltekit.head% - - - Daniel Wambua | CV - - + Daniel Wambua | Security Analyst + + + @@ -22,13 +21,13 @@ - - + + - - + + diff --git a/web/src/components/Card.svelte b/web/src/components/Card.svelte new file mode 100644 index 0000000..d6aca86 --- /dev/null +++ b/web/src/components/Card.svelte @@ -0,0 +1,57 @@ + + +{#if href} + + + +{:else} +
+ +
+{/if} + + diff --git a/web/src/components/Language.svelte b/web/src/components/Language.svelte index 99339f5..d62a51d 100644 --- a/web/src/components/Language.svelte +++ b/web/src/components/Language.svelte @@ -187,8 +187,8 @@ // Fallback for unmapped languages return { name: lang, - color: '06b6d4', // cyan color - icon: 'gnubash' // generic bash icon as fallback + color: '00ff88', + icon: 'gnubash' }; }; @@ -213,7 +213,7 @@ on:error={handleImageError} /> {:else} - 🔧 + {/if} {langAttributes.name} @@ -232,9 +232,14 @@ } .icon-fallback { - font-size: 1rem; + width: 1rem; + height: 1rem; margin-right: 0.25rem; display: inline-block; + border-radius: 50%; + background: var(--lang-color, var(--accent)); + opacity: 0.7; + vertical-align: -0.15em; } .language { diff --git a/web/src/components/PageHero.svelte b/web/src/components/PageHero.svelte new file mode 100644 index 0000000..fd74dd1 --- /dev/null +++ b/web/src/components/PageHero.svelte @@ -0,0 +1,69 @@ + + +
+ {#if eyebrow}

{eyebrow}

{/if} +

{title}

+ {#if summary}

{summary}

{/if} + {#if $$slots.actions} +
+ +
+ {/if} +
+ + diff --git a/web/src/components/SectionHeading.svelte b/web/src/components/SectionHeading.svelte new file mode 100644 index 0000000..cba19d3 --- /dev/null +++ b/web/src/components/SectionHeading.svelte @@ -0,0 +1,70 @@ + + +
+
+ {#if index}{index}{/if} +

{title}

+ {#if meta}{meta}{/if} +
+ + {#if $$slots.default} +
+ +
+ {/if} +
+ + diff --git a/web/src/components/SocialLinks.svelte b/web/src/components/SocialLinks.svelte new file mode 100644 index 0000000..b89cb91 --- /dev/null +++ b/web/src/components/SocialLinks.svelte @@ -0,0 +1,90 @@ + + + + + diff --git a/web/src/components/StatBlock.svelte b/web/src/components/StatBlock.svelte new file mode 100644 index 0000000..ac71df9 --- /dev/null +++ b/web/src/components/StatBlock.svelte @@ -0,0 +1,41 @@ + + +
+ {value} + {label} +
+ + diff --git a/web/src/components/Tag.svelte b/web/src/components/Tag.svelte new file mode 100644 index 0000000..fe017e9 --- /dev/null +++ b/web/src/components/Tag.svelte @@ -0,0 +1,30 @@ + + + + + + + diff --git a/web/src/icons.d.ts b/web/src/icons.d.ts new file mode 100644 index 0000000..cea0a70 --- /dev/null +++ b/web/src/icons.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/src/lib/actions/reveal.ts b/web/src/lib/actions/reveal.ts new file mode 100644 index 0000000..b1d701f --- /dev/null +++ b/web/src/lib/actions/reveal.ts @@ -0,0 +1,40 @@ +/** + * Scroll-reveal action. + * + * Marks the element with `data-reveal` immediately (so CSS hides it before it + * enters the viewport) and flips it to `data-revealed` once it intersects. + * Stagger siblings by setting a `--reveal-delay` inline style. + * + * Usage:
...
+ */ +export function reveal(node: HTMLElement, options?: { threshold?: number; rootMargin?: string }) { + const threshold = options?.threshold ?? 0.12; + const rootMargin = options?.rootMargin ?? '0px 0px -8% 0px'; + + node.setAttribute('data-reveal', ''); + + if (typeof IntersectionObserver === 'undefined') { + node.setAttribute('data-revealed', 'true'); + return; + } + + const observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + node.setAttribute('data-revealed', 'true'); + observer.unobserve(node); + } + } + }, + { threshold, rootMargin } + ); + + observer.observe(node); + + return { + destroy() { + observer.disconnect(); + } + }; +} diff --git a/web/src/lib/config/certificates.ts b/web/src/lib/config/certificates.ts index eddd161..37bad29 100644 --- a/web/src/lib/config/certificates.ts +++ b/web/src/lib/config/certificates.ts @@ -13,7 +13,7 @@ export const certificateImages = { // Connected Africa Summit 2026 CTF - 3rd Position (Team JOINT) connectedafricactf: '/certificates/connectedafricactf.jpg', - // NexHunt CTF — Top Performer + // NexHunt CTF, Top Performer nexhuntctf: '/certificates/nexhuntctf.jpg', // p3rf3ctr00t CTF Certificate diff --git a/web/src/lib/stores/theme.ts b/web/src/lib/stores/theme.ts deleted file mode 100644 index ea682e9..0000000 --- a/web/src/lib/stores/theme.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { writable } from 'svelte/store'; -import { browser } from '$app/environment'; - -type Theme = 'light' | 'dark'; - -// Get initial theme from localStorage or system preference -function getInitialTheme(): Theme { - if (!browser) return 'dark'; - - const stored = localStorage.getItem('theme') as Theme | null; - if (stored) return stored; - - // Check system preference - if (window.matchMedia('(prefers-color-scheme: light)').matches) { - return 'light'; - } - - return 'dark'; -} - -// Create theme store -export const theme = writable(getInitialTheme()); - -// Subscribe to theme changes and update DOM -if (browser) { - theme.subscribe((value) => { - localStorage.setItem('theme', value); - document.documentElement.setAttribute('data-theme', value); - }); - - // Apply initial theme - document.documentElement.setAttribute('data-theme', getInitialTheme()); -} - -// Theme toggle function -export function toggleTheme() { - theme.update((current) => (current === 'light' ? 'dark' : 'light')); -} diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index bf0a774..457ea48 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -4,12 +4,34 @@ import { page } from '$app/stores'; import { onNavigate } from '$app/navigation'; import { injectSpeedInsights } from '@vercel/speed-insights/sveltekit'; - import { theme, toggleTheme } from '$lib/stores/theme'; + import SocialLinks from '$components/SocialLinks.svelte'; + + import TerminalIcon from '~icons/fa6-solid/terminal'; + import ShieldIcon from '~icons/fa6-solid/shield-halved'; + import TrophyIcon from '~icons/fa6-solid/trophy'; + import CodeIcon from '~icons/fa6-solid/code'; + import CertificateIcon from '~icons/fa6-solid/certificate'; + import EnvelopeIcon from '~icons/fa6-solid/envelope'; + import HouseIcon from '~icons/fa6-solid/house'; + import FileDownIcon from '~icons/fa6-solid/file-arrow-down'; + import PaperPlaneIcon from '~icons/fa6-solid/paper-plane'; + import BriefcaseIcon from '~icons/fa6-solid/briefcase'; + import ArrowRightIcon from '~icons/fa6-solid/arrow-right'; + import BarsIcon from '~icons/fa6-solid/bars'; + import XMarkIcon from '~icons/fa6-solid/xmark'; + + import '@fontsource/space-grotesk/400.css'; + import '@fontsource/space-grotesk/500.css'; + import '@fontsource/space-grotesk/600.css'; + import '@fontsource/space-grotesk/700.css'; + import '@fontsource/inter/400.css'; + import '@fontsource/inter/500.css'; + import '@fontsource/inter/600.css'; + import '@fontsource/fira-mono/400.css'; + import '@fontsource/fira-mono/500.css'; import '../app.css'; import '../styles/variables.scss'; - import '../styles/link.scss'; - import '../styles/page-global.scss'; // Initialize Vercel Speed Insights injectSpeedInsights(); @@ -18,205 +40,177 @@ startViewTransition?: (callback: () => Promise) => void; } - onNavigate((navigation) => { + onNavigate((navigation) => { const doc = document as Document; - if (!doc.startViewTransition) return; - return new Promise((resolve) => { - doc.startViewTransition && doc.startViewTransition(async () => { - resolve(); - await navigation.complete; - }); - }); - }); - - let header: HTMLElement | null = null; - let main: HTMLElement | null = null; - - // Display banner if looking for job, within the specified dates. Format date into human readable string + if (!doc.startViewTransition) return; + return new Promise((resolve) => { + doc.startViewTransition && doc.startViewTransition(async () => { + resolve(); + await navigation.complete; + }); + }); + }); + + // Display banner if looking for job, within the specified dates const lookingForJobDates = { start: new Date('2025-07-01'), end: new Date('2026-12-30'), }; const currentDate = new Date(); - const startDateFormatted = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long' }).format(lookingForJobDates.start); + const startDateFormatted = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long' + }).format(lookingForJobDates.start); const isSeekingOpportunities = currentDate <= lookingForJobDates.end; const pagesToShowBanner = ['/', '/achievements', '/skills', '/experience', '/certificates']; - function handleScroll() { - if (header && main) { - if (main.scrollTop > 60) { - header.style.boxShadow = '24px 0 22px 0 #0000007d'; - header.style.zIndex = '13'; - } else { - header.style.boxShadow = ''; - header.style.zIndex = ''; - } - } - } - - onMount(() => { - if (browser) { - main?.addEventListener('scroll', handleScroll); - - return () => { - main?.removeEventListener('scroll', handleScroll); - }; - } - }); - - let path; - // @ts-ignore $: path = $page.url.pathname; + // Mobile drawer state + let drawerOpen = false; + $: if (drawerOpen && path) drawerOpen = false; + const socials = [ - { name: 'GitHub', url: 'https://github.com/Daniel-wambua', icon: 'fa-github', color: '#333' }, - { name: 'Twitter', url: 'https://twitter.com/wambuahdaniel', icon: 'fa-twitter', color: '#1DA1F2' }, - { name: 'LinkedIn', url: 'https://www.linkedin.com/in/daniel-wambua-a312b32b8', icon: 'fa-linkedin', color: '#0A66C2' }, - { name: 'CTFtime', url: 'https://ctftime.org/team/409004', icon: 'fa-flag', iconType: 'fa-solid', color: '#00ff41' }, - { name: 'Linktree', url: 'https://guns.lol/0xhavoc' , icon: 'fa-tree', iconType: 'fa-solid', color: '#43E55E' }, - ]; + { name: 'GitHub', url: 'https://github.com/Daniel-wambua', handle: 'Daniel-wambua', icon: 'github' }, + { name: 'Twitter', url: 'https://twitter.com/wambuahdaniel', handle: 'wambuahdaniel', icon: 'twitter' }, + { + name: 'LinkedIn', + url: 'https://www.linkedin.com/in/daniel-wambua-a312b32b8', + handle: 'daniel-wambua', + icon: 'linkedin' + }, + { name: 'CTFtime', url: 'https://ctftime.org/team/409004', handle: 'team 409004', icon: 'ctftime' }, + { name: 'Linktree', url: 'https://guns.lol/0xhavoc', handle: '0xhavoc', icon: 'linktree' } + ]; + const navLinks = [ - { name: 'Intro', url: '/intro', icon: 'fa-terminal' }, - { name: 'Experience', url: '/experience', icon: 'fa-shield-halved' }, - { name: 'Achievements', url: '/achievements', icon: 'fa-trophy' }, - { name: 'Skills', url: '/skills', icon: 'fa-code' }, - { name: 'Certificates', url: '/certificates', icon: 'fa-certificate' }, - { name: 'Contact', url: '/contact', icon: 'fa-envelope' }, + { name: 'Intro', url: '/intro', icon: TerminalIcon }, + { name: 'Experience', url: '/experience', icon: ShieldIcon }, + { name: 'Achievements', url: '/achievements', icon: TrophyIcon }, + { name: 'Skills', url: '/skills', icon: CodeIcon }, + { name: 'Certificates', url: '/certificates', icon: CertificateIcon }, + { name: 'Contact', url: '/contact', icon: EnvelopeIcon } ]; const headerLinks = [ - { name: 'Security Blog', url: 'https://havocsec.dev/blog' }, + { name: 'Blog', url: 'https://havocsec.dev/blog' }, { name: 'CTF Writeups', url: 'https://havocsec.dev/ctf/' }, - { name: 'Research', url: 'https://havocsec.dev/pentesting' }, + { name: 'Research', url: 'https://havocsec.dev/pentesting' } ]; + e.key === 'Escape' && (drawerOpen = false)} /> +
- diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 58253c3..aceef22 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -14,9 +14,22 @@ import TrophyIcon from '~icons/fa6-solid/trophy'; import LinkIcon from '~icons/fa6-solid/link'; import ShieldIcon from '~icons/fa6-solid/shield-halved'; + import CrosshairsIcon from '~icons/fa6-solid/crosshairs'; + import SatelliteDishIcon from '~icons/fa6-solid/satellite-dish'; + import BugIcon from '~icons/fa6-solid/bug'; export let data: any; + const focusAreas = [ + { label: 'Penetration testing', icon: CrosshairsIcon }, + { label: 'SOC operations', icon: SatelliteDishIcon }, + { label: 'Application security', icon: BugIcon } + ]; + + // Certificate activity window start year; the end year always rolls forward + const certStartYear = 2023; + const currentYear = new Date().getFullYear(); + const prettifyUrl = (url: string) => url.replace(/(^\w+:|^)\/\//, ''); const formatDate = (date: string) => { @@ -50,13 +63,26 @@
-

Penetration testing, SOC operations, application security

+
+ {#each focusAreas as area, i} + + + {area.label} + + {/each} +

{data.basics.name}

-

- {data['personal-statement']} -

+
+

+ {data['personal-statement']} +

+
@@ -225,7 +251,7 @@ - 5+ certificates earned between 2023 and 2026 + 5+ certificates earned between {certStartYear} and {currentYear} Browse the full gallery with verification links. @@ -240,8 +266,19 @@ /* ============ Hero ============ */ .hero { position: relative; - padding: var(--space-3xl) 0 var(--space-2xl); + padding: var(--space-2xl) var(--space-2xl) var(--space-xl); overflow: hidden; + border: 1px solid var(--glass-border); + border-radius: var(--radius-xl); + background: var(--glass-soft); + backdrop-filter: blur(14px) saturate(135%); + -webkit-backdrop-filter: blur(14px) saturate(135%); + box-shadow: var(--shadow-lg), inset 0 1px 0 var(--glass-highlight); + margin-bottom: var(--space-xl); + + @media (max-width: 600px) { + padding: var(--space-xl) var(--space-lg); + } } .hero-inner { @@ -249,19 +286,63 @@ max-width: 52rem; } + .focus-row { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + } + + .focus-chip { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.45rem 0.9rem; + border: 1px solid var(--border-color-accent); + border-radius: var(--radius-full); + background: var(--accent-transparent); + backdrop-filter: blur(10px) saturate(130%); + -webkit-backdrop-filter: blur(10px) saturate(130%); + font-family: var(--font-mono); + font-size: var(--font-size-xs); + letter-spacing: 0.04em; + color: var(--accent); + transition: all var(--transition-base); + + &:hover { + background: var(--accent-transparent-hover); + box-shadow: var(--glow-effect); + } + + :global(svg) { + width: 0.85rem; + height: 0.85rem; + flex-shrink: 0; + } + } + .hero-title { font-size: clamp(2.75rem, 8vw, 5rem); font-weight: 700; letter-spacing: var(--tracking-display); line-height: 1.02; - margin: var(--space-sm) 0 var(--space-md); + margin: var(--space-lg) 0 var(--space-md); + } + + .statement { + padding: var(--space-md) var(--space-lg); + border: 1px solid var(--border-color); + border-left: 3px solid var(--accent); + border-radius: 0 var(--radius-md) var(--radius-md) 0; + background: linear-gradient(90deg, var(--accent-transparent), transparent 75%); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); } .hero-summary { - font-size: var(--font-size-lg); + font-size: var(--font-size-base); line-height: var(--line-height-relaxed); - color: var(--text-color-dim); - max-width: 58ch; + color: var(--text-color-secondary); + max-width: 64ch; white-space: pre-line; } diff --git a/web/src/routes/certificates/+page.svelte b/web/src/routes/certificates/+page.svelte index 2de541a..76e7f37 100644 --- a/web/src/routes/certificates/+page.svelte +++ b/web/src/routes/certificates/+page.svelte @@ -202,6 +202,10 @@ } ]; + // Certificate activity window: fixed start year, rolling end year + const startYear = 2023; + const currentYear = new Date().getFullYear(); + // Filter state let selectedSkill: string = 'all'; @@ -289,7 +293,7 @@
- +
@@ -412,14 +416,14 @@ {#if selectedCertificate} -