diff --git a/404.html b/404.html index 48915d2..47fc01b 100644 --- a/404.html +++ b/404.html @@ -3,7 +3,7 @@ - + @@ -15,8 +15,8 @@ 404 | ivanch - - + + @@ -45,7 +45,7 @@ - + - \ No newline at end of file + diff --git a/css/main.css b/css/main.css index 6498e31..50e8979 100644 --- a/css/main.css +++ b/css/main.css @@ -16,7 +16,6 @@ body { font-family: var(--font-family); background: var(--background-color); min-height: 100vh; - overflow-x: hidden; position: relative; color: var(--text-color); -webkit-font-smoothing: antialiased; @@ -65,13 +64,15 @@ img { } .main-container { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + --page-gutter: var(--space-lg); + --page-top: var(--space-xl); + position: relative; + margin: 0 auto; max-width: 1200px; width: 100%; - padding: 0 var(--space-lg); + min-height: 100vh; + min-height: 100svh; + padding: var(--page-top) var(--page-gutter) var(--space-xl); display: flex; align-items: center; justify-content: center; @@ -114,8 +115,8 @@ img { padding: var(--space-2xl) var(--space-xl); transition: background var(--dur-med) var(--ease-out-soft), box-shadow var(--dur-med) var(--ease-out-soft); - min-width: 26rem; - max-width: 26rem; + min-width: 0; + width: 100%; } /* ------------------------------------------------------------------ */ @@ -158,45 +159,36 @@ img { /* ------------------------------------------------------------------ */ /* Mobile responsive layout */ -/* Single breakpoint switches the centered card to a stacked column. */ -/* Width constraints (set via tokens in section CSS) are overridden */ -/* in one place here rather than repeated across multiple sections. */ +/* Keep the page in normal flow so short screens can scroll fully. */ /* ------------------------------------------------------------------ */ @media (max-width: 1024px) { .main-container { - position: relative; - top: auto; - left: auto; - transform: none; - padding: var(--space-xl) var(--space-md); - min-height: 100vh; - display: flex; + --page-gutter: var(--space-md); align-items: flex-start; } - .content, - .homelab-section, - .projects-section { - min-width: unset; - max-width: unset; - width: 100%; - } - .content { padding: var(--space-xl) var(--space-lg); } } @media (max-width: 480px) { - body { - font-size: 14px; - } - .main-container { - padding: var(--space-xl) var(--space-sm); + --page-gutter: var(--space-xs); + --page-top: var(--space-md); } .content { padding: var(--space-lg) var(--space-md); } } + +/* Leave room for notches and the home indicator in landscape, too. */ +@supports (padding: max(0px, env(safe-area-inset-left))) { + .main-container { + padding-top: max(var(--page-top), env(safe-area-inset-top)); + padding-left: max(var(--page-gutter), env(safe-area-inset-left)); + padding-right: max(var(--page-gutter), env(safe-area-inset-right)); + padding-bottom: max(var(--space-xl), env(safe-area-inset-bottom)); + } +} diff --git a/css/matrix.css b/css/matrix.css index 8d3264e..bff6f27 100644 --- a/css/matrix.css +++ b/css/matrix.css @@ -14,8 +14,36 @@ filter: brightness(0.7) saturate(1.05); } -@media (prefers-reduced-motion: reduce) { - .matrix-canvas { - filter: brightness(0.6) saturate(0.9); +/* CSS only on phones/tablets and for reduced motion: no canvas work. */ +/* Keep this query in sync with js/flowfield.js. */ +@media (max-width: 1024px), (pointer: coarse), (prefers-reduced-motion: reduce) { + body { + background: + radial-gradient(ellipse at 0% 0%, rgba(79, 195, 247, 0.18), transparent 55%), + radial-gradient(ellipse at 100% 65%, rgba(118, 75, 162, 0.22), transparent 60%), + linear-gradient(150deg, #0b1220, #100c1c); + } + + .matrix-canvas, + .avatar .glitch-layer { + display: none; + } + + .main-container .glass, + .avatar::after, + .homelab-background, + .homelab-background::after, + .homelab-title { + animation: none; + } + + .main-container .glass { + will-change: auto; + } + + /* Avoid blur compositing work while scrolling. */ + body .main-container :is(.glass, .content, .projects-section, .homelab-section, .btn) { + backdrop-filter: none; + -webkit-backdrop-filter: none; } } diff --git a/css/sections/hero.css b/css/sections/hero.css index 8647ef0..740909f 100644 --- a/css/sections/hero.css +++ b/css/sections/hero.css @@ -5,17 +5,18 @@ padding: var(--space-2xl); width: 100%; display: grid; - grid-template-columns: 200px 1fr; + grid-template-columns: 180px minmax(0, 1fr); gap: var(--space-xl); align-items: center; animation: fadeInUp 0.8s var(--ease-out-expo); } .hero-details { - display: flex; - flex-direction: row; + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); gap: var(--space-sm); - align-items: flex-start; + align-items: stretch; + min-width: 0; width: 100%; justify-content: flex-start; } @@ -153,7 +154,6 @@ /* ------------------------------------------------------------------ */ @media (max-width: 1024px) { .hero-details { - flex-direction: column; gap: var(--space-xl); } @@ -166,6 +166,10 @@ width: 100%; align-items: stretch; } + + .tech-tags { + justify-content: center; + } } @media (max-width: 768px) { @@ -176,8 +180,13 @@ text-align: center; } + .hero-details { + grid-template-columns: minmax(0, 1fr); + gap: var(--space-lg); + } + .name { - font-size: var(--text-xl); + font-size: var(--text-2xl); } .social-links { @@ -191,35 +200,24 @@ padding: var(--space-lg) var(--space-md); } - .name { - font-size: var(--text-lg); - } - .social-links { - gap: var(--space-xs); + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--space-sm); } .social-link { padding: var(--space-sm) var(--space-md); - font-size: var(--text-xs); + font-size: var(--text-sm); min-width: 0; - flex: 1; - max-width: calc(50% - 6px); justify-content: center; } - .social-link span { - display: none; + .social-link:last-child { + grid-column: 1 / -1; } .social-link i { font-size: 16px; } } - -@media (max-width: 380px) { - .social-link { - max-width: calc(100% / 2 - 6px); - padding: var(--space-sm); - } -} diff --git a/css/sections/homelab.css b/css/sections/homelab.css index 1e6a7d0..3a766d1 100644 --- a/css/sections/homelab.css +++ b/css/sections/homelab.css @@ -1,5 +1,4 @@ .homelab-section { - margin-top: var(--space-md); padding: var(--space-xl) var(--space-lg); background: rgba(20, 20, 30, 0.10); backdrop-filter: blur(48px); @@ -7,8 +6,8 @@ border-radius: var(--border-radius-large); box-shadow: var(--shadow-md), var(--inset-shadow); border: 1px solid var(--glass-border); - min-width: 26rem; - max-width: 26rem; + min-width: 0; + width: 100%; align-self: stretch; display: flex; flex-direction: column; @@ -220,12 +219,11 @@ .homelab-buttons { gap: var(--space-sm); flex-direction: column; - align-items: center; + align-items: stretch; } .homelab-btn { width: 100%; - max-width: 200px; justify-content: center; } } diff --git a/css/sections/profile.css b/css/sections/profile.css index 117c997..2ff3558 100644 --- a/css/sections/profile.css +++ b/css/sections/profile.css @@ -262,6 +262,26 @@ /* ------------------------------------------------------------------ */ /* Responsive */ /* ------------------------------------------------------------------ */ +@media (min-width: 769px) and (max-width: 1024px) { + .profile-container { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-xl); + } + + .profile-container .profile-info { + margin-top: 0; + align-items: flex-start; + text-align: left; + } + + .profile-container .avatar { + margin: 0; + flex-shrink: 0; + } +} + @media (max-width: 1024px) { .profile-info { align-items: center; @@ -304,7 +324,7 @@ } .profile-info-item { - font-size: var(--text-xs); + font-size: var(--text-sm); flex-direction: row; gap: var(--space-xs); } @@ -327,6 +347,6 @@ } .profile-info-item { - font-size: var(--text-xs); + font-size: var(--text-sm); } -} \ No newline at end of file +} diff --git a/css/sections/projects.css b/css/sections/projects.css index 53f4a2b..bf8b994 100644 --- a/css/sections/projects.css +++ b/css/sections/projects.css @@ -1,5 +1,4 @@ .projects-section { - flex: 0 0 340px; display: flex; flex-direction: column; gap: var(--space-md); @@ -10,8 +9,8 @@ border-radius: var(--border-radius-large); box-shadow: var(--shadow-md), var(--inset-shadow); border-bottom: 1px solid var(--glass-border); - min-width: 380px; - max-width: 440px; + min-width: 0; + width: 100%; align-self: stretch; } @@ -112,13 +111,11 @@ flex-wrap: nowrap; } -/* Keep long titles from pushing icons to a new line */ +/* Let titles wrap while keeping the action links available. */ .project-title > span:first-child { flex: 1 1 auto; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; + overflow-wrap: anywhere; } .project-icon-link { @@ -150,12 +147,16 @@ /* Responsive */ /* ------------------------------------------------------------------ */ @media (max-width: 1024px) { - .projects-section { - flex: unset; - max-width: 100%; - margin-left: 0; - margin-top: var(--space-xl); - min-width: unset; + .project-icon-link { + width: 44px; + height: 44px; + } +} + +@media (pointer: coarse) { + .project-icon-link { + width: 44px; + height: 44px; } } @@ -179,7 +180,7 @@ } .projects-title { - font-size: var(--text-sm); + font-size: var(--text-md); } .project { @@ -191,7 +192,7 @@ } .project-desc { - font-size: var(--text-xs); + font-size: var(--text-sm); } .project-icons { @@ -205,7 +206,7 @@ } .project-title { - font-size: var(--text-xs); + font-size: var(--text-sm); flex-direction: row; align-items: center; gap: var(--space-xs); diff --git a/index.html b/index.html index f47cb40..320a66b 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + @@ -14,14 +14,14 @@ ivanch - + - - - - + + + + - + @@ -149,7 +149,7 @@ - + - \ No newline at end of file + diff --git a/js/flowfield.js b/js/flowfield.js index 05b6754..44f4f3d 100644 --- a/js/flowfield.js +++ b/js/flowfield.js @@ -116,7 +116,8 @@ class FlowFieldBackground { this.sparks = []; this.mouse = { x: 0, y: 0, active: false }; this.rafId = null; - this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + this.resizeRaf = null; + this.events = new AbortController(); this.width = 0; this.height = 0; @@ -157,47 +158,44 @@ class FlowFieldBackground { this.bindEvents(); this.handleResize(); - if (this.prefersReducedMotion) { - // Single long-exposure render; no animation loop at all. - this.warmup(180); - return; - } - // Pre-run a short exposure so the first paint is already textured. this.warmup(60); this.animate(); } bindEvents() { + const signal = this.events.signal; // The canvas has pointer-events: none, so listen on window. window.addEventListener('pointermove', (e) => { this.mouse.x = e.clientX; this.mouse.y = e.clientY; this.mouse.active = true; - }, { passive: true }); + }, { passive: true, signal }); window.addEventListener('pointerdown', (e) => { this.spawnClickBurst(e.clientX, e.clientY); - }, { passive: true }); + }, { passive: true, signal }); window.addEventListener('pointerout', () => { this.mouse.active = false; - }); + }, { signal }); // Pause the rAF loop when the tab is hidden to save CPU / battery. document.addEventListener('visibilitychange', () => { if (document.hidden) { this.stop(); - } else if (!this.prefersReducedMotion) { + } else { this.start(); } - }); + }, { signal }); - let resizeRaf = null; window.addEventListener('resize', () => { - if (resizeRaf) cancelAnimationFrame(resizeRaf); - resizeRaf = requestAnimationFrame(() => this.handleResize()); - }); + if (this.resizeRaf) cancelAnimationFrame(this.resizeRaf); + this.resizeRaf = requestAnimationFrame(() => { + this.resizeRaf = null; + this.handleResize(); + }); + }, { signal }); } start() { @@ -212,6 +210,17 @@ class FlowFieldBackground { } } + destroy() { + this.stop(); + this.events.abort(); + if (this.resizeRaf) cancelAnimationFrame(this.resizeRaf); + this.particles = []; + this.sparks = []; + // Release the high-DPI drawing buffer when switching to CSS only. + this.canvas.width = 1; + this.canvas.height = 1; + } + handleResize() { const w = window.innerWidth; const h = window.innerHeight; @@ -504,5 +513,22 @@ class FlowFieldBackground { } document.addEventListener('DOMContentLoaded', () => { - new FlowFieldBackground(); + // Keep this query in sync with the static backdrop in css/matrix.css. + const staticBackground = window.matchMedia( + '(max-width: 1024px), (pointer: coarse), (prefers-reduced-motion: reduce)' + ); + let background = null; + + function updateBackground() { + if (staticBackground.matches) { + if (background) background.destroy(); + background = null; + } else if (!background) { + background = new FlowFieldBackground(); + if (document.hidden) background.stop(); + } + } + + staticBackground.addEventListener('change', updateBackground); + updateBackground(); });