- Full-stack developer with a soft spot for clean architecture,
- dark mode interfaces, and coffee that's slightly too strong.
+ A short bio or description about yourself. What do you do, what do
+ you care about, and what makes you interesting?
diff --git a/src/components/Hero/Hero.module.scss b/src/components/Hero/Hero.module.scss
index 176488e..cd20b60 100644
--- a/src/components/Hero/Hero.module.scss
+++ b/src/components/Hero/Hero.module.scss
@@ -14,8 +14,8 @@
position: absolute;
inset: 0;
background-image:
- linear-gradient(rgba($color-primary, 0.08) 1px, transparent 1px),
- linear-gradient(90deg, rgba($color-primary, 0.08) 1px, transparent 1px);
+ linear-gradient(rgb(var(--color-primary) / 0.08) 1px, transparent 1px),
+ linear-gradient(90deg, rgb(var(--color-primary) / 0.08) 1px, transparent 1px);
background-size: 40px 40px;
mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, black 40%, transparent 100%);
pointer-events: none;
@@ -30,7 +30,7 @@
transform: translateX(-50%);
width: 700px;
height: 500px;
- background: radial-gradient(ellipse, rgba($color-primary-mid, 0.18) 0%, transparent 70%);
+ background: radial-gradient(ellipse, rgb(var(--color-primary-mid) / 0.18) 0%, transparent 70%);
pointer-events: none;
}
@@ -110,7 +110,7 @@
&:hover {
background: $color-primary-glow;
color: #fff;
- box-shadow: 0 0 24px rgba($color-primary-glow, 0.4);
+ box-shadow: 0 0 24px rgb(var(--color-primary-glow) / 0.4);
transform: translateY(-2px);
}
}
@@ -133,7 +133,7 @@
position: absolute;
bottom: $space-8;
right: $space-6;
- background: rgba($color-bg-card, 0.9);
+ background: rgb(var(--color-bg-card) / 0.9);
border: 1px solid rgba(34, 197, 94, 0.35);
border-radius: 100px;
padding: $space-2 $space-4;
diff --git a/src/components/Navbar/Navbar.jsx b/src/components/Navbar/Navbar.jsx
index 47dd5e3..a7d4ee3 100644
--- a/src/components/Navbar/Navbar.jsx
+++ b/src/components/Navbar/Navbar.jsx
@@ -1,36 +1,48 @@
import { useAtom } from 'jotai';
-import { navOpenState } from '../../state/atoms';
+import { navOpenState, activeSectionState } from '../../state/atoms';
+import ThemeToggle from '../ThemeToggle/ThemeToggle';
import styles from './Navbar.module.scss';
const NAV_LINKS = [
- { label: 'Home', href: '#home' },
- { label: 'About', href: '#about' },
- { label: 'Projects', href: '#projects' },
- { label: 'Contact', href: '#contact' },
+ { label: 'Home', section: 'home' },
+ { label: 'About', section: 'about' },
+ { label: 'Projects', section: 'projects' },
+ { label: 'Contact', section: 'contact' },
];
export default function Navbar() {
const [isOpen, setIsOpen] = useAtom(navOpenState);
+ const [activeSection, setActiveSection] = useAtom(activeSectionState);
+
+ function handleNav(section) {
+ setActiveSection(section);
+ setIsOpen(false);
+ }
return (
-
+ handleNav('home')}>
<
vk
/>
-
+
+
+
setIsOpen((v) => !v)}
diff --git a/src/components/Navbar/Navbar.module.scss b/src/components/Navbar/Navbar.module.scss
index 946eac5..09cc430 100644
--- a/src/components/Navbar/Navbar.module.scss
+++ b/src/components/Navbar/Navbar.module.scss
@@ -4,7 +4,7 @@
position: fixed;
inset: 0 0 auto 0;
z-index: 100;
- background: rgba($color-bg, 0.85);
+ background: rgb(var(--color-bg) / 0.85);
backdrop-filter: blur(14px);
border-bottom: 1px solid $color-border;
transition: background $transition-base;
@@ -25,6 +25,10 @@
font-weight: 700;
color: $color-text;
letter-spacing: -0.02em;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
&:hover {
color: $color-text;
@@ -40,13 +44,18 @@
display: flex;
gap: $space-8;
- a {
+ button {
+ font-family: inherit;
font-size: $text-sm;
font-weight: 500;
color: $color-text-muted;
letter-spacing: 0.04em;
text-transform: uppercase;
position: relative;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
transition: color $transition-fast;
&::after {
@@ -67,6 +76,14 @@
transform: scaleX(1);
}
}
+
+ &.active {
+ color: $color-text;
+
+ &::after {
+ transform: scaleX(1);
+ }
+ }
}
}
diff --git a/src/components/Projects/Projects.jsx b/src/components/Projects/Projects.jsx
index 1609db0..a2fa3a6 100644
--- a/src/components/Projects/Projects.jsx
+++ b/src/components/Projects/Projects.jsx
@@ -2,23 +2,23 @@ import styles from './Projects.module.scss';
const PROJECTS = [
{
- name: 'Lumina',
- desc: 'Real-time collaborative whiteboard built with WebSockets and Canvas API. Because sometimes Figma is overkill.',
- tags: ['React', 'Node.js', 'WebSockets'],
+ name: 'Project Alpha',
+ desc: 'A short description of your featured project. What problem does it solve and what technologies make it interesting?',
+ tags: ['Tag 1', 'Tag 2', 'Tag 3'],
href: '#',
star: true,
},
{
- name: 'Hallberg',
- desc: 'Personal finance tracker that actually respects your privacy — no cloud, no telemetry, just SQLite and honesty.',
- tags: ['Go', 'SQLite', 'HTMX'],
+ name: 'Project Beta',
+ desc: 'A short description of your second project. Highlight the key challenge or the most exciting part of building it.',
+ tags: ['Tag 1', 'Tag 2', 'Tag 3'],
href: '#',
star: false,
},
{
- name: 'Pikkubot',
- desc: 'Tiny Telegram bot for generating daily Finnish vocabulary cards. My Finnish is still terrible, but at least it\'s automated.',
- tags: ['Python', 'Telegram API'],
+ name: 'Project Gamma',
+ desc: 'A short description of your third project. What did you learn, or what are you most proud of about it?',
+ tags: ['Tag 1', 'Tag 2'],
href: '#',
star: false,
},
diff --git a/src/components/Projects/Projects.module.scss b/src/components/Projects/Projects.module.scss
index 3243d4c..4e99c55 100644
--- a/src/components/Projects/Projects.module.scss
+++ b/src/components/Projects/Projects.module.scss
@@ -57,12 +57,12 @@
&:hover {
transform: translateY(-4px);
- border-color: rgba($color-primary-glow, 0.4);
- box-shadow: 0 8px 32px rgba($color-primary, 0.2);
+ border-color: rgb(var(--color-primary-glow) / 0.4);
+ box-shadow: 0 8px 32px rgb(var(--color-primary) / 0.2);
}
&.featured {
- border-color: rgba($color-accent, 0.25);
+ border-color: rgb(var(--color-accent) / 0.25);
}
}
@@ -73,8 +73,8 @@
font-size: $text-xs;
font-family: $font-mono;
color: $color-accent-warm;
- background: rgba($color-accent-warm, 0.1);
- border: 1px solid rgba($color-accent-warm, 0.25);
+ background: rgb(var(--color-accent-warm) / 0.1);
+ border: 1px solid rgb(var(--color-accent-warm) / 0.25);
border-radius: 100px;
padding: 2px $space-3;
}
@@ -126,7 +126,7 @@
font-family: $font-mono;
font-size: $text-xs;
color: $color-text-dim;
- background: rgba($color-primary, 0.15);
+ background: rgb(var(--color-primary) / 0.15);
border-radius: $radius-sm;
padding: 2px $space-3;
}
diff --git a/src/components/ThemeToggle/ThemeToggle.jsx b/src/components/ThemeToggle/ThemeToggle.jsx
new file mode 100644
index 0000000..b4cc0a5
--- /dev/null
+++ b/src/components/ThemeToggle/ThemeToggle.jsx
@@ -0,0 +1,42 @@
+import { useAtom } from 'jotai';
+import { themeState } from '../../state/atoms';
+import styles from './ThemeToggle.module.scss';
+
+export default function ThemeToggle() {
+ const [theme, setTheme] = useAtom(themeState);
+ const isDark = theme === 'dark';
+
+ return (
+ setTheme(isDark ? 'light' : 'dark')}
+ aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
+ >
+ {isDark ? : }
+
+ );
+}
+
+function SunIcon() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function MoonIcon() {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/ThemeToggle/ThemeToggle.module.scss b/src/components/ThemeToggle/ThemeToggle.module.scss
new file mode 100644
index 0000000..9eaa160
--- /dev/null
+++ b/src/components/ThemeToggle/ThemeToggle.module.scss
@@ -0,0 +1,19 @@
+@use '../../styles/variables' as *;
+
+.toggle {
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: $color-text-muted;
+ padding: $space-2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: $radius-sm;
+ transition: color $transition-fast, background-color $transition-fast;
+
+ &:hover {
+ color: $color-text;
+ background-color: $color-bg-elevated;
+ }
+}
diff --git a/src/state/atoms.js b/src/state/atoms.js
index d07716c..21f96d1 100644
--- a/src/state/atoms.js
+++ b/src/state/atoms.js
@@ -3,3 +3,5 @@ import { atom } from 'jotai';
export const navOpenState = atom(false);
export const activeSectionState = atom('home');
+
+export const themeState = atom('dark');
diff --git a/src/styles/_global.scss b/src/styles/_global.scss
index ae2727c..7c9f632 100644
--- a/src/styles/_global.scss
+++ b/src/styles/_global.scss
@@ -1,5 +1,36 @@
@use 'variables' as *;
+// ── Theme tokens ───────────────────────────────────────────────
+:root {
+ --color-bg: 10 14 26;
+ --color-bg-elevated: 17 24 39;
+ --color-bg-card: 19 29 53;
+ --color-border: 30 45 77;
+ --color-primary: 30 58 138;
+ --color-primary-mid: 37 99 235;
+ --color-primary-glow: 59 130 246;
+ --color-accent: 96 165 250;
+ --color-accent-warm: 245 158 11;
+ --color-text: 226 232 240;
+ --color-text-muted: 148 163 184;
+ --color-text-dim: 71 85 105;
+}
+
+[data-theme='light'] {
+ --color-bg: 248 250 252;
+ --color-bg-elevated: 241 245 249;
+ --color-bg-card: 255 255 255;
+ --color-border: 203 213 225;
+ --color-primary: 30 58 138;
+ --color-primary-mid: 37 99 235;
+ --color-primary-glow: 59 130 246;
+ --color-accent: 37 99 235;
+ --color-accent-warm: 217 119 6;
+ --color-text: 15 23 42;
+ --color-text-muted: 71 85 105;
+ --color-text-dim: 148 163 184;
+}
+
// ── Reset & base ───────────────────────────────────────────────
*,
*::before,
@@ -21,6 +52,7 @@ body {
line-height: 1.6;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
+ transition: background-color $transition-base, color $transition-base;
}
a {
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index e5ea786..6c40846 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -1,18 +1,18 @@
// ── Palette ────────────────────────────────────────────────────
-$color-bg: #0a0e1a;
-$color-bg-elevated: #111827;
-$color-bg-card: #131d35;
-$color-border: #1e2d4d;
+$color-bg: rgb(var(--color-bg));
+$color-bg-elevated: rgb(var(--color-bg-elevated));
+$color-bg-card: rgb(var(--color-bg-card));
+$color-border: rgb(var(--color-border));
-$color-primary: #1e3a8a; // dark blue
-$color-primary-mid: #2563eb;
-$color-primary-glow:#3b82f6;
-$color-accent: #60a5fa; // light blue for highlights
-$color-accent-warm: #f59e0b; // amber – a touch of personality
+$color-primary: rgb(var(--color-primary));
+$color-primary-mid: rgb(var(--color-primary-mid));
+$color-primary-glow:rgb(var(--color-primary-glow));
+$color-accent: rgb(var(--color-accent));
+$color-accent-warm: rgb(var(--color-accent-warm));
-$color-text: #e2e8f0;
-$color-text-muted: #94a3b8;
-$color-text-dim: #475569;
+$color-text: rgb(var(--color-text));
+$color-text-muted: rgb(var(--color-text-muted));
+$color-text-dim: rgb(var(--color-text-dim));
// ── Typography ─────────────────────────────────────────────────
$font-sans: 'Inter', 'Segoe UI', system-ui, sans-serif;