From 16bd425c209dd81df5b9d2490e751ff966b21df7 Mon Sep 17 00:00:00 2001 From: John Gatward Date: Tue, 31 Mar 2026 21:54:01 +0100 Subject: [PATCH] Added open source links --- icons/docker-logo.svg | 25 + icons/haskell-logo.svg | 6 + icons/java-icon.svg | 45 ++ icons/react-logo.svg | 1 + index.html | 913 +++++++++++++------------- script.js | 195 +++--- style.css | 1371 ++++++++++++++++++++-------------------- 7 files changed, 1348 insertions(+), 1208 deletions(-) create mode 100644 icons/docker-logo.svg create mode 100644 icons/haskell-logo.svg create mode 100644 icons/java-icon.svg create mode 100644 icons/react-logo.svg diff --git a/icons/docker-logo.svg b/icons/docker-logo.svg new file mode 100644 index 0000000..09a5a66 --- /dev/null +++ b/icons/docker-logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/icons/haskell-logo.svg b/icons/haskell-logo.svg new file mode 100644 index 0000000..d7b1862 --- /dev/null +++ b/icons/haskell-logo.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/icons/java-icon.svg b/icons/java-icon.svg new file mode 100644 index 0000000..35b1f6a --- /dev/null +++ b/icons/java-icon.svg @@ -0,0 +1,45 @@ + + + + + + + + diff --git a/icons/react-logo.svg b/icons/react-logo.svg new file mode 100644 index 0000000..8bf7ec4 --- /dev/null +++ b/icons/react-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html index 4e569b1..b121b1d 100644 --- a/index.html +++ b/index.html @@ -1,686 +1,707 @@ - - - + + + Havox - - + + - - + + - - - - -
-

// havox.org - v4

-

John Gatward

-

Software Engineer

-

+ +

+

// havox.org - v4

+

John Gatward

+

Software Engineer

+

Backend engineerby trade Developer enthusiastby curiosity Unofficial family cloud engineerby necessity -

-
+ +
-
+
- -
-
+ +
+

Havox & me

-
+
-
+
-
-

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

+
+ + -
+
- -
- -

+ +
+ +

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

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

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

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

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 my projects and an overview of my journey as a developer. Often times getting inspired and trying to recreate something cool I saw. -

+

-
+
Featured projects -
+
- + + -
+
All projects -
+
-
+
-
+
-
+
-
+
-
+
-
-
+ + -
+
- -
- -
+ +
+ +
-

Let's talk

-

- I am currently employed and open to the right software engineering - opportunity, particularly backend or full-stack roles with friendly, - close-knit teams. -

-

For professional enquiries, please contact 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 friendly, + close-knit teams. +

+

For professional enquiries, please contact me on LinkedIn.

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