diff --git a/resume.yml b/resume.yml index 22c22b1..e77e468 100644 --- a/resume.yml +++ b/resume.yml @@ -164,6 +164,13 @@ awards: # Showcase your best technical projects # Fields: name, description, url, github, language, stars (optional), tags (optional) projects: + - name: 'BlackBook MCP' + description: 'Read-only, source-grounded cybersecurity knowledge MCP server: hybrid FTS5 and semantic retrieval, a knowledge graph, and exact citations from HackTricks, 0xdf, and PDF corpora' + github: 'Daniel-wambua/BlackBook' + url: 'https://blackbook.havocsec.me' + language: 'Python' + stars: '1' + - name: 'CTF Writeups & Security Blog' description: 'Personal blog documenting CTF solutions, tooling notes, and security concepts' github: 'Daniel-wambua/blog' diff --git a/tex/resume.tex b/tex/resume.tex index cdd0ee4..d1662fe 100644 --- a/tex/resume.tex +++ b/tex/resume.tex @@ -193,6 +193,14 @@ \vspace{0.2mm} \begin{cvachievements} \setlength{\itemsep}{0mm} + \item {\hspace{-5pt} + \textbf{ BlackBook MCP } (1 \raisebox{0.5pt}{\scriptsize\faStar}) + \href{ https://blackbook.havocsec.me }{\scriptsize\color{lightgray}\faExternalLink~Live} + \hfill + \href{https://github.com/Daniel-wambua/BlackBook}{\hspace{1pt}\faGithub~Daniel-wambua/BlackBook}\\ + Read-only, source-grounded cybersecurity knowledge MCP server: hybrid FTS5 and semantic retrieval, a knowledge graph, and exact citations from HackTricks, 0xdf, and PDF corpora + \vspace{0.2mm} + } \item {\hspace{-5pt} \textbf{ CTF Writeups \& Security Blog } (21 \raisebox{0.5pt}{\scriptsize\faStar}) \href{ https://havocsec.dev }{\scriptsize\color{lightgray}\faExternalLink~Live} 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..30138d2 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,96 @@ 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; -} - -.text-right { - text-align: right; -} +::-webkit-scrollbar-thumb:hover { + background: var(--text-color-muted); +} + +/* ========================================================================== + Ambient backdrop - fixed layer of soft glows and a faint grid. + Everything translucent (glass panels) layers over this, so the blur has + something to be glass over. Rendered once, pointer-events none. + ========================================================================== */ +.app-backdrop { + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background: + radial-gradient(ellipse 60% 50% at 12% 0%, rgba(0, 255, 136, 0.09) 0%, transparent 60%), + radial-gradient(ellipse 50% 40% at 88% 12%, rgba(0, 153, 79, 0.07) 0%, transparent 55%), + radial-gradient(ellipse 55% 45% at 70% 95%, rgba(0, 204, 106, 0.05) 0%, transparent 60%); +} + +.app-backdrop::after { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(232, 240, 236, 0.025) 1px, transparent 1px), + linear-gradient(90deg, rgba(232, 240, 236, 0.025) 1px, transparent 1px); + background-size: 48px 48px; + -webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 20%, black 20%, transparent 80%); + mask-image: radial-gradient(ellipse 90% 70% at 50% 20%, black 20%, transparent 80%); +} + +/* ========================================================================== + Glass treatment - blur for every translucent panel. Class names are the + unscoped originals from Card/StatBlock and the page styles, so this one + rule covers all of them. + ========================================================================== */ +.card, +.stat, +.job, +.tech-card, +.extra-card, +.soft-card, +.note, +.certificate-card, +.upcoming-item, +.badge-item, +.statement, +.value-item, +.req-item, +.explore-card, +.email-panel, +.guide, +.thanks, +.banner { + backdrop-filter: blur(14px) saturate(135%); + -webkit-backdrop-filter: blur(14px) saturate(135%); +} + +/* ========================================================================== + Utilities + ========================================================================== */ +.text-center { text-align: center; } +.text-left { text-align: left; } +.text-right { text-align: right; } .text-column { display: flex; @@ -243,26 +308,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 +399,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..fc68f07 --- /dev/null +++ b/web/src/components/Card.svelte @@ -0,0 +1,58 @@ + + +{#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..971772f --- /dev/null +++ b/web/src/components/SectionHeading.svelte @@ -0,0 +1,76 @@ + + +
+
+ {#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..8313a42 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,187 @@ 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'), + start: new Date('2026-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 + // Footer copyright rolls forward automatically + const startYear = 2025; + const currentYear = currentDate.getFullYear(); + $: 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: 'X', url: 'https://x.com/HavocRichie', handle: 'HavocRichie', 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' }, + // /pentesting itself has no index page, it only holds category subfolders + { name: 'Research', url: 'https://havocsec.dev/pentesting/hackthebox/' } ]; + e.key === 'Escape' && (drawerOpen = false)} /> + + +
-