diff --git a/index.html b/index.html index d07ece2..451d83d 100644 --- a/index.html +++ b/index.html @@ -15,13 +15,10 @@ + - - - @@ -51,7 +48,7 @@
-

Nikolaj Gade

+

Nikolaj Gade

Software Developer

they/them · he/him

@@ -85,18 +82,13 @@
-
- scroll -
-
- 01 -

<About/>

+

About

@@ -121,8 +113,7 @@
- 02 -

<Skills/>

+

Skills

@@ -131,7 +122,7 @@
Python Bash - C + C C# SQL Go @@ -158,8 +149,7 @@
- 03 -

<Projects/>

+

Projects

@@ -187,7 +177,7 @@

Both are projects I actively maintain and support, with a small but dedicated community of around 500 players.

- PythonPCSX2 / PINEReverse engineeringArchipelagoOpen source + PythonPCSX2 / PINEReverse engineeringArchipelagoasyncio
@@ -209,7 +199,7 @@

The language includes a tree-walking interpreter and a compiler that targets C. Optional modules extend the standard library with FORS (random numbers), FRACTIO (base-12 fractions for floats), MAGNVM (higher numbers than standard roman numerals), and SVBNVLLA (negative numbers).

- PythonLanguage designCompilerInterpreter + PythonLanguage designCompilerLSP
@@ -235,11 +225,11 @@
-

A trading card game where every card is generated from a Wikipedia article. Built for friends, it features booster pack opening, a fully implemented card game, and trading between players. It also, naturally, includes predatory microtransactions.

+

A trading card game where every card is generated from a Wikipedia article. Built for friends, it features booster pack opening, a fully implemented card game, and trading between players. Matches run in real time against friends or AI opponents, one of which is a neural network trained through self-play. It also, naturally, includes predatory microtransactions.

The frontend is built in Svelte and the backend in Python, deployed with Docker Compose.

- SveltePythonDockerAPIsFull-stack + SveltePythonDockerAPIsGame AIPostgreSQL
@@ -254,12 +244,12 @@

Born out of a deep distrust of Microsoft and Google, this project is my ongoing effort to self-host as much of my digital life as possible. Every device on my network is named after a Pokémon and assigned a local IP of 10.0.0.x, where x is its Pokédex number.

-

The primary server, Charizard, runs Unraid OS and manages a disk array totalling 44 TB. It hosts a Gitea instance for personal projects, a file server, and a media server, all running in Docker containers and exposed to the internet through Nginx Proxy Manager with SSL certificates from Let's Encrypt.

+

The primary server, Charizard, runs Unraid OS and manages a disk array totalling 44 TB. It hosts a Gitea instance for personal projects, a file server, and a media server, exposed to the internet through Nginx Proxy Manager with SSL certificates from Let's Encrypt.

An extension of the setup lives on Bulbasaur, a Raspberry Pi running Pi-hole for network-wide ad blocking and a WireGuard VPN server for secure remote access.

Charizard is turning five this year. In that time I've learned an enormous amount about networking, Linux administration, and the particular satisfaction of fixing something broken at 2 in the morning that nobody asked you to build in the first place.

- UnraidDockerReverse ProxyNetworkingSelf-hosting + UnraidDockerReverse ProxyNetworkingSelf-hosting
@@ -271,8 +261,7 @@
- 04 -

<Experience/>

+

Experience

@@ -280,7 +269,7 @@
- Sep 2024 — Present + Sep 2024 — Jul 2026 Part-time

Junior Developer

@@ -331,8 +320,7 @@
- 05 -

<Education/>

+

Education

diff --git a/script.js b/script.js index 24c367c..63fad0a 100644 --- a/script.js +++ b/script.js @@ -1,47 +1,3 @@ -// ── Hero typewriter ──────────────────────────────────────────── -function startHeroTypewriter(nameEl, fullName, restEls) { - const cursor = document.createElement('span'); - cursor.className = 'hero-cursor'; - nameEl.appendChild(cursor); - - let i = 0; - function typeNext() { - if (i < fullName.length) { - nameEl.insertBefore(document.createTextNode(fullName[i]), cursor); - i++; - setTimeout(typeNext, 95); - } else { - // Cursor done — fade it out, then reveal the rest - cursor.style.transition = 'opacity 0.3s'; - cursor.style.opacity = '0'; - setTimeout(() => { - cursor.remove(); - restEls.forEach((el, idx) => { - setTimeout(() => el.classList.add('visible'), idx * 110); - }); - }, 0); - } - } - typeNext(); -} - -// ── Hero intro ──────────────────────────────────────────────── -(function () { - const nameEl = document.querySelector('.hero-name'); - const restEls = [...document.querySelectorAll('.hero-role, .hero-pronouns, .hero-links, .hero-visual')]; - - // Store the name and clear it so the typewriter can type it out - const fullName = nameEl.textContent.trim(); - nameEl.textContent = ''; - - // Hide hero elements that reveal after typing. They're already hidden by the - // .js pre-reveal block in style.css; this just adds the transition. - restEls.forEach(el => el.classList.add('fade-up')); - - startHeroTypewriter(nameEl, fullName, restEls); -})(); - - // ── Sprinkle background ──────────────────────────── (function () { const canvas = document.getElementById('bgCanvas'); @@ -67,6 +23,11 @@ function startHeroTypewriter(nameEl, fullName, restEls) { if (s.x > W) s.x = rand(0, W); if (s.y > H) s.y = rand(0, H); }); + // Resizing clears the canvas; with no animation loop running, repaint it + if (reduceMotion.matches) { + cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(draw); + } } function init() { @@ -109,6 +70,9 @@ function startHeroTypewriter(nameEl, fullName, restEls) { ctx.stroke(); ctx.restore(); + // Reduced motion: sprinkles stay where they are + if (reduceMotion.matches) return; + // Drift s.x += s.vx; s.y += s.vy; @@ -138,9 +102,17 @@ function startHeroTypewriter(nameEl, fullName, restEls) { if (s.x > W + MAX_LEN) s.x = -MAX_LEN; }); - requestAnimationFrame(draw); + if (!reduceMotion.matches) rafId = requestAnimationFrame(draw); } + // With reduced motion the sprinkles render once as a static backdrop + const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)'); + let rafId; + reduceMotion.addEventListener('change', () => { + cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(draw); + }); + window.addEventListener('resize', resize, { passive: true }); resize(); let mouseX = -9999, mouseY = -9999; @@ -148,7 +120,7 @@ function startHeroTypewriter(nameEl, fullName, restEls) { mouseX = e.clientX; mouseY = e.clientY; }, { passive: true }); - requestAnimationFrame(draw); + rafId = requestAnimationFrame(draw); })(); @@ -193,25 +165,61 @@ function startHeroTypewriter(nameEl, fullName, restEls) { })(); -// ── Scroll fade-in ───────────────────────────────────────────── +// ── Avatar spin on click ────────────────────────────────────── (function () { - // Keep this list in sync with the .js pre-reveal block in style.css - const targets = document.querySelectorAll( - '.section-header, .about-grid, .skill-card, .timeline-item, ' + - '.project-card, .edu-card' - ); - targets.forEach(el => el.classList.add('fade-up')); + const avatar = document.querySelector('.avatar-ring'); - const observer = new IntersectionObserver((entries) => { - entries.forEach(e => { - if (!e.isIntersecting) return; - const siblings = [...e.target.parentElement.querySelectorAll('.fade-up')]; - const idx = siblings.indexOf(e.target); - setTimeout(() => e.target.classList.add('visible'), idx * 80); - observer.unobserve(e.target); + avatar.addEventListener('click', () => { + if (avatar.classList.contains('spinning')) return; + avatar.classList.add('spinning'); + }); + avatar.addEventListener('animationend', () => { + avatar.classList.remove('spinning'); + }); +})(); + + +// ── Tag pill tooltips: tap toggle (touch devices only) ──────── +(function () { + // Hover-capable devices already get the tooltips via :hover + if (window.matchMedia('(hover: hover)').matches) return; + + const pills = [...document.querySelectorAll('[data-tip]')]; + + function closeAll() { + pills.forEach(p => { + p.classList.remove('tip-open'); + p.setAttribute('aria-expanded', 'false'); + }); + } + + pills.forEach(pill => { + pill.setAttribute('tabindex', '0'); + pill.setAttribute('role', 'button'); + pill.setAttribute('aria-expanded', 'false'); + // Pseudo-element tooltips are invisible to screen readers + pill.setAttribute('aria-description', pill.dataset.tip); + + pill.addEventListener('click', (e) => { + e.stopPropagation(); + const wasOpen = pill.classList.contains('tip-open'); + closeAll(); + if (!wasOpen) { + pill.classList.add('tip-open'); + pill.setAttribute('aria-expanded', 'true'); + } + }); + pill.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + pill.click(); + } }); - }, { threshold: 0.1 }); + }); - targets.forEach(el => observer.observe(el)); + document.addEventListener('click', closeAll); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeAll(); + }); })(); diff --git a/style.css b/style.css index f4c9259..a85ca57 100644 --- a/style.css +++ b/style.css @@ -1,6 +1,7 @@ :root { --red: #e63946; --orange: #f4722b; + --orange-deep: #a84a0c; /* text-safe orange: ≥4.5:1 on cream */ --yellow: #ffbe0b; --pink: #ff006e; --cream: #fefee3; @@ -9,11 +10,13 @@ --glass-bg: rgba(254, 254, 227, 0.75); --glass-border: rgba(244, 114, 43, 0.25); --card-shadow: 0 8px 32px rgba(230, 57, 70, 0.10); - --glow-orange: 0 0 28px rgba(244, 114, 43, 0.40); - --glow-red: 0 0 28px rgba(230, 57, 70, 0.35); --grad: linear-gradient(135deg, var(--red), var(--orange), var(--yellow)); --font: 'Outfit', sans-serif; --mono: 'Fira Code', monospace; + --radius-sm: 6px; + --radius-md: 12px; + --radius-lg: 18px; + --radius-pill: 999px; --nav-h: 64px; --max-w: 1100px; --ease: cubic-bezier(0.25, 0.46, 0.45, 0.94); @@ -66,8 +69,7 @@ nav { display: flex; align-items: center; justify-content: space-between; } .nav-logo { - font-family: var(--mono); - font-size: 1.15rem; + font-size: 1.2rem; font-weight: 700; color: var(--ink); transition: color 0.2s; @@ -89,11 +91,11 @@ nav { } .nav-link { font-weight: 700; - font-size: 0.8rem; + font-size: 0.75rem; letter-spacing: 0.05em; text-transform: uppercase; - padding: 0.45rem 0.8rem; - border-radius: 6px; + padding: 0.5rem 0.75rem; + border-radius: var(--radius-sm); color: var(--ink-soft); position: relative; transition: color 0.2s; @@ -105,7 +107,7 @@ nav { transform: translateX(-50%); width: 0; height: 2px; background: var(--grad); - border-radius: 99px; + border-radius: var(--radius-pill); transition: width 0.25s var(--ease); } .nav-link:hover, @@ -114,7 +116,7 @@ nav { .nav-link.active::after { width: 70%; } .burger { display: none; flex-direction: column; gap: 5px; padding: 8px; } -.burger span { display: block; width: 22px; height: 2px; background: var(--ink); border-radius: 99px; transition: all 0.3s var(--ease); } +.burger span { display: block; width: 22px; height: 2px; background: var(--ink); border-radius: var(--radius-pill); transition: all 0.3s var(--ease); } .burger.open span:nth-child(1) { transform: rotate(45deg) translate(5px, 5px); } .burger.open span:nth-child(2) { opacity: 0; } .burger.open span:nth-child(3) { transform: rotate(-45deg) translate(5px, -5px); } @@ -126,59 +128,17 @@ main { position: relative; z-index: 1; } padding: 7rem 0; position: relative; background: rgba(254, 254, 227, 0.5); - /* backdrop-filter: blur(2px); - -webkit-backdrop-filter: blur(2px); */ } .section-inner { max-width: var(--max-w); margin: 0 auto; padding: 0 2rem; } -.section-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 3rem; } -.section-num { font-family: var(--mono); font-size: 0.75rem; color: var(--orange); font-weight: 500; min-width: 24px; } +.section-header { display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem; } .section-title { font-size: clamp(1.8rem, 3vw, 2.4rem); font-weight: 900; letter-spacing: -0.02em; white-space: nowrap; } -.section-title .bracket { color: var(--orange); } -.section-rule { flex: 1; height: 2px; background: linear-gradient(to right, var(--orange), transparent); border-radius: 99px; } - -/* Fade-in */ -.fade-up { opacity: 0; transform: translateY(28px); transition: opacity 0.6s var(--ease), transform 0.6s var(--ease); } -.fade-up.visible { opacity: 1; transform: none; } - -/* Pre-reveal state. script.js adds .fade-up at runtime, which is too late to stop - these from painting first, so hide them here instead. Selectors mirror the - queries in script.js — change both together. */ -.js .hero-name-static { visibility: hidden; } -.js .hero-role:not(.visible), -.js .hero-pronouns:not(.visible), -.js .hero-links:not(.visible), -.js .hero-visual:not(.visible), -.js .section-header:not(.visible), -.js .about-grid:not(.visible), -.js .skill-card:not(.visible), -.js .timeline-item:not(.visible), -.js .project-card:not(.visible), -.js .edu-card:not(.visible) { - opacity: 0; - transform: translateY(28px); -} - -/* Hero typewriter cursor */ -.hero-cursor { - display: inline-block; - width: 5px; - height: 0.8em; - background: var(--ink); - margin-left: 5px; - vertical-align: baseline; - border-radius: 1px; - animation: cursorBlink 0.7s step-end infinite; -} -@keyframes cursorBlink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } -} +.section-rule { flex: 1; height: 2px; background: linear-gradient(to right, var(--orange), transparent); border-radius: var(--radius-pill); } /* ── Hero ───────────────────────────────────────────────────── */ .hero { @@ -193,7 +153,6 @@ main { position: relative; z-index: 1; } .hero-content { flex: 1; } .hero-visual { flex: 0 0 300px; display: flex; align-items: center; justify-content: center; position: relative; } -.hero-greeting { font-family: var(--mono); font-size: 0.9rem; color: var(--orange); margin-bottom: 0.6rem; } .hero-name { font-size: clamp(3rem, 5.5vw, 5rem); font-weight: 900; @@ -205,32 +164,24 @@ main { position: relative; z-index: 1; } .hero-role { font-size: clamp(1rem, 2vw, 1.3rem); font-weight: 700; - color: var(--orange); - margin-bottom: 1rem; + color: var(--orange-deep); + margin-bottom: 0.5rem; letter-spacing: 0.06em; text-transform: uppercase; } -.hero-desc { - font-size: 1.05rem; - color: var(--ink-soft); - max-width: 440px; - margin-bottom: 2rem; - line-height: 1.8; -} - /* Hero social links */ .hero-links { display: flex; flex-wrap: wrap; - gap: 0.65rem; + gap: 0.75rem; } .hero-link { display: inline-flex; align-items: center; - gap: 0.45rem; + gap: 0.5rem; padding: 0.5rem 1rem; - border-radius: 999px; - font-size: 0.82rem; + border-radius: var(--radius-pill); + font-size: 0.85rem; font-weight: 700; letter-spacing: 0.03em; border: 1.5px solid var(--glass-border); @@ -245,7 +196,6 @@ main { position: relative; z-index: 1; } border-color: var(--orange); color: #fff; transform: translateY(-2px); - box-shadow: var(--glow-orange); } .hero-link:hover i { color: #fff; } @@ -277,40 +227,38 @@ main { position: relative; z-index: 1; } } .avatar-placeholder img { width: 100%; height: 100%; object-fit: cover; } +.avatar-ring.spinning { animation: avatarSpin 0.8s var(--ease); } +/* rotate is only keyed at 0%/100% so it sweeps through the scale midpoint + without easing to a stop at 180° */ +@keyframes avatarSpin { + 0% { rotate: 0deg; scale: 1; } + 50% { scale: 1.1; } + 100% { rotate: 360deg; scale: 1; } +} +@media (prefers-reduced-motion: reduce) { + .avatar-ring.spinning { animation: none; } +} -.scroll-hint { - position: absolute; bottom: 2rem; left: 2rem; - display: flex; align-items: center; gap: 0.65rem; - font-family: var(--mono); font-size: 0.68rem; - color: var(--orange); letter-spacing: 0.1em; text-transform: uppercase; -} -.scroll-line { width: 36px; height: 1px; background: var(--orange); position: relative; overflow: hidden; } -.scroll-line::after { - content: ''; position: absolute; top: 0; left: -100%; - width: 100%; height: 100%; background: var(--yellow); - animation: scanline 1.8s linear infinite; -} -@keyframes scanline { to { left: 200%; } } .hero-pronouns { - font-family: var(--mono); - font-size: 0.8rem; + font-size: 0.85rem; color: var(--ink-soft); - margin-bottom: 0.85rem; + margin-left: 0.5rem; + margin-bottom: 0.75rem; letter-spacing: 0.06em; } /* ── About ──────────────────────────────────────────────────── */ .about-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3.5rem; align-items: start; } .about-right { display: flex; flex-direction: column; gap: 1.25rem; } -.about-text p { font-size: 0.97rem; color: var(--ink-soft); margin-bottom: 1.1rem; line-height: 1.85; } +.about-text p { font-size: 0.95rem; color: var(--ink-soft); margin-bottom: 1rem; } .about-text strong { color: var(--ink); } .pepsi-card { background: var(--glass-bg); border: 1px solid var(--glass-border); backdrop-filter: blur(12px); - border-radius: 16px; + border-radius: var(--radius-lg); overflow: hidden; box-shadow: var(--card-shadow); text-align: center; @@ -326,22 +274,19 @@ main { position: relative; z-index: 1; } width: 100%; height: 100%; object-fit: cover; } .pepsi-caption { - padding: 0.6rem 1rem; - font-size: 0.78rem; + padding: 0.5rem 1rem; + font-size: 0.85rem; font-style: italic; color: var(--ink-soft); } /* ── Skills ─────────────────────────────────────────────────── */ -.skills-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; } .skill-card { background: var(--glass-bg); border: 1px solid var(--glass-border); - border-radius: 18px; padding: 1.75rem; + border-radius: var(--radius-lg); padding: 1.75rem; box-shadow: var(--card-shadow); - transition: transform 0.3s var(--ease), box-shadow 0.3s; } -.skill-card:hover { box-shadow: var(--glow-orange); } -.skill-card-icon { width: 42px; height: 42px; border-radius: 10px; background: var(--grad); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.05rem; margin-bottom: 0.85rem; } +.skill-card-icon { width: 42px; height: 42px; border-radius: var(--radius-md); background: var(--grad); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.1rem; margin-bottom: 0.75rem; } .skill-card h3 { font-size: 1.05rem; font-weight: 800; margin-bottom: 1.25rem; } /* ── Projects (list layout) ─────────────────────────────────── */ @@ -349,13 +294,10 @@ main { position: relative; z-index: 1; } .project-card { background: var(--glass-bg); border: 1px solid var(--glass-border); - border-radius: 20px; + border-radius: var(--radius-lg); padding: 2rem; box-shadow: var(--card-shadow); - transition: transform 0.3s var(--ease), box-shadow 0.3s; - will-change: transform; } -.project-card:hover { box-shadow: var(--glow-orange); } .project-header { display: flex; align-items: flex-start; gap: 1rem; @@ -363,7 +305,7 @@ main { position: relative; z-index: 1; } } .project-icon { width: 46px; height: 46px; flex-shrink: 0; - border-radius: 12px; background: var(--grad); + border-radius: var(--radius-md); background: var(--grad); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.1rem; } @@ -374,11 +316,11 @@ main { position: relative; z-index: 1; } } .project-title-row h3 { font-size: 1.2rem; font-weight: 800; } -.project-ext-links { display: flex; gap: 0.6rem; flex-wrap: wrap; } +.project-ext-links { display: flex; gap: 0.5rem; flex-wrap: wrap; } .project-link { - display: inline-flex; align-items: center; gap: 0.35rem; - font-size: 0.76rem; font-weight: 700; - padding: 0.35rem 0.8rem; border-radius: 999px; + display: inline-flex; align-items: center; gap: 0.5rem; + font-size: 0.75rem; font-weight: 700; + padding: 0.25rem 0.75rem; border-radius: var(--radius-pill); border: 1.5px solid var(--glass-border); background: rgba(255,255,255,0.4); color: var(--ink-soft); @@ -387,34 +329,34 @@ main { position: relative; z-index: 1; } .project-link:hover { background: var(--orange); border-color: var(--orange); color: #fff; } .project-badge { - display: inline-flex; align-items: center; gap: 0.35rem; - font-size: 0.72rem; font-weight: 700; - padding: 0.35rem 0.85rem; border-radius: 999px; + display: inline-flex; align-items: center; gap: 0.5rem; + font-size: 0.75rem; font-weight: 700; + padding: 0.25rem 0.75rem; border-radius: var(--radius-pill); background: rgba(244, 114, 43, 0.12); - color: var(--orange); + color: var(--orange-deep); border: 1px solid rgba(244, 114, 43, 0.25); } .project-body { margin-bottom: 1.25rem; } .project-body p { - font-size: 0.93rem; color: var(--ink-soft); line-height: 1.8; - margin-bottom: 0.85rem; + font-size: 0.95rem; color: var(--ink-soft); + margin-bottom: 0.75rem; } .project-body p:last-child { margin-bottom: 0; } -.project-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; } +.project-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; } .project-tags span, .timeline-tags span { - font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; - padding: 0.25rem 0.7rem; border-radius: 999px; + font-size: 0.75rem; font-weight: 700; letter-spacing: 0.04em; + padding: 0.25rem 0.75rem; border-radius: var(--radius-pill); background: rgba(26,10,0,0.07); color: var(--ink-soft); border: 1px solid rgba(244, 114, 43, 0.18); transition: all 0.2s var(--ease); } -.project-tags span:hover, .timeline-tags span:hover { +.project-tags span:hover, .timeline-tags span:hover, +.project-tags span.tip-open, .timeline-tags span.tip-open { background: var(--orange); border-color: var(--orange); color: #fff; - box-shadow: var(--glow-orange); } /* ── Timeline / Experience ──────────────────────────────────── */ @@ -422,7 +364,7 @@ main { position: relative; z-index: 1; } .timeline::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 2px; background: linear-gradient(to bottom, var(--orange), var(--yellow), transparent); - border-radius: 99px; + border-radius: var(--radius-pill); } .timeline-item { position: relative; padding-left: 2.25rem; padding-bottom: 3rem; } .timeline-item:last-child { padding-bottom: 0; } @@ -430,63 +372,48 @@ main { position: relative; z-index: 1; } position: absolute; left: -2.16rem; top: 0rem; width: 16px; height: 16px; border-radius: 50%; background: var(--orange); border: 3px solid var(--cream); - box-shadow: var(--glow-orange); } .timeline-card { background: var(--glass-bg); border: 1px solid var(--glass-border); - border-radius: 18px; padding: 1.75rem; + border-radius: var(--radius-lg); padding: 1.75rem; box-shadow: var(--card-shadow); - transition: transform 0.3s var(--ease), box-shadow 0.3s; } -.timeline-card:hover { box-shadow: var(--glow-orange); } -.timeline-meta { display: flex; align-items: center; gap: 0.65rem; margin-bottom: 0.6rem; } -.timeline-date { font-family: var(--mono); font-size: 0.75rem; color: var(--orange); } -.timeline-tag { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; background: rgba(244,114,43,0.12); color: var(--orange); border-radius: 999px; padding: 0.18rem 0.6rem; } -.timeline-role { font-size: 1.25rem; font-weight: 900; letter-spacing: -0.01em; margin-bottom: 0.2rem; } -.timeline-company { font-size: 0.88rem; color: var(--ink-soft); margin-bottom: 0.85rem; } -.timeline-desc { font-size: 0.9rem; color: var(--ink-soft); line-height: 1.75; margin-bottom: 1rem; } -.timeline-achievements { - display: flex; flex-direction: column; gap: 0.45rem; - margin-bottom: 1rem; padding: 0.85rem 1.1rem; - background: rgba(244,114,43,0.06); border-radius: 10px; border-left: 3px solid var(--orange); -} -.timeline-achievements li { font-size: 0.85rem; color: var(--ink-soft); display: flex; align-items: center; gap: 0.55rem; } -.timeline-achievements i { color: var(--orange); font-size: 0.6rem; flex-shrink: 0; } -.timeline-tags { display: flex; flex-wrap: wrap; gap: 0.4rem; } +.timeline-meta { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.5rem; } +.timeline-date { font-size: 0.75rem; font-weight: 700; color: var(--orange-deep); } +.timeline-tag { font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; background: rgba(244,114,43,0.12); color: var(--orange-deep); border-radius: var(--radius-pill); padding: 0.25rem 0.75rem; } +.timeline-role { font-size: 1.2rem; font-weight: 900; letter-spacing: -0.01em; margin-bottom: 0.25rem; } +.timeline-company { font-size: 0.85rem; color: var(--ink-soft); margin-bottom: 0.75rem; } +.timeline-desc { font-size: 0.95rem; color: var(--ink-soft); margin-bottom: 1rem; } +.timeline-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; } /* ── Education ──────────────────────────────────────────────── */ .education-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.5rem; } .edu-card { background: var(--glass-bg); border: 1px solid var(--glass-border); - border-radius: 18px; padding: 1.75rem; + border-radius: var(--radius-lg); padding: 1.75rem; box-shadow: var(--card-shadow); display: flex; gap: 1.25rem; align-items: flex-start; - transition: transform 0.3s var(--ease), box-shadow 0.3s; } -.edu-card:hover { box-shadow: var(--glow-orange); } -.edu-icon { width: 46px; height: 46px; flex-shrink: 0; border-radius: 12px; background: var(--grad); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.15rem; } +.edu-icon { width: 46px; height: 46px; flex-shrink: 0; border-radius: var(--radius-md); background: var(--grad); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.1rem; } .edu-body h3 { font-size: 1.05rem; font-weight: 800; margin-bottom: 0.25rem; } -.edu-school { font-weight: 700; color: var(--orange); font-size: 0.85rem; margin-bottom: 0.2rem; } -.edu-date { font-family: var(--mono); font-size: 0.75rem; color: var(--ink-soft); margin-bottom: 0.65rem; } -.edu-desc { font-size: 0.85rem; color: var(--ink-soft); line-height: 1.7; } -.edu-links { margin-top: 0.85rem; display: flex; flex-wrap: wrap; gap: 0.5rem; } +.edu-school { font-weight: 700; color: var(--orange-deep); font-size: 0.85rem; margin-bottom: 0.25rem; } +.edu-date { font-size: 0.75rem; font-weight: 700; color: var(--ink-soft); margin-bottom: 0.75rem; } +.edu-desc { font-size: 0.85rem; color: var(--ink-soft); } +.edu-links { margin-top: 0.75rem; display: flex; flex-wrap: wrap; gap: 0.5rem; } /* ── Footer ─────────────────────────────────────────────────── */ footer { position: relative; z-index: 1; background: var(--ink); color: rgba(255,255,255,0.45); - text-align: center; padding: 1.75rem 2rem; font-size: 0.8rem; + text-align: center; padding: 1.75rem 2rem; font-size: 0.85rem; } /* ── Responsive ─────────────────────────────────────────────── */ @media (max-width: 960px) { - .skills-grid { grid-template-columns: repeat(2, 1fr); } .about-grid { grid-template-columns: 1fr; gap: 2rem; } .hero-visual { display: none; } .hero { flex-direction: column; text-align: center; padding-top: calc(var(--nav-h) + 2rem); } - .hero-desc { max-width: 100%; } .hero-links { justify-content: center; } - .scroll-hint { display: none; } } @media (max-width: 768px) { @@ -511,7 +438,6 @@ footer { transform: none; } .burger { display: flex; } - .skills-grid { grid-template-columns: 1fr; } .education-grid { grid-template-columns: 1fr; } .project-title-row { flex-direction: column; align-items: flex-start; } } @@ -525,9 +451,8 @@ footer { } .skills-intro { - font-size: 1rem; + font-size: 1.05rem; color: var(--ink-soft); - line-height: 1.8; max-width: 680px; margin-bottom: 2rem; } @@ -540,11 +465,10 @@ footer { } .lang-tag { - font-family: var(--mono); font-size: 0.95rem; - font-weight: 500; + font-weight: 700; padding: 0.5rem 1.25rem; - border-radius: 999px; + border-radius: var(--radius-pill); background: var(--glass-bg); border: 1.5px solid var(--glass-border); backdrop-filter: blur(8px); @@ -552,11 +476,11 @@ footer { transition: all 0.2s var(--ease); } -.lang-tag:hover { +.lang-tag:hover, +.lang-tag.tip-open { background: var(--orange); border-color: var(--orange); color: #fff; - box-shadow: var(--glow-orange); } /* ── Skill pills: shared behavior (no select + tooltip anchor) ─── */ @@ -567,9 +491,10 @@ footer { -webkit-user-select: none; } -.lang-tag[data-tip]:not([data-tip=""]):hover::after, -.project-tags span[data-tip]:not([data-tip=""]):hover::after, -.timeline-tags span[data-tip]:not([data-tip=""]):hover::after { +/* Tooltip: shows on hover, keyboard focus, or tap (script.js toggles .tip-open) */ +[data-tip]:not([data-tip=""]):hover::after, +[data-tip]:not([data-tip=""]):focus-visible::after, +[data-tip]:not([data-tip=""]).tip-open::after { content: attr(data-tip); position: absolute; bottom: calc(100% + 10px); @@ -577,12 +502,12 @@ footer { transform: translateX(-50%); width: max-content; max-width: 220px; - padding: 0.5rem 0.7rem; - border-radius: 10px; + padding: 0.5rem 0.75rem; + border-radius: var(--radius-md); background: var(--ink); color: var(--cream); font-family: var(--font); - font-size: 0.72rem; + font-size: 0.75rem; font-weight: 500; line-height: 1.4; letter-spacing: normal; @@ -595,9 +520,9 @@ footer { } /* Tooltip arrow */ -.lang-tag[data-tip]:not([data-tip=""]):hover::before, -.project-tags span[data-tip]:not([data-tip=""]):hover::before, -.timeline-tags span[data-tip]:not([data-tip=""]):hover::before { +[data-tip]:not([data-tip=""]):hover::before, +[data-tip]:not([data-tip=""]):focus-visible::before, +[data-tip]:not([data-tip=""]).tip-open::before { content: ''; position: absolute; bottom: calc(100% - 3px); @@ -624,6 +549,6 @@ footer { font-size: 0.88em; background: rgba(26, 10, 0, 0.07); padding: 0.1em 0.4em; - border-radius: 4px; + border-radius: var(--radius-sm); color: var(--orange); } \ No newline at end of file