From af0cb702a2404fde5d00e094c1df1de3f398b3b2 Mon Sep 17 00:00:00 2001 From: John Gatward Date: Fri, 20 Mar 2026 18:46:38 +0000 Subject: [PATCH] formatting --- index.html | 716 +++++++++++++++---------------- script.js | 116 ++--- style.css | 1213 +++++++++++++++++++++++++++++----------------------- 3 files changed, 1099 insertions(+), 946 deletions(-) diff --git a/index.html b/index.html index 98b5bff..5ff6d35 100644 --- a/index.html +++ b/index.html @@ -2,465 +2,467 @@ - - + + Havox — John Gatward - + - + href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400&family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,600;1,9..144,300&display=swap" + rel="stylesheet"> + - - + + - -
-

// havox.org — v4

-

John Gatward

-

Software Engineer

-

+ +

+

// havox.org — v4

+

John Gatward

+

Software Engineer

+

-

- -
+ + I like building useful things and understanding how they work. +

+ +
-
+
- -
-
- -

Havox & me

-
+ +
+
+ +

Havox & me

+
-
+
- -
- -
- day job -

Backend engineer at Sainsbury's Supply Chain & Logistics. Microservices, - Spring Boot, Kafka, MongoDB, AWS. It's good fun.

-
- -
- dev -

Fascinated with new languages, frameworks, and shiny tech I probably don’t need. Constantly - tinkering, breaking things, and learning just enough to build something cooler next time.

-
- -
- linux -

Long-time Linux user and recovering distro-hopper. Currently: Hyprland on Void. Home server - running 24/7 for friends and family who didn't ask for it but appreciate it.

-
- -
- havox -

Started in 2017 as a place to dump whatever I found interesting. Four redesigns and several dead - domains later, it's still going. Less a portfolio, more a paper trail.

-
+ +
+
+ day job +

Backend engineer at Sainsbury's Supply Chain & Logistics. Microservices, + Spring Boot, Kafka, MongoDB, AWS. It's good fun.

- -
- -
+
+ dev +

Fascinated with new languages, frameworks, and shiny tech I probably don’t need. Constantly + tinkering, breaking things, and learning just enough to build something cooler next time.

+
-
-
-
-
Havox V4 current
-
2026 → present
-
Catppuccin Mocha. Single page. Built to highlight projects and craft.
+
+ linux +

Long-time Linux user and recovering distro-hopper. Currently: Hyprland on Void. Home server + running 24/7 for friends and family who didn't ask for it but appreciate it.

+
+ +
+ havox +

Started in 2017 as a place to dump whatever I found interesting. Four redesigns and several dead + domains later, it's still going. Less a portfolio, more a paper trail.

+
+ +
+ + + - -
- -
- -
- - -
- -

What I know

- -
-
- -

Backend

-

Professional Java experience building microservices & RESTful APIs with Spring Boot. TDD with - JUnit & Mockito.

-
- Java - Spring Boot - AWS - Kafka - MongoDB -
-
-
- 🎨 -

Frontend & Creative

-

Comfortable with react and CSS. I enjoy visualising algorithms interactively — it's more fun than - a console output.

-
- React - TypeScript - HTML/CSS -
-
-
- 🖥 -

Systems & Infra

-

Long-term Linux user. Self-hosted web & media servers. Comfortable with networking, security - practices, and the command line.

-
- 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 -
+
-
-
+ - -
- -

Things I built

-

- A timeline of projects focused on technical challenge and what each build taught me. -

+
-
- Featured projects +
+ + +
+ +

What I know

+ +
+
+ +

Backend

+

Professional Java experience building microservices & RESTful APIs with Spring Boot. TDD with + JUnit & Mockito.

+
+ Java + Spring Boot + AWS + Kafka + MongoDB +
+
+ 🎨 +

Frontend & Creative

+

Comfortable with react and CSS. I enjoy visualising algorithms interactively — it's more fun than + a console output.

+
+ React + TypeScript + HTML/CSS +
+
+
+ 🖥 +

Systems & Infra

+

Long-term Linux user. Self-hosted web & media servers. Comfortable with networking, security + practices, and the command line.

+
+ 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 +
+
+
+
- -
- All projects -
+
+ All projects +
-
- - - - - - - - - -
+
+ + + + + + + + + +
-
- - 03 Apr 2025 - Pub Quiz Dashboard - A Python dashboard to track quiz performance. - +
+ + 03 Apr 2025 + Pub Quiz Dashboard + A Python dashboard to track quiz performance. + Python - - - - 14 Nov 2024 - Thin Ice - A small browser tile game inspired by one I played growing up. - + + + + 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 - A nearest-neighbor + 2-opt visualiser. - + + + + 20 Mar 2024 + Travelling Salesman Problem + A nearest-neighbor + 2-opt visualiser. + Zig WebGL WebAssembly - - - - 31 Jan 2024 - Flocking - A boids simulation showing emergent behaviour from three simple rules. - + + + + 31 Jan 2024 + Flocking + A boids simulation showing emergent behaviour from three simple rules. + Rust Raylib - - - - 1 Nov 2023 - samstoreymusic.com - A website design and build for a friend working in music. - + + + + 1 Nov 2023 + samstoreymusic.com + A website design and build for a friend working in music. + HTML/CSS JavaScript - - - - 21 Jul 2023 - Game of Life - After building Conway's Game of Life in p5.js, I expanded it into a generic automata simulator. - + + + + 21 Jul 2023 + Game of Life + After building Conway's Game of Life in p5.js, I expanded it into a generic automata simulator. + React JavaScript - - - - 5 Feb 2022 - Percolation - A visual demo of 'water' percolating through a medium, as it disappears. - + + + + 5 Feb 2022 + Percolation + A visual demo of 'water' percolating through a medium, as it disappears. + C RayLib WebAssembly - - - - 1 Oct 2021 - Drawing Bézier curves - An interactive diagram for cubic Bézier curves. - + + + + 1 Oct 2021 + Drawing Bézier curves + An interactive diagram for cubic Bézier curves. + JavaScript p5.js - - - - 17 Sep 2021 - 2D Marching Squares - Basic map rendering. - + + + + 17 Sep 2021 + 2D Marching Squares + Basic map rendering. + JavaScript p5.js - - - - 2 Apr 2020 - Müller-Lyer illusion - A JavaScript visualisation of the Müller-Lyer optical illusion. - + + + + 2 Apr 2020 + Müller-Lyer illusion + A JavaScript visualisation of the Müller-Lyer optical illusion. + JavaScript p5.js - - - - 27 Feb 2019 - Fourier series - Builds a square wave from sine components to show Fourier series in motion. - + + + + 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 - A geometric construction demo inspired by 3Blue1Brown. - + + + + 23 Feb 2019 + Constructing an ellipse + A geometric construction demo inspired by 3Blue1Brown. + JavaScript p5.js - - - - 18 Mar 2018 - Calculating PI - A Monte Carlo approximation of pi using random sampling. - + + + + 18 Mar 2018 + Calculating PI + A Monte Carlo approximation of pi using random sampling. + JavaScript p5.js - - - - 17 Dec 2017 - Oscillations in 3D - A JavaScript recreation of a Bees and Bombs animation. - + + + + 17 Dec 2017 + Oscillations in 3D + A JavaScript recreation of a Bees and Bombs animation. + JavaScript p5.js - - - - 13 Nov 2017 - Maze generator - A p5.js maze generator using depth-first search and recursive backtracking. - + + + + 13 Nov 2017 + Maze generator + A p5.js maze generator using depth-first search and recursive backtracking. + JavaScript p5.js - - + + +
+ +
+ +
+
+ +
+ + +
+ +
+
+

Let's talk

+

+ I am currently employed and open to the right software engineering opportunity, particularly + backend or full-stack roles with teams that value thoughtful delivery and technical quality. +

+

+ For professional enquiries, please connect with me on LinkedIn. +

- -
- -
-
- -
- - -
- -
-
-

Let's talk

-

- I am currently employed and open to the right software engineering opportunity, particularly - backend or full-stack roles with teams that value thoughtful delivery and technical quality. -

-

- For professional enquiries, please connect with me on LinkedIn. -

-
- -
+ + - - - + diff --git a/script.js b/script.js index a5f9972..77b4b27 100644 --- a/script.js +++ b/script.js @@ -1,26 +1,26 @@ // ─── 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); - } - }); -}, { threshold: 0.1, rootMargin: '0px 0px -40px 0px' }); + entries.forEach(e => { + if (e.isIntersecting) { + e.target.classList.add('visible'); + observer.unobserve(e.target); + } + }); +}, {threshold: 0.1, rootMargin: '0px 0px -40px 0px'}); reveals.forEach(el => observer.observe(el)); // ─── 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; - }); - navLinks.forEach(a => { - a.style.color = a.getAttribute('href') === `#${current}` ? 'var(--mauve)' : ''; - }); + let current = ''; + sections.forEach(s => { + if (window.scrollY >= s.offsetTop - 120) current = s.id; + }); + navLinks.forEach(a => { + a.style.color = a.getAttribute('href') === `#${current}` ? 'var(--mauve)' : ''; + }); }); // ─── Project filters ────────────────────────────────────────────── @@ -30,57 +30,57 @@ const seeMoreButton = document.querySelector('#projects-see-more'); const DEFAULT_VISIBLE_PROJECTS = 8; if (filterChips.length > 0 && projectCards.length > 0) { - let selectedFilter = 'all'; - let isExpanded = false; + let selectedFilter = 'all'; + let isExpanded = false; - const applyFilter = () => { - const visibleCards = []; + const applyFilter = () => { + const visibleCards = []; - projectCards.forEach(card => { - const tech = (card.dataset.tech || '').split(/\s+/).filter(Boolean); - const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter); - card.classList.toggle('is-hidden', !shouldShow); + projectCards.forEach(card => { + const tech = (card.dataset.tech || '').split(/\s+/).filter(Boolean); + 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) { + card.classList.remove('is-collapsed'); + visibleCards.push(card); + } + }); + + if (!isExpanded) { + 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'; + } + }; + + filterChips.forEach(chip => { + chip.addEventListener('click', () => { + selectedFilter = chip.dataset.filter || 'all'; + isExpanded = false; + + filterChips.forEach(other => { + other.classList.toggle('active', other === chip); + }); + + applyFilter(); + }); }); - if (!isExpanded) { - 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'; + seeMoreButton.addEventListener('click', () => { + isExpanded = !isExpanded; + applyFilter(); + }); } - }; - filterChips.forEach(chip => { - chip.addEventListener('click', () => { - selectedFilter = chip.dataset.filter || 'all'; - isExpanded = false; - - filterChips.forEach(other => { - other.classList.toggle('active', other === chip); - }); - - applyFilter(); - }); - }); - - if (seeMoreButton) { - seeMoreButton.addEventListener('click', () => { - isExpanded = !isExpanded; - applyFilter(); - }); - } - - applyFilter(); + applyFilter(); } diff --git a/style.css b/style.css index 601f2e3..f821d55 100644 --- a/style.css +++ b/style.css @@ -1,864 +1,1015 @@ /* ─── 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; } +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} -html { scroll-behavior: smooth; } +html { + scroll-behavior: smooth; +} body { - background: var(--base); - color: var(--text); - font-family: 'JetBrains Mono', 'Apple Color Emoji', monospace; - font-size: 15px; - line-height: 1.7; - overflow-x: hidden; - position: relative; + background: var(--base); + 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: 36rem; - height: 36rem; - border-radius: 50%; - pointer-events: none; - z-index: -1; - filter: blur(70px); - opacity: 0.12; + content: ''; + position: fixed; + width: 36rem; + height: 36rem; + border-radius: 50%; + pointer-events: none; + z-index: -1; + filter: blur(70px); + opacity: 0.12; } body::before { - top: -10rem; - right: -10rem; - background: radial-gradient(circle, var(--mauve), transparent 65%); + top: -10rem; + right: -10rem; + background: radial-gradient(circle, var(--mauve), transparent 65%); } body::after { - bottom: -14rem; - left: -12rem; - background: radial-gradient(circle, var(--blue), transparent 65%); + bottom: -14rem; + left: -12rem; + background: radial-gradient(circle, var(--blue), transparent 65%); } -::selection { background: var(--mauve); color: var(--crust); } +::selection { + background: var(--mauve); + color: var(--crust); +} /* ─── Scrollbar ────────────────────────────────────────── */ -::-webkit-scrollbar { width: 6px; } -::-webkit-scrollbar-track { background: var(--mantle); } -::-webkit-scrollbar-thumb { background: var(--surface1); border-radius: 3px; } -::-webkit-scrollbar-thumb:hover { background: var(--mauve); } +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + background: var(--mantle); +} + +::-webkit-scrollbar-thumb { + background: var(--surface1); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + 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); } -.nav-links a:hover::after { transform: scaleX(1); } +.nav-links a:hover { + color: var(--text); +} + +.nav-links a:hover::after { + 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; + font-family: 'Fraunces', serif; + font-style: italic; + font-size: 0.85rem; + color: var(--mauve); + opacity: 0.8; } .hero-by-role::before { - content: '— '; - opacity: 0.4; + content: '— '; + opacity: 0.4; } .hero-by-footer { - display: block; - margin-top: 1rem; - color: var(--subtext0); - font-size: 0.82rem; - opacity: 0.75; + display: block; + margin-top: 1rem; + color: var(--subtext0); + font-size: 0.82rem; + opacity: 0.75; } .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), 0 10px 26px rgba(17,17,27,0.4); + 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); } -.btn-primary:hover { 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); } -.btn-ghost:hover { 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-block:last-child { border-bottom: none; } +.about-block:last-child { + 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; } -.about-block p em { color: var(--mauve); font-style: italic; } -.about-block p s { color: var(--overlay1); } +.about-block p strong { + color: var(--text); + font-weight: 700; +} + +.about-block p em { + color: var(--mauve); + font-style: italic; +} + +.about-block p s { + 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, background 0.2s; - position: relative; - z-index: 1; + 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; } .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, transform 0.2s; - display: inline-block; + font-size: 0.7rem; + color: var(--overlay1); + margin-left: 0.3rem; + transition: color 0.2s, transform 0.2s; + 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); } +#skills h2 em { + 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 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); } -.tag-green { background: rgba(166,227,161,0.12); color: var(--green); } -.tag-peach { background: rgba(250,179,135,0.12); color: var(--peach); } -.tag-teal { background: rgba(148,226,213,0.12); color: var(--teal); } -.tag-mauve { background: rgba(203,166,247,0.12); color: var(--mauve); } -.tag-yellow { background: rgba(249,226,175,0.12); color: var(--yellow); } -.tag-sky { background: rgba(137,220,235,0.12); color: var(--sky); } -.tag-red { background: rgba(243,139,168,0.12); color: var(--red); } -.tag-pink { background: rgba(245,194,231,0.12); color: var(--pink); } +.tag-blue { + background: rgba(137, 180, 250, 0.12); + color: var(--blue); +} + +.tag-green { + background: rgba(166, 227, 161, 0.12); + color: var(--green); +} + +.tag-peach { + background: rgba(250, 179, 135, 0.12); + color: var(--peach); +} + +.tag-teal { + background: rgba(148, 226, 213, 0.12); + color: var(--teal); +} + +.tag-mauve { + background: rgba(203, 166, 247, 0.12); + color: var(--mauve); +} + +.tag-yellow { + background: rgba(249, 226, 175, 0.12); + color: var(--yellow); +} + +.tag-sky { + background: rgba(137, 220, 235, 0.12); + color: var(--sky); +} + +.tag-red { + background: rgba(243, 139, 168, 0.12); + color: var(--red); +} + +.tag-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; + position: relative; + overflow: hidden; } .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:hover::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; } .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; } -.card:hover .card-title { color: var(--mauve); } +.card:hover .card-title { + color: var(--mauve); +} .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-arrow { - font-size: 0.8rem; - color: var(--overlay0); - transition: color 0.2s, transform 0.2s; - align-self: flex-end; - margin-top: 0.5rem; + 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); } +.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); } +.contact-inner h2 em { + 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 { - font-size: 1.1rem; - width: 1.5rem; - text-align: center; + font-size: 1.1rem; + width: 1.5rem; + text-align: center; } .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; } -footer a:hover { color: var(--mauve); } +footer a { + color: var(--overlay1); + text-decoration: none; +} + +footer a:hover { + 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, transform 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-links { gap: 1.2rem; } - section { padding: 6rem 1.5rem 4rem; } - .card-grid.card-grid--featured { grid-template-columns: 1fr; } - #project-grid { grid-template-columns: 1fr; } - .about-columns { grid-template-columns: 1fr; } - .about-block-tag { width: 4rem; } - .skills-grid { grid-template-columns: 1fr; } - .skill-card--wide { grid-column: 1; } - .contact-inner { grid-template-columns: 1fr; gap: 2rem; padding: 2rem; } - .hero-name { font-size: clamp(2.8rem, 12vw, 5rem); } - footer { flex-direction: column; gap: 0.5rem; text-align: center; } + nav { + padding: 0 1.5rem; + } + + .nav-links { + gap: 1.2rem; + } + + section { + padding: 6rem 1.5rem 4rem; + } + + .card-grid.card-grid--featured { + grid-template-columns: 1fr; + } + + #project-grid { + grid-template-columns: 1fr; + } + + .about-columns { + grid-template-columns: 1fr; + } + + .about-block-tag { + width: 4rem; + } + + .skills-grid { + grid-template-columns: 1fr; + } + + .skill-card--wide { + grid-column: 1; + } + + .contact-inner { + grid-template-columns: 1fr; + gap: 2rem; + padding: 2rem; + } + + .hero-name { + font-size: clamp(2.8rem, 12vw, 5rem); + } + + footer { + flex-direction: column; + gap: 0.5rem; + text-align: center; + } }