making it mobile friendly
Homepage Build and Deploy (Test) / Build Homepage Image (push) Successful in 42s
Homepage Build and Deploy (Test) / Deploy Homepage (push) Successful in 5s

This commit is contained in:
ivanch committed 2026-10-11 16:48:40 -03:00
1 parent 6a93e9ae8d
commit e205172fce
9 files changed
+175 -112

No files matched your search

+5 -5
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="The requested page could not be found."> <meta name="description" content="The requested page could not be found.">
<meta name="author" content="ivanch"> <meta name="author" content="ivanch">
@@ -15,8 +15,8 @@
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon"> <link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
<title>404 | ivanch</title> <title>404 | ivanch</title>
<link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="css/main.css?v=20261011-2">
<link rel="stylesheet" href="css/matrix.css"> <link rel="stylesheet" href="css/matrix.css?v=20261011-2">
<link rel="stylesheet" href="css/button.css"> <link rel="stylesheet" href="css/button.css">
<link rel="stylesheet" href="css/sections/error.css"> <link rel="stylesheet" href="css/sections/error.css">
@@ -45,7 +45,7 @@
</section> </section>
</main> </main>
<script src="js/flowfield.js"></script> <script src="js/flowfield.js?v=20261011-2"></script>
</body> </body>
</html> </html>
+23 -31
View File
@@ -16,7 +16,6 @@ body {
font-family: var(--font-family); font-family: var(--font-family);
background: var(--background-color); background: var(--background-color);
min-height: 100vh; min-height: 100vh;
overflow-x: hidden;
position: relative; position: relative;
color: var(--text-color); color: var(--text-color);
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
@@ -65,13 +64,15 @@ img {
} }
.main-container { .main-container {
position: absolute; --page-gutter: var(--space-lg);
top: 50%; --page-top: var(--space-xl);
left: 50%; position: relative;
transform: translate(-50%, -50%); margin: 0 auto;
max-width: 1200px; max-width: 1200px;
width: 100%; 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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -114,8 +115,8 @@ img {
padding: var(--space-2xl) var(--space-xl); padding: var(--space-2xl) var(--space-xl);
transition: background var(--dur-med) var(--ease-out-soft), transition: background var(--dur-med) var(--ease-out-soft),
box-shadow var(--dur-med) var(--ease-out-soft); box-shadow var(--dur-med) var(--ease-out-soft);
min-width: 26rem; min-width: 0;
max-width: 26rem; width: 100%;
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -158,45 +159,36 @@ img {
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Mobile responsive layout */ /* Mobile responsive layout */
/* Single breakpoint switches the centered card to a stacked column. */ /* Keep the page in normal flow so short screens can scroll fully. */
/* Width constraints (set via tokens in section CSS) are overridden */
/* in one place here rather than repeated across multiple sections. */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.main-container { .main-container {
position: relative; --page-gutter: var(--space-md);
top: auto;
left: auto;
transform: none;
padding: var(--space-xl) var(--space-md);
min-height: 100vh;
display: flex;
align-items: flex-start; align-items: flex-start;
} }
.content,
.homelab-section,
.projects-section {
min-width: unset;
max-width: unset;
width: 100%;
}
.content { .content {
padding: var(--space-xl) var(--space-lg); padding: var(--space-xl) var(--space-lg);
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
body {
font-size: 14px;
}
.main-container { .main-container {
padding: var(--space-xl) var(--space-sm); --page-gutter: var(--space-xs);
--page-top: var(--space-md);
} }
.content { .content {
padding: var(--space-lg) var(--space-md); 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));
}
}
+31 -3
View File
@@ -14,8 +14,36 @@
filter: brightness(0.7) saturate(1.05); filter: brightness(0.7) saturate(1.05);
} }
@media (prefers-reduced-motion: reduce) { /* CSS only on phones/tablets and for reduced motion: no canvas work. */
.matrix-canvas { /* Keep this query in sync with js/flowfield.js. */
filter: brightness(0.6) saturate(0.9); @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;
} }
} }
+21 -23
View File
@@ -5,17 +5,18 @@
padding: var(--space-2xl); padding: var(--space-2xl);
width: 100%; width: 100%;
display: grid; display: grid;
grid-template-columns: 200px 1fr; grid-template-columns: 180px minmax(0, 1fr);
gap: var(--space-xl); gap: var(--space-xl);
align-items: center; align-items: center;
animation: fadeInUp 0.8s var(--ease-out-expo); animation: fadeInUp 0.8s var(--ease-out-expo);
} }
.hero-details { .hero-details {
display: flex; display: grid;
flex-direction: row; grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr);
gap: var(--space-sm); gap: var(--space-sm);
align-items: flex-start; align-items: stretch;
min-width: 0;
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;
} }
@@ -153,7 +154,6 @@
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.hero-details { .hero-details {
flex-direction: column;
gap: var(--space-xl); gap: var(--space-xl);
} }
@@ -166,6 +166,10 @@
width: 100%; width: 100%;
align-items: stretch; align-items: stretch;
} }
.tech-tags {
justify-content: center;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@@ -176,8 +180,13 @@
text-align: center; text-align: center;
} }
.hero-details {
grid-template-columns: minmax(0, 1fr);
gap: var(--space-lg);
}
.name { .name {
font-size: var(--text-xl); font-size: var(--text-2xl);
} }
.social-links { .social-links {
@@ -191,35 +200,24 @@
padding: var(--space-lg) var(--space-md); padding: var(--space-lg) var(--space-md);
} }
.name {
font-size: var(--text-lg);
}
.social-links { .social-links {
gap: var(--space-xs); display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-sm);
} }
.social-link { .social-link {
padding: var(--space-sm) var(--space-md); padding: var(--space-sm) var(--space-md);
font-size: var(--text-xs); font-size: var(--text-sm);
min-width: 0; min-width: 0;
flex: 1;
max-width: calc(50% - 6px);
justify-content: center; justify-content: center;
} }
.social-link span { .social-link:last-child {
display: none; grid-column: 1 / -1;
} }
.social-link i { .social-link i {
font-size: 16px; font-size: 16px;
} }
} }
@media (max-width: 380px) {
.social-link {
max-width: calc(100% / 2 - 6px);
padding: var(--space-sm);
}
}
+3 -5
View File
@@ -1,5 +1,4 @@
.homelab-section { .homelab-section {
margin-top: var(--space-md);
padding: var(--space-xl) var(--space-lg); padding: var(--space-xl) var(--space-lg);
background: rgba(20, 20, 30, 0.10); background: rgba(20, 20, 30, 0.10);
backdrop-filter: blur(48px); backdrop-filter: blur(48px);
@@ -7,8 +6,8 @@
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
box-shadow: var(--shadow-md), var(--inset-shadow); box-shadow: var(--shadow-md), var(--inset-shadow);
border: 1px solid var(--glass-border); border: 1px solid var(--glass-border);
min-width: 26rem; min-width: 0;
max-width: 26rem; width: 100%;
align-self: stretch; align-self: stretch;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -220,12 +219,11 @@
.homelab-buttons { .homelab-buttons {
gap: var(--space-sm); gap: var(--space-sm);
flex-direction: column; flex-direction: column;
align-items: center; align-items: stretch;
} }
.homelab-btn { .homelab-btn {
width: 100%; width: 100%;
max-width: 200px;
justify-content: center; justify-content: center;
} }
} }
+23 -3
View File
@@ -262,6 +262,26 @@
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Responsive */ /* 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) { @media (max-width: 1024px) {
.profile-info { .profile-info {
align-items: center; align-items: center;
@@ -304,7 +324,7 @@
} }
.profile-info-item { .profile-info-item {
font-size: var(--text-xs); font-size: var(--text-sm);
flex-direction: row; flex-direction: row;
gap: var(--space-xs); gap: var(--space-xs);
} }
@@ -327,6 +347,6 @@
} }
.profile-info-item { .profile-info-item {
font-size: var(--text-xs); font-size: var(--text-sm);
} }
} }
+17 -16
View File
@@ -1,5 +1,4 @@
.projects-section { .projects-section {
flex: 0 0 340px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--space-md); gap: var(--space-md);
@@ -10,8 +9,8 @@
border-radius: var(--border-radius-large); border-radius: var(--border-radius-large);
box-shadow: var(--shadow-md), var(--inset-shadow); box-shadow: var(--shadow-md), var(--inset-shadow);
border-bottom: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
min-width: 380px; min-width: 0;
max-width: 440px; width: 100%;
align-self: stretch; align-self: stretch;
} }
@@ -112,13 +111,11 @@
flex-wrap: nowrap; 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 { .project-title > span:first-child {
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
overflow: hidden; overflow-wrap: anywhere;
text-overflow: ellipsis;
white-space: nowrap;
} }
.project-icon-link { .project-icon-link {
@@ -150,12 +147,16 @@
/* Responsive */ /* Responsive */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.projects-section { .project-icon-link {
flex: unset; width: 44px;
max-width: 100%; height: 44px;
margin-left: 0; }
margin-top: var(--space-xl); }
min-width: unset;
@media (pointer: coarse) {
.project-icon-link {
width: 44px;
height: 44px;
} }
} }
@@ -179,7 +180,7 @@
} }
.projects-title { .projects-title {
font-size: var(--text-sm); font-size: var(--text-md);
} }
.project { .project {
@@ -191,7 +192,7 @@
} }
.project-desc { .project-desc {
font-size: var(--text-xs); font-size: var(--text-sm);
} }
.project-icons { .project-icons {
@@ -205,7 +206,7 @@
} }
.project-title { .project-title {
font-size: var(--text-xs); font-size: var(--text-sm);
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: var(--space-xs); gap: var(--space-xs);
+9 -9
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes"> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="description" content="ivanch's personal portfolio showcasing projects and homelab setup."> <meta name="description" content="ivanch's personal portfolio showcasing projects and homelab setup.">
<meta name="author" content="ivanch"> <meta name="author" content="ivanch">
@@ -14,14 +14,14 @@
<link rel="icon" href="/assets/favicon.ico" type="image/x-icon"> <link rel="icon" href="/assets/favicon.ico" type="image/x-icon">
<title>ivanch</title> <title>ivanch</title>
<link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="css/main.css?v=20261011-2">
<link rel="stylesheet" href="css/sections/profile.css"> <link rel="stylesheet" href="css/sections/profile.css?v=20261011-2">
<link rel="stylesheet" href="css/sections/homelab.css"> <link rel="stylesheet" href="css/sections/homelab.css?v=20261011-2">
<link rel="stylesheet" href="css/sections/projects.css"> <link rel="stylesheet" href="css/sections/projects.css?v=20261011-2">
<link rel="stylesheet" href="css/sections/hero.css"> <link rel="stylesheet" href="css/sections/hero.css?v=20261011-2">
<link rel="stylesheet" href="css/matrix.css"> <link rel="stylesheet" href="css/matrix.css?v=20261011-2">
<link rel="stylesheet" href="css/button.css"> <link rel="stylesheet" href="css/button.css">
<!-- Remix Icon CDN for social icons --> <!-- Remix Icon CDN for social icons -->
@@ -149,7 +149,7 @@
</div> </div>
</main> </main>
<script src="js/flowfield.js"></script> <script src="js/flowfield.js?v=20261011-2"></script>
<script src="js/social.js"></script> <script src="js/social.js"></script>
</body> </body>
</html> </html>
+43 -17
View File
@@ -116,7 +116,8 @@ class FlowFieldBackground {
this.sparks = []; this.sparks = [];
this.mouse = { x: 0, y: 0, active: false }; this.mouse = { x: 0, y: 0, active: false };
this.rafId = null; this.rafId = null;
this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; this.resizeRaf = null;
this.events = new AbortController();
this.width = 0; this.width = 0;
this.height = 0; this.height = 0;
@@ -157,47 +158,44 @@ class FlowFieldBackground {
this.bindEvents(); this.bindEvents();
this.handleResize(); 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. // Pre-run a short exposure so the first paint is already textured.
this.warmup(60); this.warmup(60);
this.animate(); this.animate();
} }
bindEvents() { bindEvents() {
const signal = this.events.signal;
// The canvas has pointer-events: none, so listen on window. // The canvas has pointer-events: none, so listen on window.
window.addEventListener('pointermove', (e) => { window.addEventListener('pointermove', (e) => {
this.mouse.x = e.clientX; this.mouse.x = e.clientX;
this.mouse.y = e.clientY; this.mouse.y = e.clientY;
this.mouse.active = true; this.mouse.active = true;
}, { passive: true }); }, { passive: true, signal });
window.addEventListener('pointerdown', (e) => { window.addEventListener('pointerdown', (e) => {
this.spawnClickBurst(e.clientX, e.clientY); this.spawnClickBurst(e.clientX, e.clientY);
}, { passive: true }); }, { passive: true, signal });
window.addEventListener('pointerout', () => { window.addEventListener('pointerout', () => {
this.mouse.active = false; this.mouse.active = false;
}); }, { signal });
// Pause the rAF loop when the tab is hidden to save CPU / battery. // Pause the rAF loop when the tab is hidden to save CPU / battery.
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.hidden) { if (document.hidden) {
this.stop(); this.stop();
} else if (!this.prefersReducedMotion) { } else {
this.start(); this.start();
} }
}); }, { signal });
let resizeRaf = null;
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
if (resizeRaf) cancelAnimationFrame(resizeRaf); if (this.resizeRaf) cancelAnimationFrame(this.resizeRaf);
resizeRaf = requestAnimationFrame(() => this.handleResize()); this.resizeRaf = requestAnimationFrame(() => {
}); this.resizeRaf = null;
this.handleResize();
});
}, { signal });
} }
start() { 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() { handleResize() {
const w = window.innerWidth; const w = window.innerWidth;
const h = window.innerHeight; const h = window.innerHeight;
@@ -504,5 +513,22 @@ class FlowFieldBackground {
} }
document.addEventListener('DOMContentLoaded', () => { 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();
}); });