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 @@
+
+
+
+ {#each socials as social (social.name)}
+ -
+
+
+
+
+ {/each}
+
+
+
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)} />
+
+
+
-