+
-
-
- Havox started in 2017 as a place to dump whatever projects I had
- written. Four redesigns and several dead domains later, it's still
- going. Less a portfolio, more a scrapbook.
-
-
+
+
+ Havox started in 2017 as a place to dump whatever projects I had
+ written. Four redesigns and several dead domains later, it's still
+ going. Less a portfolio, more a scrapbook.
+
+
-
-
day job
-
- Backend engineer at Sainsbury's Supply Chain
- & Logistics. It's good fun.
-
-
+
+
day job
+
+ Backend engineer at Sainsbury's Supply Chain
+ & Logistics. It's good fun.
+
+
-
-
dev
-
- Fascinated with new languages, frameworks, and shiny tech I
- probably don’t need. Constantly learning just enough to build
- something slightly cooler next time.
-
-
+
+
dev
+
+ Fascinated with new languages, frameworks, and shiny tech I
+ probably don’t need. Constantly learning just enough to build
+ something slightly cooler next time.
+
+
-
-
linux
-
- Long-time Linux user and distro-hopper. Self-hosted server running
- 24/7 for friends and family who didn't ask for it but
- probably definitely appreciate it.
-
-
+
+
linux
+
+ Long-time Linux user and distro-hopper. Self-hosted server running
+ 24/7 for friends and family who didn't ask for it but
+ probably definitely appreciate it.
+
+
+
-
+
-
-
- skills
-
+
+
+ skills
+
What I know
-
+
-
+
-
☕
-
Backend
-
- Professional experience designing, building and deploying services.
- Using a modern tech stack in the Supply Chain & Logistics industry.
-
-
- Java
- Spring Boot
- AWS
- Kafka
- MongoDB
-
+
+
+
+
Backend
+
+ Professional experience designing, building and deploying services.
+ Using a modern tech stack in the Supply Chain & Logistics industry.
+
+
+ Java
+ Spring Boot
+ AWS
+ Kafka
+ MongoDB
+
-
🎨
-
Frontend & Creative
-
- Comfortable with React and CSS. I've always enjoyed visualising
- algorithms interactively - it's way more fun than a console output.
-
-
- React
- TypeScript
- HTML/CSS
-
+
+
+
+
Frontend & Creative
+
+ Comfortable with React and CSS. I've always enjoyed visualising
+ algorithms interactively - it's way more fun than a console output.
+
+
+ React
+ TypeScript
+ HTML/CSS
+
-
🖥
-
Systems & Infra
-
- Long-term Linux & Vim user. Self-hosted web & media servers. Using
- industry practices when it comes to networks & security.
-
-
- Linux
- Docker
- Self-Hosting
- Wireguard
-
+
+
+
+
Systems & Infra
+
+ Long-term Linux & Vim user. Self-hosted web & media servers. Using
+ industry practices when it comes to networks & security.
+
+
+ Linux
+ Docker
+ Self-Hosting
+ Wireguard
+
-
🎓
-
Academic Breadth
-
- C, C++, x86_64 assembly, Haskell, R & MatLab from uni. Modules
- in Advanced networking, algorithms, compilers, graphics,
- cryptography, malware analysis & many more.
-
-
- C / C++
- Haskell
- Algorithms
- Security
- Low Level OS
-
+
+
+
+
Academic Breadth
+
+ C, C++, x86_64 assembly, Haskell, R & MatLab from uni. Modules
+ in Advanced networking, algorithms, compilers, graphics,
+ cryptography, malware analysis & many more.
+
+
+ C / C++
+ Haskell
+ Algorithms
+ Security
+ Low Level OS
+
-
-
+
+
-
+
-
-
- projects
-
+
+
+ projects
+
Things I built
-
-
+
+
A timeline of my projects and an overview of my journey as a developer.
Often times getting inspired and trying to recreate something cool I
saw.
-
+
-
-
+
+
-
-
+
- All
+ All
- JavaScript
+ JavaScript
- Python
+ Python
- Rust
+ Rust
- React
+ React
- WebAssembly
+ WebAssembly
- p5.js
+ p5.js
Zig
- C / C++
+ C / C++
-
+
-
+
- 03 Apr 2025
- Pub Quiz Dashboard
- 03 Apr 2025
+ Active development
+ Pub Quiz Dashboard
+ A Python dashboard to track our local pub quiz performances.
-
+ >
+
Python
- →
- 14 Nov 2024
- Thin Ice
- 14 Nov 2024
+ Thin Ice
+ A small browser tile game inspired by one I played growing
up.
-
+ >
+
C++
WebAssembly
RayLib
- →
- 20 Mar 2024
- Travelling Salesman Problem
- 20 Mar 2024
+ Travelling Salesman Problem
+ A nearest-neighbor + 2-opt visualiser. Wanted to revisit one of my
first projects but with a super optimised tech-stack &
algorithm.
-
+ >
+
Zig
WebGL
WebAssembly
- →
- 31 Jan 2024
- Flocking
- 31 Jan 2024
+ Flocking
+ Rewrote the entire project in Rust from GoLang to compile to wasm.
Was fun being able to use DOM controls to affect the
simulation.
-
+ >
+
Rust
Raylib
- →
- 1 Nov 2023
- samstoreymusic.com
- 1 Nov 2023
+ Active development
+ samstoreymusic.com
+ A website design and build for a friend working in music. Saving
him the hosting fees :)
-
+ >
+
HTML/CSS
JavaScript
- →
- 21 Jul 2023
- Game of Life
- 21 Jul 2023
+ Game of Life
+ After building Conway's Game of Life in p5.js as one of my first
projects, I expanded it into a generic automata simulator.
-
+ >
+
React
JavaScript
- →
- 5 Feb 2022
- Percolation
- 5 Feb 2022
+ Percolation
+ Watched a brilliant video by
Spectral Collective Spectral Collective
and just had to recreate it.
-
+ >
+
C
RayLib
WebAssembly
- →
- 1 Oct 2021
- Drawing Bézier curves
- 1 Oct 2021
+ Drawing Bézier curves
+ An interactive animation for how cubic Bézier curves are
constructed. Inspirated by this beautiful video by
Fraya Holmér Fraya Holmér
-
+ >
+
JavaScript
p5.js
- →
- 17 Sep 2021
- 2D Marching Squares
- 17 Sep 2021
+ 2D Marching Squares
+ I saw the 3D version of this algorithm in a game dev log and was
inspired to create this 2d land/sea map generator thing.
-
+ >
+
JavaScript
p5.js
- →
- 2 Apr 2020
- Müller-Lyer illusion
- 2 Apr 2020
+ Müller-Lyer illusion
+ JavaScript visualisation of the Müller-Lyer optical illusion.
-
+ >
+
JavaScript
p5.js
- →
- 27 Feb 2019
- Fourier series
- 27 Feb 2019
+ Fourier series
+ Builds a square wave from sine components to show Fourier series in
motion.
-
+ >
+
JavaScript
p5.js
- →
- 23 Feb 2019
- Constructing an ellipse
- 23 Feb 2019
+ Constructing an ellipse
+ A geometric construction demo inspired by
3Blue1Brown's video 3Blue1Brown's video
on Feynman's lost lecture.
-
+ >
+
JavaScript
p5.js
- →
- 18 Mar 2018
- Calculating PI
- 18 Mar 2018
+ Calculating PI
+ A Monte Carlo approximation of pi using random sampling.
-
+ >
+
JavaScript
p5.js
- →
- 17 Dec 2017
- Oscillations in 3D
- 17 Dec 2017
+ Oscillations in 3D
+ A JavaScript recreation of a Bees and Bombs animation.
-
+ >
+
JavaScript
p5.js
- →
- 13 Nov 2017
- Maze generator
- 13 Nov 2017
+ Maze generator
+ Discovered the
Coding Train's channel Coding Train's channel
and learnt coding through following along.
-
+ >
+
JavaScript
p5.js
- →
-
+
-
+
- See more
+ See more
-
-
+
+
-
+
-
-
-
-
- umbra.mom - John Gatward
-
+
+
+ umbra.mom - John Gatward
+
Source code (self‑hosted):
repository
-
+
-
-
+
+
diff --git a/script.js b/script.js
index 12be41f..8c1ab0a 100644
--- a/script.js
+++ b/script.js
@@ -1,89 +1,142 @@
// ─── Scroll reveal ─────────────────────────────────────────
const reveals = document.querySelectorAll('.reveal');
-const observer = new IntersectionObserver((entries) => {
- entries.forEach(e => {
- if (e.isIntersecting) {
- e.target.classList.add('visible');
- observer.unobserve(e.target);
+const revealObserver = new IntersectionObserver((entries, observer) => {
+ entries.forEach((entry) => {
+ if (!entry.isIntersecting) {
+ return;
}
+
+ entry.target.classList.add('visible');
+ observer.unobserve(entry.target);
});
}, {threshold: 0.1, rootMargin: '0px 0px -40px 0px'});
-reveals.forEach(el => observer.observe(el));
+reveals.forEach((element) => revealObserver.observe(element));
// ─── Active nav link on scroll ──────────────────────────────
-const sections = document.querySelectorAll('section[id]');
-const navLinks = document.querySelectorAll('.nav-links a');
-window.addEventListener('scroll', () => {
- let current = '';
- sections.forEach(s => {
- if (window.scrollY >= s.offsetTop - 120) current = s.id;
+const sections = Array.from(document.querySelectorAll('section[id]'));
+const navLinks = Array.from(document.querySelectorAll('.nav-links a'));
+const updateActiveNavLink = () => {
+ let currentSectionId = '';
+
+ sections.forEach((section) => {
+ if (window.scrollY >= section.offsetTop - 120) {
+ currentSectionId = section.id;
+ }
});
- navLinks.forEach(a => {
- a.style.color = a.getAttribute('href') === `#${current}` ? 'var(--mauve)' : '';
+
+ navLinks.forEach((link) => {
+ const isActive = link.getAttribute('href') === `#${currentSectionId}`;
+ link.classList.toggle('is-active', isActive);
+ });
+};
+window.addEventListener('scroll', updateActiveNavLink, {passive: true});
+updateActiveNavLink();
+
+// ─── Project card links + filters ───────────────────────────
+const filterChips = Array.from(document.querySelectorAll('.filter-chip'));
+const projectCards = Array.from(document.querySelectorAll('#project-grid .project-card'));
+const seeMoreButton = document.querySelector('#projects-see-more');
+const cardLinks = document.querySelectorAll('.card-link[data-href]');
+const DEFAULT_VISIBLE_PROJECTS = 9;
+const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]';
+
+const isNestedInteractiveTarget = (card, target) => {
+ if (!(target instanceof Element)) {
+ return false;
+ }
+
+ const interactiveAncestor = target.closest(interactiveSelector);
+ return interactiveAncestor !== null && interactiveAncestor !== card;
+};
+
+const goToCardHref = (card, event) => {
+ const href = card.dataset.href;
+ if (!href) {
+ return;
+ }
+
+ const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1;
+ if (openInNewTab) {
+ window.open(href, '_blank', 'noopener');
+ return;
+ }
+
+ globalThis.location.href = href;
+};
+
+cardLinks.forEach((card) => {
+ card.setAttribute('role', 'link');
+ card.setAttribute('tabindex', '0');
+
+ card.addEventListener('click', (event) => {
+ if (isNestedInteractiveTarget(card, event.target)) {
+ return;
+ }
+
+ goToCardHref(card, event);
+ });
+
+ card.addEventListener('keydown', (event) => {
+ if (event.key !== 'Enter' && event.key !== ' ') {
+ return;
+ }
+
+ if (isNestedInteractiveTarget(card, event.target)) {
+ return;
+ }
+
+ event.preventDefault();
+ goToCardHref(card, event);
});
});
-// ─── Project filters ──────────────────────────────────────────────
-const filterChips = document.querySelectorAll('.filter-chip');
-const projectCards = document.querySelectorAll('#project-grid .project-card');
-const seeMoreButton = document.querySelector('#projects-see-more');
-const DEFAULT_VISIBLE_PROJECTS = 9;
-
if (filterChips.length > 0 && projectCards.length > 0) {
let selectedFilter = 'all';
let isExpanded = false;
- const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]';
- const goToCardHref = (card, event) => {
- const href = card.dataset.href;
- if (!href) {
- return;
- }
-
- const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1;
- if (openInNewTab) {
- window.open(href, '_blank', 'noopener');
- return;
- }
-
- globalThis.location.href = href;
- };
+ const techByCard = new Map(
+ projectCards.map((card) => [card, (card.dataset.tech || '').split(/\s+/).filter(Boolean)]),
+ );
const applyFilter = () => {
const visibleCards = [];
- projectCards.forEach(card => {
- const tech = (card.dataset.tech || '').split(/\s+/).filter(Boolean);
+ projectCards.forEach((card) => {
+ const tech = techByCard.get(card) || [];
const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter);
card.classList.toggle('is-hidden', !shouldShow);
- if (shouldShow) {
- card.classList.remove('is-collapsed');
- visibleCards.push(card);
+ if (!shouldShow) {
+ return;
}
+
+ card.classList.remove('is-collapsed');
+ visibleCards.push(card);
});
if (!isExpanded) {
- visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach(card => {
+ visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach((card) => {
card.classList.add('is-collapsed');
});
}
- if (seeMoreButton) {
- const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS;
- seeMoreButton.hidden = !canExpand;
- seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
- seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
+ if (!seeMoreButton) {
+ return;
}
+
+ const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS;
+ seeMoreButton.hidden = !canExpand;
+ seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
+ seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
};
- filterChips.forEach(chip => {
+ filterChips.forEach((chip) => {
chip.addEventListener('click', () => {
selectedFilter = chip.dataset.filter || 'all';
isExpanded = false;
- filterChips.forEach(other => {
- other.classList.toggle('active', other === chip);
+ filterChips.forEach((otherChip) => {
+ otherChip.classList.toggle('active', otherChip === chip);
});
applyFilter();
@@ -97,47 +150,5 @@ if (filterChips.length > 0 && projectCards.length > 0) {
});
}
- projectCards.forEach(card => {
- if (!card.dataset.href) {
- return;
- }
-
- card.setAttribute('role', 'link');
- card.setAttribute('tabindex', '0');
-
- card.addEventListener('click', event => {
- const target = event.target;
- if (!(target instanceof Element)) {
- return;
- }
-
- // Let inline links and other controls inside the card handle their own clicks.
- if (target.closest(interactiveSelector) && target.closest(interactiveSelector) !== card) {
- return;
- }
-
- goToCardHref(card, event);
- });
-
- card.addEventListener('keydown', event => {
- if (event.key !== 'Enter' && event.key !== ' ') {
- return;
- }
-
- const target = event.target;
- if (!(target instanceof Element)) {
- return;
- }
-
- if (target.closest(interactiveSelector) && target.closest(interactiveSelector) !== card) {
- return;
- }
-
- event.preventDefault();
- goToCardHref(card, event);
- });
- });
-
applyFilter();
-}
-
+}
\ No newline at end of file
diff --git a/style.css b/style.css
index cb895dc..bbf495d 100644
--- a/style.css
+++ b/style.css
@@ -1,1126 +1,1157 @@
/* ─── Catppuccin Mocha ─────────────────────────────────── */
:root {
- --base: #1e1e2e;
- --mantle: #181825;
- --crust: #11111b;
- --surface0: #313244;
- --surface1: #45475a;
- --surface2: #585b70;
- --overlay0: #6c7086;
- --overlay1: #7f849c;
- --overlay2: #9399b2;
- --text: #cdd6f4;
- --subtext0: #a6adc8;
- --subtext1: #bac2de;
- --lavender: #b4befe;
- --blue: #89b4fa;
- --sapphire: #74c7ec;
- --sky: #89dceb;
- --teal: #94e2d5;
- --green: #a6e3a1;
- --yellow: #f9e2af;
- --peach: #fab387;
- --maroon: #eba0ac;
- --red: #f38ba8;
- --mauve: #cba6f7;
- --pink: #f5c2e7;
- --flamingo: #f2cdcd;
- --rosewater: #f5e0dc;
+ --base: #1e1e2e;
+ --mantle: #181825;
+ --crust: #11111b;
+ --surface0: #313244;
+ --surface1: #45475a;
+ --surface2: #585b70;
+ --overlay0: #6c7086;
+ --overlay1: #7f849c;
+ --overlay2: #9399b2;
+ --text: #cdd6f4;
+ --subtext0: #a6adc8;
+ --subtext1: #bac2de;
+ --lavender: #b4befe;
+ --blue: #89b4fa;
+ --sapphire: #74c7ec;
+ --sky: #89dceb;
+ --teal: #94e2d5;
+ --green: #a6e3a1;
+ --yellow: #f9e2af;
+ --peach: #fab387;
+ --maroon: #eba0ac;
+ --red: #f38ba8;
+ --mauve: #cba6f7;
+ --pink: #f5c2e7;
+ --flamingo: #f2cdcd;
+ --rosewater: #f5e0dc;
}
*,
*::before,
*::after {
- box-sizing: border-box;
- margin: 0;
- padding: 0;
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
}
html {
- scroll-behavior: smooth;
+ scroll-behavior: smooth;
}
body {
- background:
- radial-gradient(rgba(205, 214, 244, 0.028) 0.5px, transparent 0.6px),
+ background: radial-gradient(rgba(205, 214, 244, 0.028) 0.5px, transparent 0.6px),
radial-gradient(rgba(17, 17, 27, 0.032) 0.5px, transparent 0.6px),
radial-gradient(
- 1200px 800px at 12% 8%,
- rgba(137, 180, 250, 0.07),
- transparent 62%
+ 1200px 800px at 12% 8%,
+ rgba(137, 180, 250, 0.07),
+ transparent 62%
),
radial-gradient(
- 1000px 720px at 88% 92%,
- rgba(203, 166, 247, 0.07),
- transparent 64%
+ 1000px 720px at 88% 92%,
+ rgba(203, 166, 247, 0.07),
+ transparent 64%
),
linear-gradient(180deg, #1e1e2e 0%, #1b1b2b 55%, #1a1a29 100%);
- background-size:
- 3px 3px,
+ background-size: 3px 3px,
3px 3px,
auto,
auto,
auto;
- background-position:
- 0 0,
+ background-position: 0 0,
1px 1px,
0 0,
0 0,
0 0;
- color: var(--text);
- font-family: "JetBrains Mono", "Apple Color Emoji", monospace;
- font-size: 15px;
- line-height: 1.7;
- overflow-x: hidden;
- position: relative;
+ color: var(--text);
+ font-family: "JetBrains Mono", "Apple Color Emoji", monospace;
+ font-size: 15px;
+ line-height: 1.7;
+ overflow-x: hidden;
+ position: relative;
}
body::before,
body::after {
- content: "";
- position: fixed;
- width: 52rem;
- height: 42rem;
- border-radius: 58% 42% 55% 45% / 46% 54% 48% 52%;
- pointer-events: none;
- z-index: -1;
- filter: blur(90px);
- opacity: 0.16;
+ content: "";
+ position: fixed;
+ width: 52rem;
+ height: 42rem;
+ border-radius: 58% 42% 55% 45% / 46% 54% 48% 52%;
+ pointer-events: none;
+ z-index: -1;
+ filter: blur(90px);
+ opacity: 0.16;
}
body::before {
- top: -18rem;
- right: -16rem;
- transform: rotate(14deg);
- background: radial-gradient(
- closest-side at 38% 42%,
- rgba(203, 166, 247, 0.42),
- transparent 72%
- );
+ top: -18rem;
+ right: -16rem;
+ transform: rotate(14deg);
+ background: radial-gradient(
+ closest-side at 38% 42%,
+ rgba(203, 166, 247, 0.42),
+ transparent 72%
+ );
}
body::after {
- bottom: -20rem;
- left: -18rem;
- transform: rotate(-12deg);
- background: radial-gradient(
- closest-side at 60% 58%,
- rgba(137, 180, 250, 0.36),
- transparent 72%
- );
+ bottom: -20rem;
+ left: -18rem;
+ transform: rotate(-12deg);
+ background: radial-gradient(
+ closest-side at 60% 58%,
+ rgba(137, 180, 250, 0.36),
+ transparent 72%
+ );
}
::selection {
- background: var(--mauve);
- color: var(--crust);
+ background: var(--mauve);
+ color: var(--crust);
}
/* ─── Scrollbar ────────────────────────────────────────── */
::-webkit-scrollbar {
- width: 6px;
+ width: 6px;
}
::-webkit-scrollbar-track {
- background: var(--mantle);
+ background: var(--mantle);
}
::-webkit-scrollbar-thumb {
- background: var(--surface1);
- border-radius: 3px;
+ background: var(--surface1);
+ border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
- background: var(--mauve);
+ background: var(--mauve);
}
/* ─── Nav ──────────────────────────────────────────────── */
nav {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- z-index: 100;
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 3rem;
- height: 56px;
- background: rgba(17, 17, 27, 0.85);
- backdrop-filter: blur(12px);
- border-bottom: 1px solid var(--surface0);
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 3rem;
+ height: 56px;
+ background: rgba(17, 17, 27, 0.85);
+ backdrop-filter: blur(12px);
+ border-bottom: 1px solid var(--surface0);
}
.nav-logo {
- font-family: "Fraunces", serif;
- font-size: 1.3rem;
- color: var(--mauve);
- text-decoration: none;
- letter-spacing: 0.02em;
+ font-family: "Fraunces", serif;
+ font-size: 1.3rem;
+ color: var(--mauve);
+ text-decoration: none;
+ letter-spacing: 0.02em;
}
.nav-links {
- display: flex;
- gap: 2.5rem;
- list-style: none;
+ display: flex;
+ gap: 2.5rem;
+ list-style: none;
}
.nav-links a {
- color: var(--subtext0);
- text-decoration: none;
- font-size: 0.78rem;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- transition: color 0.2s;
- position: relative;
+ color: var(--subtext0);
+ text-decoration: none;
+ font-size: 0.78rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ transition: color 0.2s;
+ position: relative;
}
.nav-links a::after {
- content: "";
- position: absolute;
- bottom: -3px;
- left: 0;
- right: 0;
- height: 1px;
- background: var(--mauve);
- transform: scaleX(0);
- transform-origin: left;
- transition: transform 0.2s;
+ content: "";
+ position: absolute;
+ bottom: -3px;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: var(--mauve);
+ transform: scaleX(0);
+ transform-origin: left;
+ transition: transform 0.2s;
}
.nav-links a:hover {
- color: var(--text);
+ color: var(--text);
+}
+
+.nav-links a.is-active {
+ color: var(--mauve);
}
.nav-links a:hover::after {
- transform: scaleX(1);
+ transform: scaleX(1);
}
/* ─── Sections ─────────────────────────────────────────── */
section {
- min-height: 100vh;
- padding: 7rem 3rem 5rem;
- max-width: 1100px;
- margin: 0 auto;
+ min-height: 100vh;
+ padding: 7rem 3rem 5rem;
+ max-width: 1100px;
+ margin: 0 auto;
}
section.full-width {
- max-width: none;
- padding-left: 0;
- padding-right: 0;
+ max-width: none;
+ padding-left: 0;
+ padding-right: 0;
}
/* ─── Section labels ───────────────────────────────────── */
.section-label {
- font-size: 0.7rem;
- letter-spacing: 0.2em;
- text-transform: uppercase;
- color: var(--mauve);
- margin-bottom: 0.5rem;
- display: flex;
- align-items: center;
- gap: 0.75rem;
+ font-size: 0.7rem;
+ letter-spacing: 0.2em;
+ text-transform: uppercase;
+ color: var(--mauve);
+ margin-bottom: 0.5rem;
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
}
.section-label::after {
- content: "";
- display: block;
- height: 1px;
- width: 3rem;
- background: var(--mauve);
- opacity: 0.5;
+ content: "";
+ display: block;
+ height: 1px;
+ width: 3rem;
+ background: var(--mauve);
+ opacity: 0.5;
}
/* ─── Hero ─────────────────────────────────────────────── */
#hero {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- justify-content: center;
- padding-top: 5rem;
- position: relative;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding-top: 5rem;
+ position: relative;
}
.hero-eyebrow {
- font-size: 0.75rem;
- letter-spacing: 0.25em;
- text-transform: uppercase;
- color: var(--green);
- margin-bottom: 1.5rem;
- opacity: 0;
- animation: fadeUp 0.6s 0.2s forwards;
+ font-size: 0.75rem;
+ letter-spacing: 0.25em;
+ text-transform: uppercase;
+ color: var(--green);
+ margin-bottom: 1.5rem;
+ opacity: 0;
+ animation: fadeUp 0.6s 0.2s forwards;
}
.hero-name {
- font-family: "Fraunces", serif;
- font-size: clamp(3.5rem, 9vw, 7.5rem);
- font-weight: 600;
- line-height: 1;
- color: var(--text);
- margin-bottom: 0.5rem;
- opacity: 0;
- animation: fadeUp 0.7s 0.35s forwards;
+ font-family: "Fraunces", serif;
+ font-size: clamp(3.5rem, 9vw, 7.5rem);
+ font-weight: 600;
+ line-height: 1;
+ color: var(--text);
+ margin-bottom: 0.5rem;
+ opacity: 0;
+ animation: fadeUp 0.7s 0.35s forwards;
}
.hero-name em {
- font-style: italic;
- color: var(--mauve);
+ font-style: italic;
+ color: var(--mauve);
}
.hero-subtitle {
- font-family: "Fraunces", serif;
- font-size: clamp(1.2rem, 3vw, 2.2rem);
- font-weight: 300;
- color: var(--subtext1);
- margin-bottom: 2rem;
- opacity: 0;
- animation: fadeUp 0.7s 0.5s forwards;
+ font-family: "Fraunces", serif;
+ font-size: clamp(1.2rem, 3vw, 2.2rem);
+ font-weight: 300;
+ color: var(--subtext1);
+ margin-bottom: 2rem;
+ opacity: 0;
+ animation: fadeUp 0.7s 0.5s forwards;
}
.hero-desc {
- max-width: 560px;
- color: var(--subtext0);
- font-size: 0.9rem;
- line-height: 1.8;
- margin-bottom: 2.5rem;
- opacity: 0;
- animation: fadeUp 0.7s 0.65s forwards;
+ max-width: 560px;
+ color: var(--subtext0);
+ font-size: 0.9rem;
+ line-height: 1.8;
+ margin-bottom: 2.5rem;
+ opacity: 0;
+ animation: fadeUp 0.7s 0.65s forwards;
}
.hero-by-line {
- display: flex;
- align-items: baseline;
- gap: 0.6rem;
- margin-bottom: 0.3rem;
+ display: flex;
+ align-items: baseline;
+ gap: 0.6rem;
+ margin-bottom: 0.3rem;
}
.hero-by {
- color: var(--text);
- font-size: 0.9rem;
+ color: var(--text);
+ font-size: 0.9rem;
}
.hero-by-role {
- font-family: "Fraunces", serif;
- font-style: italic;
- font-size: 0.85rem;
- color: var(--mauve);
- opacity: 0.8;
-}
-
-.hero-by-footer {
- display: block;
- margin-top: 1rem;
- color: var(--subtext0);
- font-size: 0.82rem;
- opacity: 0.75;
+ font-family: "Fraunces", serif;
+ font-style: italic;
+ font-size: 0.85rem;
+ color: var(--mauve);
+ opacity: 0.8;
}
.hero-links {
- display: flex;
- gap: 1.25rem;
- flex-wrap: wrap;
- opacity: 0;
- animation: fadeUp 0.7s 0.8s forwards;
+ display: flex;
+ gap: 1.25rem;
+ flex-wrap: wrap;
+ opacity: 0;
+ animation: fadeUp 0.7s 0.8s forwards;
}
.btn {
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.6rem 1.4rem;
- border-radius: 4px;
- font-family: "JetBrains Mono", monospace;
- font-size: 0.78rem;
- letter-spacing: 0.05em;
- text-decoration: none;
- transition: all 0.2s;
- cursor: pointer;
- border: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.6rem 1.4rem;
+ border-radius: 4px;
+ font-family: "JetBrains Mono", monospace;
+ font-size: 0.78rem;
+ letter-spacing: 0.05em;
+ text-decoration: none;
+ transition: all 0.2s;
+ cursor: pointer;
+ border: none;
}
.btn-primary {
- background: var(--mauve);
- color: var(--crust);
- box-shadow:
- 0 0 0 1px rgba(203, 166, 247, 0.25),
+ background: var(--mauve);
+ color: var(--crust);
+ box-shadow: 0 0 0 1px rgba(203, 166, 247, 0.25),
0 10px 26px rgba(17, 17, 27, 0.4);
}
.btn-primary:hover {
- background: var(--lavender);
- transform: translateY(-2px);
+ background: var(--lavender);
+ transform: translateY(-2px);
}
.btn-ghost {
- background: transparent;
- color: var(--text);
- border: 1px solid var(--surface1);
+ background: transparent;
+ color: var(--text);
+ border: 1px solid var(--surface1);
}
.btn-ghost:hover {
- border-color: var(--mauve);
- color: var(--mauve);
- transform: translateY(-2px);
+ border-color: var(--mauve);
+ color: var(--mauve);
+ transform: translateY(-2px);
}
/* Decorative grid bg */
#hero::before {
- content: "";
- position: absolute;
- inset: 0;
- background-image:
- linear-gradient(var(--surface0) 1px, transparent 1px),
+ content: "";
+ position: absolute;
+ inset: 0;
+ background-image: linear-gradient(var(--surface0) 1px, transparent 1px),
linear-gradient(90deg, var(--surface0) 1px, transparent 1px);
- background-size: 60px 60px;
- opacity: 0.18;
- mask-image: radial-gradient(ellipse 80% 60% at 50% 50%, black, transparent);
- pointer-events: none;
+ background-size: 60px 60px;
+ opacity: 0.18;
+ mask-image: radial-gradient(ellipse 80% 60% at 50% 50%, black, transparent);
+ pointer-events: none;
}
/* ─── About ────────────────────────────────────────────── */
.about-section {
- min-height: auto;
- padding-bottom: 5rem;
+ min-height: auto;
+ padding-bottom: 5rem;
}
.about-columns {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 2rem;
- margin-top: 2.5rem;
- align-items: start;
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 2rem;
+ margin-top: 2.5rem;
+ align-items: start;
}
.about-grid {
- display: flex;
- flex-direction: column;
- border: 1px solid var(--surface0);
- border-radius: 8px;
- overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--surface0);
+ border-radius: 8px;
+ overflow: hidden;
}
.about-block {
- background: var(--mantle);
- padding: 1.25rem 1.5rem;
- border-bottom: 1px solid var(--surface0);
- transition: background 0.2s;
- display: flex;
- align-items: baseline;
- gap: 1.25rem;
+ background: var(--mantle);
+ padding: 1.25rem 1.5rem;
+ border-bottom: 1px solid var(--surface0);
+ transition: background 0.2s;
+ display: flex;
+ align-items: baseline;
+ gap: 1.25rem;
}
.about-grid .about-block:first-child {
- background: linear-gradient(
- 90deg,
- rgba(203, 166, 247, 0.07),
- rgba(203, 166, 247, 0.03)
- );
- border-left: 2px solid rgba(203, 166, 247, 0.65);
- box-shadow: inset 0 0 0 1px rgba(203, 166, 247, 0.08);
+ background: linear-gradient(
+ 90deg,
+ rgba(203, 166, 247, 0.07),
+ rgba(203, 166, 247, 0.03)
+ );
+ border-left: 2px solid rgba(203, 166, 247, 0.65);
+ box-shadow: inset 0 0 0 1px rgba(203, 166, 247, 0.08);
}
.about-block:last-child {
- border-bottom: none;
+ border-bottom: none;
}
.about-block-tag {
- flex-shrink: 0;
- font-size: 0.65rem;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- width: 5rem;
- text-align: center;
+ flex-shrink: 0;
+ font-size: 0.65rem;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ width: 5rem;
+ text-align: center;
}
.about-block p {
- color: var(--subtext0);
- font-size: 0.85rem;
- line-height: 1.75;
+ color: var(--subtext0);
+ font-size: 0.85rem;
+ line-height: 1.75;
}
.about-block p strong {
- color: var(--text);
- font-weight: 700;
+ color: var(--text);
+ font-weight: 700;
}
.about-block p em {
- color: var(--mauve);
- font-style: italic;
+ color: var(--mauve);
+ font-style: italic;
}
.about-block p s {
- color: var(--overlay1);
+ color: var(--overlay1);
}
.version-timeline {
- display: flex;
- flex-direction: column;
- gap: 0;
- position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ position: relative;
}
.version-timeline::before {
- content: "";
- position: absolute;
- left: 0.65rem;
- top: 0.5rem;
- bottom: 0.5rem;
- width: 1px;
- background: var(--surface1);
+ content: "";
+ position: absolute;
+ left: 0.65rem;
+ top: 0.5rem;
+ bottom: 0.5rem;
+ width: 1px;
+ background: var(--surface1);
}
.version-item {
- display: flex;
- gap: 1.25rem;
- padding: 1rem 0;
- position: relative;
+ display: flex;
+ gap: 1.25rem;
+ padding: 1rem 0;
+ position: relative;
}
.v-dot {
- width: 1.3rem;
- height: 1.3rem;
- border-radius: 50%;
- background: var(--surface0);
- border: 2px solid var(--surface1);
- flex-shrink: 0;
- margin-top: 0.15rem;
- transition:
- border-color 0.2s,
+ width: 1.3rem;
+ height: 1.3rem;
+ border-radius: 50%;
+ background: var(--surface0);
+ border: 2px solid var(--surface1);
+ flex-shrink: 0;
+ margin-top: 0.15rem;
+ transition: border-color 0.2s,
background 0.2s;
- position: relative;
- z-index: 1;
+ position: relative;
+ z-index: 1;
}
.version-item:hover .v-dot {
- border-color: var(--mauve);
- background: var(--mauve);
+ border-color: var(--mauve);
+ background: var(--mauve);
}
.v-title {
- font-size: 0.85rem;
- font-weight: 700;
- color: var(--text);
- margin-bottom: 0.15rem;
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: var(--text);
+ margin-bottom: 0.15rem;
}
.v-year {
- font-size: 0.7rem;
- color: var(--mauve);
- margin-bottom: 0.3rem;
- letter-spacing: 0.05em;
+ font-size: 0.7rem;
+ color: var(--mauve);
+ margin-bottom: 0.3rem;
+ letter-spacing: 0.05em;
}
.v-desc {
- font-size: 0.8rem;
- color: var(--subtext0);
- line-height: 1.6;
+ font-size: 0.8rem;
+ color: var(--subtext0);
+ line-height: 1.6;
}
.version-item--link {
- text-decoration: none;
- color: inherit;
- cursor: pointer;
+ text-decoration: none;
+ color: inherit;
+ cursor: pointer;
}
.version-item--link:hover .v-dot {
- border-color: var(--mauve);
- background: var(--mauve);
+ border-color: var(--mauve);
+ background: var(--mauve);
}
.version-item--link:hover .v-title {
- color: var(--mauve);
+ color: var(--mauve);
}
.v-link-arrow {
- font-size: 0.7rem;
- color: var(--overlay1);
- margin-left: 0.3rem;
- transition:
- color 0.2s,
+ font-size: 0.7rem;
+ color: var(--overlay1);
+ margin-left: 0.3rem;
+ transition: color 0.2s,
transform 0.2s;
- display: inline-block;
+ display: inline-block;
}
.version-item--link:hover .v-link-arrow {
- color: var(--mauve);
- transform: translate(2px, -2px);
+ color: var(--mauve);
+ transform: translate(2px, -2px);
}
.v-dot--current {
- border-color: var(--mauve);
- background: var(--mauve);
+ border-color: var(--mauve);
+ background: var(--mauve);
}
.v-current-badge {
- font-size: 0.6rem;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- background: rgba(203, 166, 247, 0.15);
- color: var(--mauve);
- padding: 0.1rem 0.45rem;
- border-radius: 3px;
- margin-left: 0.4rem;
- vertical-align: middle;
- font-weight: 700;
+ font-size: 0.6rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ background: rgba(203, 166, 247, 0.15);
+ color: var(--mauve);
+ padding: 0.1rem 0.45rem;
+ border-radius: 3px;
+ margin-left: 0.4rem;
+ vertical-align: middle;
+ font-weight: 700;
}
/* ─── Skills ───────────────────────────────────────────── */
#skills h2 {
- font-family: "Fraunces", serif;
- font-size: 2.8rem;
- font-weight: 600;
- color: var(--text);
- margin-bottom: 3rem;
+ font-family: "Fraunces", serif;
+ font-size: 2.8rem;
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 3rem;
}
#skills h2 em {
- font-style: italic;
- color: var(--mauve);
+ font-style: italic;
+ color: var(--mauve);
}
.skills-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 1px;
- background: var(--surface0);
- border: 1px solid var(--surface0);
- border-radius: 8px;
- overflow: hidden;
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1px;
+ background: var(--surface0);
+ border: 1px solid var(--surface0);
+ border-radius: 8px;
+ overflow: hidden;
}
.skill-card--wide {
- grid-column: 1 / -1;
+ grid-column: 1 / -1;
}
.skill-card {
- background: var(--mantle);
- padding: 2rem;
- transition: background 0.2s;
+ background: var(--mantle);
+ padding: 2rem;
+ transition: background 0.2s;
}
.skill-card-icon {
- font-size: 1.5rem;
- margin-bottom: 1rem;
- display: block;
+ font-size: 1.5rem;
+ margin-bottom: 1rem;
+ display: block;
+}
+
+.skill-card-icon-image {
+ width: 1.75rem;
+ height: 1.75rem;
+ display: block;
}
.skill-card h3 {
- font-size: 0.85rem;
- font-weight: 700;
- color: var(--text);
- margin-bottom: 0.5rem;
- letter-spacing: 0.05em;
- text-transform: uppercase;
+ font-size: 0.85rem;
+ font-weight: 700;
+ color: var(--text);
+ margin-bottom: 0.5rem;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
}
.skill-card p {
- font-size: 0.82rem;
- color: var(--subtext0);
- line-height: 1.7;
+ font-size: 0.82rem;
+ color: var(--subtext0);
+ line-height: 1.7;
}
.tag-row {
- display: flex;
- flex-wrap: wrap;
- gap: 0.4rem;
- margin-top: 1rem;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4rem;
+ margin-top: 1rem;
}
.tag {
- display: inline-block;
- padding: 0.15rem 0.6rem;
- border-radius: 3px;
- font-size: 0.7rem;
- letter-spacing: 0.04em;
- font-weight: 700;
+ display: inline-block;
+ padding: 0.15rem 0.6rem;
+ border-radius: 3px;
+ font-size: 0.7rem;
+ letter-spacing: 0.04em;
+ font-weight: 700;
}
.tag-blue {
- background: rgba(137, 180, 250, 0.12);
- color: var(--blue);
+ background: rgba(137, 180, 250, 0.12);
+ color: var(--blue);
}
.tag-green {
- background: rgba(166, 227, 161, 0.12);
- color: var(--green);
+ background: rgba(166, 227, 161, 0.12);
+ color: var(--green);
}
.tag-peach {
- background: rgba(250, 179, 135, 0.12);
- color: var(--peach);
+ background: rgba(250, 179, 135, 0.12);
+ color: var(--peach);
}
.tag-teal {
- background: rgba(148, 226, 213, 0.12);
- color: var(--teal);
+ background: rgba(148, 226, 213, 0.12);
+ color: var(--teal);
}
.tag-mauve {
- background: rgba(203, 166, 247, 0.12);
- color: var(--mauve);
+ background: rgba(203, 166, 247, 0.12);
+ color: var(--mauve);
}
.tag-yellow {
- background: rgba(249, 226, 175, 0.12);
- color: var(--yellow);
+ background: rgba(249, 226, 175, 0.12);
+ color: var(--yellow);
}
.tag-sky {
- background: rgba(137, 220, 235, 0.12);
- color: var(--sky);
+ background: rgba(137, 220, 235, 0.12);
+ color: var(--sky);
}
.tag-red {
- background: rgba(243, 139, 168, 0.12);
- color: var(--red);
+ background: rgba(243, 139, 168, 0.12);
+ color: var(--red);
}
.tag-pink {
- background: rgba(245, 194, 231, 0.12);
- color: var(--pink);
+ background: rgba(245, 194, 231, 0.12);
+ color: var(--pink);
}
/* ─── Tutorials / Projects shared ─────────────────────── */
.section-heading {
- font-family: "Fraunces", serif;
- font-size: 2.8rem;
- font-weight: 600;
- color: var(--text);
- margin-bottom: 0.75rem;
+ font-family: "Fraunces", serif;
+ font-size: 2.8rem;
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 0.75rem;
}
.section-heading em {
- font-style: italic;
- color: var(--mauve);
- text-shadow: 0 0 16px rgba(203, 166, 247, 0.25);
+ font-style: italic;
+ color: var(--mauve);
+ text-shadow: 0 0 16px rgba(203, 166, 247, 0.25);
}
.section-intro {
- color: var(--subtext0);
- font-size: 0.88rem;
- max-width: 560px;
- line-height: 1.8;
- margin-bottom: 3rem;
+ color: var(--subtext0);
+ font-size: 0.88rem;
+ max-width: 560px;
+ line-height: 1.8;
+ margin-bottom: 3rem;
}
.projects-subheading {
- margin: 0 0 0.9rem;
- font-size: 0.7rem;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: var(--mauve);
+ margin: 0 0 0.9rem;
+ font-size: 0.7rem;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--mauve);
}
.card-grid.card-grid--featured {
- margin-bottom: 2rem;
- grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin-bottom: 2rem;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
}
#projects .section-intro {
- margin-bottom: 1.8rem;
- max-width: 640px;
+ margin-bottom: 1.8rem;
+ max-width: 640px;
}
#project-grid {
- grid-template-columns: repeat(3, minmax(0, 1fr));
+ grid-template-columns: repeat(3, minmax(0, 1fr));
}
#project-grid .card {
- padding: 1.2rem;
+ padding: 1.2rem;
}
.project-filters {
- display: flex;
- flex-wrap: wrap;
- gap: 0.5rem;
- border: none;
- padding: 0;
- min-width: 0;
- margin-bottom: 1.25rem;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ border: none;
+ padding: 0;
+ min-width: 0;
+ margin-bottom: 1.25rem;
}
.filter-chip {
- border: 1px solid var(--surface1);
- background: var(--mantle);
- color: var(--subtext0);
- border-radius: 999px;
- padding: 0.32rem 0.8rem;
- font-size: 0.68rem;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- font-family: "JetBrains Mono", monospace;
- cursor: pointer;
- transition: all 0.2s;
+ border: 1px solid var(--surface1);
+ background: var(--mantle);
+ color: var(--subtext0);
+ border-radius: 999px;
+ padding: 0.32rem 0.8rem;
+ font-size: 0.68rem;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ font-family: "JetBrains Mono", monospace;
+ cursor: pointer;
+ transition: all 0.2s;
}
.filter-chip:hover {
- border-color: var(--mauve);
- color: var(--text);
+ border-color: var(--mauve);
+ color: var(--text);
}
.filter-chip.active {
- background: rgba(203, 166, 247, 0.18);
- border-color: rgba(203, 166, 247, 0.7);
- color: var(--mauve);
+ background: rgba(203, 166, 247, 0.18);
+ border-color: rgba(203, 166, 247, 0.7);
+ color: var(--mauve);
}
.card.is-hidden {
- display: none;
+ display: none;
}
.card.is-collapsed {
- display: none;
+ display: none;
}
.projects-more-row {
- margin-top: 1rem;
- display: flex;
- justify-content: center;
+ margin-top: 1rem;
+ display: flex;
+ justify-content: center;
}
.projects-more-btn {
- border: 1px solid var(--surface1);
- background: var(--mantle);
- color: var(--subtext0);
- border-radius: 999px;
- padding: 0.42rem 1rem;
- font-size: 0.72rem;
- letter-spacing: 0.06em;
- text-transform: uppercase;
- font-family: "JetBrains Mono", monospace;
- cursor: pointer;
- transition: all 0.2s;
+ border: 1px solid var(--surface1);
+ background: var(--mantle);
+ color: var(--subtext0);
+ border-radius: 999px;
+ padding: 0.42rem 1rem;
+ font-size: 0.72rem;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ font-family: "JetBrains Mono", monospace;
+ cursor: pointer;
+ transition: all 0.2s;
}
.projects-more-btn:hover {
- border-color: var(--mauve);
- color: var(--text);
+ border-color: var(--mauve);
+ color: var(--text);
}
/* ─── Card grid ────────────────────────────────────────── */
.card-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
- gap: 1px;
- background: var(--surface0);
- border: 1px solid var(--surface0);
- border-radius: 8px;
- overflow: hidden;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1px;
+ background: var(--surface0);
+ border: 1px solid var(--surface0);
+ border-radius: 8px;
+ overflow: hidden;
}
.card {
- background: var(--mantle);
- padding: 1.5rem;
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- text-decoration: none;
- color: inherit;
- transition: background 0.2s;
- position: relative;
- overflow: hidden;
+ background: var(--mantle);
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ text-decoration: none;
+ color: inherit;
+ transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
+ position: relative;
+ overflow: hidden;
}
.card-link {
- cursor: pointer;
+ cursor: pointer;
+ box-shadow: inset 0 0 0 1px transparent;
}
.card-link:focus-visible {
- outline: 1px solid var(--mauve);
- outline-offset: -1px;
+ outline: none;
+ background: var(--surface0);
+ transform: translateY(-2px);
+ box-shadow:
+ inset 0 0 0 1px rgba(203, 166, 247, 0.28),
+ 0 14px 30px rgba(17, 17, 27, 0.28);
}
.card::before {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- width: 3px;
- height: 100%;
- background: var(--mauve);
- transform: scaleY(0);
- transform-origin: bottom;
- transition: transform 0.25s;
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 3px;
+ height: 100%;
+ background: var(--mauve);
+ transform: scaleY(0);
+ transform-origin: bottom;
+ transition: transform 0.25s;
}
.card:hover {
- background: var(--surface0);
- transform: translateY(-1px);
+ background: var(--surface0);
+ transform: translateY(-1px);
}
-.card:hover::before {
- transform: scaleY(1);
+.card-link:hover {
+ box-shadow:
+ inset 0 0 0 1px rgba(203, 166, 247, 0.18),
+ 0 12px 24px rgba(17, 17, 27, 0.22);
+ transform: translateY(-2px);
+}
+
+.card:hover::before,
+.card:focus-visible::before {
+ transform: scaleY(1);
}
.card-date {
- font-size: 0.68rem;
- color: var(--overlay1);
- letter-spacing: 0.06em;
+ font-size: 0.68rem;
+ color: var(--overlay1);
+ letter-spacing: 0.06em;
+}
+
+.project-status {
+ display: inline-block;
+ align-self: flex-start;
+ font-size: 0.64rem;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+ text-transform: uppercase;
+ padding: 0.12rem 0.45rem;
+ border-radius: 999px;
+}
+
+.project-status--active {
+ color: var(--green);
+ background: rgba(166, 227, 161, 0.12);
+ border: 1px solid rgba(166, 227, 161, 0.24);
}
.card-title {
- font-size: 0.92rem;
- font-weight: 700;
- color: var(--text);
- transition: color 0.2s;
+ font-size: 0.92rem;
+ font-weight: 700;
+ color: var(--text);
+ transition: color 0.2s;
+ text-decoration: underline;
+ text-decoration-color: transparent;
+ text-underline-offset: 0.18em;
}
-.card:hover .card-title {
- color: var(--mauve);
+.card:hover .card-title,
+.card:focus-visible .card-title {
+ color: var(--mauve);
+ text-decoration-color: currentColor;
}
.card-desc {
- font-size: 0.8rem;
- color: var(--subtext0);
- line-height: 1.6;
- flex: 1;
+ font-size: 0.8rem;
+ color: var(--subtext0);
+ line-height: 1.6;
+ flex: 1;
+}
+
+.card-source {
+ font-size: 0.7rem;
+ color: var(--overlay1);
+ letter-spacing: 0.04em;
+}
+
+.card-source a {
+ color: var(--sky);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.card-source a:hover {
+ color: var(--mauve);
}
.card-desc a {
- color: var(--sky);
- text-decoration: underline;
- text-underline-offset: 2px;
+ color: var(--sky);
+ text-decoration: underline;
+ text-underline-offset: 2px;
}
.card-desc a:hover {
- color: var(--mauve);
+ color: var(--mauve);
}
-.card-arrow {
- font-size: 0.8rem;
- color: var(--overlay0);
- transition:
- color 0.2s,
- transform 0.2s;
- align-self: flex-end;
- margin-top: 0.5rem;
-}
-
-.card:hover .card-arrow {
- color: var(--mauve);
- transform: translateX(3px);
-}
/* ─── Contact ──────────────────────────────────────────── */
#contact {
- min-height: auto;
- padding-bottom: 8rem;
+ min-height: auto;
+ padding-bottom: 8rem;
}
.contact-inner {
- border: 1px solid var(--surface0);
- border-radius: 8px;
- padding: 3.5rem;
- background: var(--mantle);
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 4rem;
- align-items: start;
+ border: 1px solid var(--surface0);
+ border-radius: 8px;
+ padding: 3.5rem;
+ background: var(--mantle);
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 4rem;
+ align-items: start;
}
.contact-inner h2 {
- font-family: "Fraunces", serif;
- font-size: 2.4rem;
- font-weight: 600;
- color: var(--text);
- margin-bottom: 1rem;
+ font-family: "Fraunces", serif;
+ font-size: 2.4rem;
+ font-weight: 600;
+ color: var(--text);
+ margin-bottom: 1rem;
}
.contact-inner h2 em {
- font-style: italic;
- color: var(--mauve);
+ font-style: italic;
+ color: var(--mauve);
}
.contact-inner p {
- color: var(--subtext0);
- font-size: 0.88rem;
- line-height: 1.8;
- margin-bottom: 1.5rem;
+ color: var(--subtext0);
+ font-size: 0.88rem;
+ line-height: 1.8;
+ margin-bottom: 1.5rem;
}
.contact-links {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
}
.contact-link {
- display: flex;
- align-items: center;
- gap: 1rem;
- padding: 1rem 1.25rem;
- border: 1px solid var(--surface0);
- border-radius: 6px;
- text-decoration: none;
- color: var(--text);
- background: var(--base);
- transition: all 0.2s;
- font-size: 0.85rem;
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem 1.25rem;
+ border: 1px solid var(--surface0);
+ border-radius: 6px;
+ text-decoration: none;
+ color: var(--text);
+ background: var(--base);
+ transition: all 0.2s;
+ font-size: 0.85rem;
}
.contact-link:hover {
- border-color: var(--mauve);
- color: var(--mauve);
- transform: translateX(4px);
+ border-color: var(--mauve);
+ color: var(--mauve);
+ transform: translateX(4px);
}
.contact-link-icon {
- width: 1.125rem;
- height: 1.125rem;
- flex: 0 0 1.125rem;
- background: currentColor;
- -webkit-mask-repeat: no-repeat;
- -webkit-mask-position: center;
- -webkit-mask-size: contain;
- mask-repeat: no-repeat;
- mask-position: center;
- mask-size: contain;
+ width: 1.125rem;
+ height: 1.125rem;
+ flex: 0 0 1.125rem;
+ background: currentColor;
+ -webkit-mask-repeat: no-repeat;
+ -webkit-mask-position: center;
+ -webkit-mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
}
.contact-link-icon--github {
- -webkit-mask-image: url("icons/github.svg");
- mask-image: url("icons/github.svg");
+ -webkit-mask-image: url("icons/github.svg");
+ mask-image: url("icons/github.svg");
}
.contact-link-icon--linkedin {
- -webkit-mask-image: url("icons/linkedin.svg");
- mask-image: url("icons/linkedin.svg");
+ -webkit-mask-image: url("icons/linkedin.svg");
+ mask-image: url("icons/linkedin.svg");
}
.contact-link-label {
- flex: 1;
+ flex: 1;
}
.contact-link-handle {
- font-size: 0.72rem;
- color: var(--overlay1);
+ font-size: 0.72rem;
+ color: var(--overlay1);
}
/* ─── Footer ───────────────────────────────────────────── */
footer {
- border-top: 1px solid var(--surface0);
- padding: 1.5rem 3rem;
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 0.72rem;
- color: var(--overlay0);
+ border-top: 1px solid var(--surface0);
+ padding: 1.5rem 3rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 0.72rem;
+ color: var(--overlay0);
}
footer a {
- color: var(--overlay1);
- text-decoration: none;
+ color: var(--overlay1);
+ text-decoration: none;
}
footer a:hover {
- color: var(--mauve);
+ color: var(--mauve);
}
/* ─── Animations ───────────────────────────────────────── */
@keyframes fadeUp {
- from {
- opacity: 0;
- transform: translateY(20px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
.reveal {
- opacity: 0;
- transform: translateY(24px);
- transition:
- opacity 0.6s ease,
+ opacity: 0;
+ transform: translateY(24px);
+ transition: opacity 0.6s ease,
transform 0.6s ease;
}
.reveal.visible {
- opacity: 1;
- transform: none;
+ opacity: 1;
+ transform: none;
}
/* ─── Divider ──────────────────────────────────────────── */
.divider {
- height: 1px;
- background: var(--surface0);
- max-width: 1100px;
- margin: 0 auto;
+ height: 1px;
+ background: var(--surface0);
+ max-width: 1100px;
+ margin: 0 auto;
}
/* ─── Responsive ───────────────────────────────────────── */
@media (max-width: 768px) {
- nav {
- padding: 0 1.5rem;
- }
+ nav {
+ padding: 0 1.5rem;
+ }
- .nav-links {
- gap: 1.2rem;
- }
+ .nav-links {
+ gap: 1.2rem;
+ }
- section {
- padding: 6rem 1.5rem 4rem;
- }
+ section {
+ padding: 6rem 1.5rem 4rem;
+ }
- .card-grid.card-grid--featured {
- grid-template-columns: 1fr;
- }
+ .card-grid.card-grid--featured {
+ grid-template-columns: 1fr;
+ }
- #project-grid {
- grid-template-columns: 1fr;
- }
+ #project-grid {
+ grid-template-columns: 1fr;
+ }
- .about-columns {
- grid-template-columns: 1fr;
- }
+ .about-columns {
+ grid-template-columns: 1fr;
+ }
- .about-block-tag {
- width: 4rem;
- }
+ .about-block-tag {
+ width: 4rem;
+ }
- .skills-grid {
- grid-template-columns: 1fr;
- }
+ .skills-grid {
+ grid-template-columns: 1fr;
+ }
- .skill-card--wide {
- grid-column: 1;
- }
+ .skill-card--wide {
+ grid-column: 1;
+ }
- .contact-inner {
- grid-template-columns: 1fr;
- gap: 2rem;
- padding: 2rem;
- }
+ .contact-inner {
+ grid-template-columns: 1fr;
+ gap: 2rem;
+ padding: 2rem;
+ }
- .hero-name {
- font-size: clamp(2.8rem, 12vw, 5rem);
- }
+ .hero-name {
+ font-size: clamp(2.8rem, 12vw, 5rem);
+ }
- .hero-by-line {
- align-items: flex-start;
- flex-wrap: wrap;
- gap: 0.35rem;
- margin-bottom: 0.45rem;
- }
+ .hero-by-line {
+ align-items: flex-start;
+ flex-wrap: wrap;
+ gap: 0.35rem;
+ margin-bottom: 0.45rem;
+ }
- .hero-by-line:last-child {
- margin-bottom: 0;
- }
+ .hero-by-line:last-child {
+ margin-bottom: 0;
+ }
- .hero-by {
- font-size: 0.86rem;
- line-height: 1.5;
- }
+ .hero-by {
+ font-size: 0.86rem;
+ line-height: 1.5;
+ }
- .hero-by-role {
- font-size: 0.8rem;
- line-height: 1.5;
- opacity: 0.75;
- }
+ .hero-by-role {
+ font-size: 0.8rem;
+ line-height: 1.5;
+ opacity: 0.75;
+ }
- footer {
- flex-direction: column;
- gap: 0.5rem;
- text-align: center;
- }
+ footer {
+ flex-direction: column;
+ gap: 0.5rem;
+ text-align: center;
+ }
}