diff --git a/404.html b/404.html new file mode 100644 index 0000000..6806c49 --- /dev/null +++ b/404.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + 404 | ivanch + + + + + + + + + + + + +
+
+ +

Signal lost

+

This page doesn't exist.

+

+ The route you followed drifted out of reach. +

+

+ Let's get you back to somewhere useful, shall we? +

+ + + Back to home + +
+
+ + + + + \ No newline at end of file diff --git a/css/button.css b/css/button.css index 2f47921..13c6adb 100644 --- a/css/button.css +++ b/css/button.css @@ -1,42 +1,66 @@ .btn { - padding: 8px 16px; + padding: var(--space-sm) var(--space-md); border: 1px solid transparent; - border-radius: 8px; - font-weight: 400; - font-size: 15px; + border-radius: var(--radius-md); + font-weight: 500; + font-size: var(--text-sm); color: var(--text-color); background: var(--button-gradient); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08); - transition: all 0.3s ease; + box-shadow: var(--shadow-sm), var(--inset-shadow); + transition: + background var(--dur-fast) var(--ease-out-soft), + transform var(--dur-fast) var(--ease-out-soft), + box-shadow var(--dur-fast) var(--ease-out-soft); position: relative; overflow: hidden; display: inline-flex; align-items: center; justify-content: center; - min-height: 44px; /* Minimum touch target size for mobile */ + gap: var(--space-xs); + min-height: 44px; text-align: center; + -webkit-tap-highlight-color: transparent; } -.btn:hover { +.btn::before { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient(120deg, transparent 30%, rgba(255, 255, 255, 0.06) 50%, transparent 70%); + transform: translateX(-100%); + transition: transform var(--dur-med) var(--ease-out-soft); + pointer-events: none; +} + +.btn:hover, +.btn:focus-visible { background: var(--button-hover-gradient); - transform: translateY(-1px); - box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15), 0 3px 6px rgba(0, 0, 0, 0.1); + transform: translateY(-1px) scale(1.012); + box-shadow: var(--shadow-md), var(--accent-glow-ring); + color: var(--text-color); +} + +.btn:hover::before, +.btn:focus-visible::before { + transform: translateX(100%); +} + +.btn:active { + transform: translateY(0) scale(0.99); } -/* Mobile button improvements */ @media (max-width: 768px) { .btn { min-height: 48px; - padding: 12px 20px; - font-size: 14px; + padding: var(--space-md) var(--space-lg); + font-size: var(--text-sm); } } @media (max-width: 480px) { .btn { - padding: 10px 16px; - font-size: 13px; + padding: var(--space-sm) var(--space-md); } -} \ No newline at end of file +} diff --git a/css/main.css b/css/main.css index 983ca24..6498e31 100644 --- a/css/main.css +++ b/css/main.css @@ -1,5 +1,6 @@ @import url('variables.css'); @import url('https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400..700&display=swap'); * { margin: 0; @@ -7,26 +8,24 @@ box-sizing: border-box; } +html { + scrollbar-gutter: stable; +} + body { font-family: var(--font-family); background: var(--background-color); min-height: 100vh; overflow-x: hidden; position: relative; - /* Improve text rendering on mobile */ + color: var(--text-color); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - /* Prevent text size adjust on orientation change */ -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; } -h1, -h2, -h3, -h4, -h5, -h6 { +h1, h2, h3, h4, h5, h6 { font-weight: 600; color: var(--text-color); } @@ -34,7 +33,7 @@ h6 { a { text-decoration: none; color: var(--primary-color); - transition: color 0.3s; + transition: color var(--dur-fast) var(--ease-out-soft); } a:hover { @@ -50,16 +49,19 @@ img { height: auto; } +/* ------------------------------------------------------------------ */ +/* Glass surface (refined shadow animation: calmer two-state pulse) */ +/* ------------------------------------------------------------------ */ .glass { background: var(--glass-background); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); - border-radius: var(--border-radius); + backdrop-filter: blur(8px) saturate(140%); + -webkit-backdrop-filter: blur(8px) saturate(140%); + border-radius: var(--border-radius-large); border-top: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border); - box-shadow: 0 8px 36px 0 var(--glass-shadow), var(--inset-shadow); + box-shadow: var(--shadow-lg), var(--inset-shadow); will-change: box-shadow; - animation: glass-shadow 8s ease-in-out infinite !important; + animation: glass-shadow 10s ease-in-out infinite; } .main-container { @@ -69,7 +71,7 @@ img { transform: translate(-50%, -50%); max-width: 1200px; width: 100%; - padding: 0 20px; + padding: 0 var(--space-lg); display: flex; align-items: center; justify-content: center; @@ -78,98 +80,63 @@ img { @keyframes fadeInUp { from { opacity: 0; - transform: translateY(30px); + transform: translateY(28px); } - to { opacity: 1; transform: translateY(0); } } -/* subtle, natural moving/pulsing shadow for .glass */ +/* Subtle, calm pulsing shadow for .glass */ @keyframes glass-shadow { - 0% { - box-shadow: 0 6px 24px -2px var(--glass-shadow), 0 18px 48px -12px var(--dark-shadow-15), inset 0 1px 0 rgba(255, 55, 255, 0.1); + 0%, 100% { + box-shadow: + 0 8px 24px -6px var(--glass-shadow), + 0 18px 48px -16px var(--dark-shadow-20), + inset 0 1px 0 rgba(255, 255, 255, 0.08); } - - 25% { - box-shadow: 6px 10px 38px -6px var(--tertiary-color-20), 0 22px 60px -18px var(--dark-shadow-18), inset 0 1px 0 rgba(255, 55, 255, 0.1); - } - 50% { - box-shadow: 0 8px 36px 0 var(--glass-shadow), 0 28px 72px -24px var(--dark-shadow-20), inset 0 1px 0 rgba(255, 255, 255, 0.1); - } - - 75% { - box-shadow: -6px 10px 38px -6px var(--primary-color-16), 0 22px 60px -18px rgba(0, 0, 0, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.1); - } - - 100% { - box-shadow: 0 6px 24px -2px var(--glass-shadow), 0 18px 48px -12px var(--dark-shadow-15), inset 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: + 0 12px 32px -6px var(--tertiary-color-20), + 0 28px 72px -24px var(--dark-shadow-18), + inset 0 1px 0 rgba(255, 255, 255, 0.1); } } .content { color: var(--text-color); - background: rgba(0, 0, 0, 0.10); - backdrop-filter: blur(32px) saturate(180%) brightness(1.1); - -webkit-backdrop-filter: blur(32px) saturate(180%) brightness(1.1); + background: rgba(0, 0, 0, 0.12); + backdrop-filter: blur(32px) saturate(180%) brightness(1.08); + -webkit-backdrop-filter: blur(32px) saturate(180%) brightness(1.08); border-radius: var(--border-radius-large); - box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.15); - padding: 32px 24px; - transition: background 0.3s, box-shadow 0.3s; + box-shadow: var(--shadow-md), var(--inset-shadow); + 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; } -/* Mobile responsive content */ -@media (max-width: 1024px) { - .content { - min-width: unset; - max-width: unset; - width: 100%; - padding: 24px 20px; - } - - .main-container { - position: relative; - top: auto; - left: auto; - transform: none; - padding: 20px 15px; - min-height: 100vh; - display: flex; - align-items: flex-start; - padding-top: 40px; - padding-bottom: 40px; - } +/* ------------------------------------------------------------------ */ +/* Accessibility */ +/* ------------------------------------------------------------------ */ +:focus-visible { + outline: 2px solid var(--primary-500); + outline-offset: 2px; + border-radius: var(--radius-sm); } -@media (max-width: 480px) { - body { - font-size: 14px; - } - - .main-container { - padding: 15px 10px; - padding-top: 30px; - padding-bottom: 30px; - } - - .content { - padding: 20px 16px; - min-width: unset; - max-width: unset; - width: 100%; - } -} - -/* Accessibility: disable animated shadows for users who prefer reduced motion */ @media (prefers-reduced-motion: reduce) { .glass { animation: none !important; } + *, *::before, *::after { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.001ms !important; + scroll-behavior: auto !important; + } } /* High DPI display optimizations */ @@ -180,13 +147,56 @@ img { only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { .glass { - /* Enhanced blur for high DPI screens */ - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px) saturate(140%); + -webkit-backdrop-filter: blur(12px) saturate(140%); } - .content { - backdrop-filter: blur(40px) saturate(180%) brightness(1.1); - -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(1.1); + backdrop-filter: blur(40px) saturate(180%) brightness(1.08); + -webkit-backdrop-filter: blur(40px) saturate(180%) brightness(1.08); } -} \ No newline at end of file +} + +/* ------------------------------------------------------------------ */ +/* 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. */ +/* ------------------------------------------------------------------ */ +@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; + 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); + } + + .content { + padding: var(--space-lg) var(--space-md); + } +} diff --git a/css/matrix.css b/css/matrix.css index 0db27cb..8d3264e 100644 --- a/css/matrix.css +++ b/css/matrix.css @@ -7,8 +7,15 @@ z-index: -1; pointer-events: none; overflow: hidden; - background: rgba(0, 0, 0, 0.85); - background-image: linear-gradient(135deg, rgba(0,0,0,0.9) 0%, rgba(20,20,40,0.7) 100%); - filter: brightness(0.55); + background: rgba(0, 0, 0, 0.88); + background-image: + radial-gradient(120% 100% at 50% 0%, rgba(20, 24, 48, 0.55) 0%, rgba(0, 0, 0, 0) 60%), + linear-gradient(135deg, rgba(0, 0, 0, 0.9) 0%, rgba(18, 18, 38, 0.72) 100%); + filter: brightness(0.7) saturate(1.05); } +@media (prefers-reduced-motion: reduce) { + .matrix-canvas { + filter: brightness(0.6) saturate(0.9); + } +} diff --git a/css/sections/error.css b/css/sections/error.css new file mode 100644 index 0000000..1479d29 --- /dev/null +++ b/css/sections/error.css @@ -0,0 +1,63 @@ +.error-card { + width: min(100%, 680px); + padding: var(--space-3xl) var(--space-2xl); + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: var(--space-md); + animation: fadeInUp 0.8s var(--ease-out-expo); +} + +.error-code { + font-family: var(--font-mono); + font-size: clamp(5rem, 16vw, 9rem); + font-weight: 700; + line-height: 0.85; + letter-spacing: -0.08em; + background: var(--accent-gradient-text); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + filter: drop-shadow(0 0 18px rgba(79, 195, 247, 0.12)); +} + +.error-kicker { + margin-top: var(--space-sm); + color: var(--primary-400); + font-family: var(--font-mono); + font-size: var(--text-xs); + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.error-card h1 { + font-size: var(--text-xl); + margin-top: var(--space-2xs); +} + +.error-message { + max-width: 34rem; + color: var(--text-color-mid); + font-size: var(--text-base); + line-height: 1.6; +} + +.error-home-link { + margin-top: var(--space-sm); +} + +@media (max-width: 480px) { + .error-card { + padding: var(--space-2xl) var(--space-lg); + } + + .error-card h1 { + font-size: var(--text-lg); + } + + .error-message { + font-size: var(--text-sm); + } +} diff --git a/css/sections/hero.css b/css/sections/hero.css index e6d609d..8647ef0 100644 --- a/css/sections/hero.css +++ b/css/sections/hero.css @@ -1,207 +1,217 @@ -.social-links { -} - -.social-link { -} - -.email-tooltip { - position: absolute; - background: rgba(40, 40, 40, 0.95); - color: #fff; - padding: 6px 16px; - border-radius: 8px; - font-size: 0.95rem; - font-weight: 500; - box-shadow: 0 2px 12px rgba(0,0,0,0.18); - opacity: 0; - pointer-events: none; - transition: opacity 0.3s, transform 0.3s; - z-index: 10; - white-space: nowrap; - width: max-content; - display: block; -} - -.email-tooltip { - position: absolute; - left: 50%; - top: -32px; - transform: translateX(-50%) scale(1); - background: rgba(40, 40, 40, 0.95); - color: #fff; - padding: 6px 16px; - border-radius: 8px; - font-size: 0.95rem; - font-weight: 500; - box-shadow: 0 2px 12px rgba(0,0,0,0.18); - opacity: 0; - pointer-events: none; - transition: opacity 0.3s, transform 0.3s; - z-index: 10; - white-space: nowrap; +/* ------------------------------------------------------------------ */ +/* Hero card layout */ +/* ------------------------------------------------------------------ */ +.hero-card { + padding: var(--space-2xl); + width: 100%; + display: grid; + grid-template-columns: 200px 1fr; + gap: var(--space-xl); + align-items: center; + animation: fadeInUp 0.8s var(--ease-out-expo); } .hero-details { display: flex; flex-direction: row; - gap: 8px; + gap: var(--space-sm); align-items: flex-start; width: 100%; justify-content: flex-start; } + .hero-main-column { display: flex; flex-direction: column; - gap: 24px; + gap: var(--space-xl); flex: 1 1 auto; min-width: 0; } -.hero-card { - padding: 42px; - width: 100%; - display: grid; - grid-template-columns: 200px 1fr; - gap: 40px; - align-items: center; - animation: fadeInUp 0.8s ease-out; -} - +/* ------------------------------------------------------------------ */ +/* Name / description / quote */ +/* ------------------------------------------------------------------ */ .name { - font-size: 2.8rem; + font-size: var(--text-2xl); font-weight: 700; - margin-bottom: 12px; - background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); + margin-bottom: var(--space-sm); + background: var(--accent-gradient-text); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; letter-spacing: -0.5px; } -.description { - font-size: 1rem; - margin-bottom: 32px; - color: var(--text-color-light); - font-weight: 400; - line-height: 1.6; - letter-spacing: 0.2px; +/* Terminal caret — blinks subtly after the name */ +/* Tech chips for the skill tags line */ +.tech-tags { + display: flex; + flex-wrap: wrap; + gap: var(--space-xs); + margin-bottom: var(--space-sm); } +.tech-tag { + display: inline-flex; + align-items: center; + padding: var(--space-2xs) var(--space-sm); + font-size: var(--text-xs); + font-weight: 500; + color: var(--text-color-light); + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--surface-divider); + border-radius: var(--radius-pill); + letter-spacing: 0.02em; + transition: background var(--dur-fast) var(--ease-out-soft), + color var(--dur-fast) var(--ease-out-soft); +} + +/* Styled pull-quote (the Lain line) */ +.quote { + display: block; + margin-top: var(--space-sm); + margin-bottom: var(--space-lg); + padding-left: var(--space-md); + font-size: var(--text-sm); + color: var(--text-color-mid); + font-style: italic; + border-left: 2px solid var(--primary-500); + line-height: 1.5; +} + +/* ------------------------------------------------------------------ */ +/* Social links */ +/* ------------------------------------------------------------------ */ .social-links { display: flex; - gap: 16px; + gap: var(--space-md); flex-wrap: wrap; justify-content: center; align-items: center; - margin-top: 8px; + margin-top: var(--space-sm); position: relative; } .social-link { display: inline-flex; align-items: center; - gap: 8px; - transition: transform 0.2s ease, box-shadow 0.2s ease; + gap: var(--space-sm); + transition: transform var(--dur-fast) var(--ease-out-soft), + box-shadow var(--dur-fast) var(--ease-out-soft); } .social-link:hover { transform: translateY(-2px); - box-shadow: 0 4px 12px var(--dark-shadow-20); -} -.social-link { - display: inline-flex; - align-items: center; - gap: 8px; } -/* Responsive Design - Mobile First Approach */ +/* ------------------------------------------------------------------ */ +/* Email tooltip (consolidated — was duplicated in original CSS) */ +/* ------------------------------------------------------------------ */ +.email-tooltip { + position: absolute; + left: 50%; + top: -32px; + transform: translateX(-50%) scale(1); + background: rgba(20, 22, 32, 0.96); + color: #fff; + padding: var(--space-xs) var(--space-md); + border-radius: var(--radius-sm); + border: 1px solid var(--glass-border); + font-size: var(--text-sm); + font-weight: 500; + box-shadow: var(--shadow-md); + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity var(--dur-fast) var(--ease-out-soft), + transform var(--dur-fast) var(--ease-out-soft); + z-index: 10; + white-space: nowrap; +} + +.email-tooltip.is-visible { + opacity: 1; + visibility: visible; + transform: translateX(-50%) translateY(-75%); +} + +.email-tooltip::after { + content: ""; + position: absolute; + bottom: -5px; + left: 50%; + transform: translateX(-50%) rotate(45deg); + width: 8px; + height: 8px; + background: rgba(20, 22, 32, 0.96); + border-right: 1px solid var(--glass-border); + border-bottom: 1px solid var(--glass-border); +} + +/* ------------------------------------------------------------------ */ +/* Responsive */ +/* ------------------------------------------------------------------ */ @media (max-width: 1024px) { .hero-details { flex-direction: column; - gap: 24px; + gap: var(--space-xl); } - + .hero-card { grid-template-columns: 1fr; text-align: center; } -} -/* Ensure hero main column and its boxes fill available width on tablet/mobile */ -@media (max-width: 1024px) { .hero-main-column { width: 100%; align-items: stretch; } - - /* Override .content fixed min/max so it can expand to the container */ - .content { - min-width: unset; - max-width: unset; - width: 100%; - } - - .homelab-section { - min-width: unset; - max-width: unset; - width: 100%; - } } @media (max-width: 768px) { .hero-card { grid-template-columns: 1fr; - gap: 32px; - padding: 32px 24px; + gap: var(--space-xl); + padding: var(--space-xl) var(--space-lg); text-align: center; } .name { - font-size: 2rem; - } - - .description { - margin-bottom: 24px; - font-size: 1rem; + font-size: var(--text-xl); } .social-links { justify-content: center; - gap: 14px; + gap: var(--space-sm); } } @media (max-width: 480px) { .hero-card { - padding: 24px 16px; + padding: var(--space-lg) var(--space-md); } .name { - font-size: 1.8rem; - } - - .description { - margin-bottom: 20px; + font-size: var(--text-lg); } .social-links { - gap: 12px; - flex-wrap: wrap; + gap: var(--space-xs); } .social-link { - padding: 10px 14px; - font-size: 13px; + padding: var(--space-sm) var(--space-md); + font-size: var(--text-xs); min-width: 0; flex: 1; max-width: calc(50% - 6px); justify-content: center; } - + .social-link span { display: none; } - + .social-link i { font-size: 16px; } @@ -210,12 +220,6 @@ @media (max-width: 380px) { .social-link { max-width: calc(100% / 2 - 6px); - padding: 12px; + padding: var(--space-sm); } } - -.quote { - font-size: 0.9rem; - color: var(--text-color-dark); - font-style: italic; -} \ No newline at end of file diff --git a/css/sections/homelab.css b/css/sections/homelab.css index 0e93338..1e6a7d0 100644 --- a/css/sections/homelab.css +++ b/css/sections/homelab.css @@ -1,68 +1,52 @@ .homelab-section { - margin-top: 18px; - padding: 32px 24px; + margin-top: var(--space-md); + padding: var(--space-xl) var(--space-lg); background: rgba(20, 20, 30, 0.10); backdrop-filter: blur(48px); -webkit-backdrop-filter: blur(48px); - border-radius: 24px; - box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.18), inset 0 2px 0 rgba(255, 255, 255, 0.10); + 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; align-self: stretch; display: flex; flex-direction: column; - gap: 18px; + gap: var(--space-md); position: relative; overflow: hidden; text-align: center; + transition: transform var(--dur-med) var(--ease-out-soft), + border-color var(--dur-med) var(--ease-out-soft), + box-shadow var(--dur-med) var(--ease-out-soft); } -/* Mobile responsive homelab section */ -@media (max-width: 768px) { - .homelab-section { - min-width: unset; - max-width: unset; - width: 100%; - padding: 28px 20px; - } - - .homelab-title { - font-size: 1.3rem; - text-align: center; - } - - .homelab-buttons { - justify-content: center; - } +.homelab-section::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--accent-gradient); + opacity: 0.5; + transition: opacity var(--dur-med) var(--ease-out-soft), + box-shadow var(--dur-med) var(--ease-out-soft); + z-index: 2; } -@media (max-width: 480px) { - .homelab-section { - padding: 24px 16px; - gap: 16px; - min-width: unset; - max-width: unset; - width: 100%; - } - - .homelab-title { - font-size: 1.2rem; - } - - .homelab-buttons { - gap: 12px; - flex-direction: column; - align-items: center; - } - - .homelab-btn { - width: 100%; - max-width: 200px; - text-align: center; - justify-content: center; - } +.homelab-section:hover { + transform: translateY(-2px); + border-color: rgba(79, 195, 247, 0.25); + box-shadow: var(--shadow-lg), var(--accent-glow-ring); } +.homelab-section:hover::before { + opacity: 1; + box-shadow: 0 0 8px var(--primary-500); +} + +/* Calm aurora background — single tasteful radial + fine grid overlay */ .homelab-background { position: absolute; top: 0; @@ -71,31 +55,36 @@ height: 100%; z-index: -1; background: - radial-gradient(120% 140% at 10% 10%, rgba(90, 210, 255, 0.18) 0%, rgba(6, 18, 38, 0.0) 45%), - radial-gradient(140% 160% at 90% 10%, rgba(142, 86, 255, 0.16) 0%, rgba(10, 6, 24, 0.0) 50%), - radial-gradient(160% 180% at 50% 100%, rgba(255, 77, 156, 0.12) 0%, rgba(12, 8, 20, 0.0) 55%), - linear-gradient(135deg, rgba(10, 14, 30, 0.95) 0%, rgba(12, 22, 44, 0.9) 40%, rgba(14, 10, 36, 0.9) 100%); - background-size: 160% 160%, 170% 170%, 180% 180%, 100% 100%; - animation: homelab-aurora 12s ease-in-out infinite; - filter: saturate(1.15) contrast(1.05); + radial-gradient(120% 140% at 15% 10%, rgba(79, 195, 247, 0.16) 0%, rgba(6, 18, 38, 0) 50%), + radial-gradient(120% 140% at 85% 90%, rgba(118, 75, 162, 0.16) 0%, rgba(10, 6, 24, 0) 55%), + linear-gradient(135deg, rgba(10, 14, 30, 0.92) 0%, rgba(12, 22, 44, 0.88) 50%, rgba(14, 10, 36, 0.9) 100%); + background-size: 160% 160%, 170% 170%, 100% 100%; + animation: homelab-aurora 14s ease-in-out infinite; + filter: saturate(1.1); + transition: opacity var(--dur-med) var(--ease-out-soft), + filter var(--dur-med) var(--ease-out-soft); +} + +.homelab-section:hover .homelab-background { + opacity: 0.95; + filter: saturate(1.25) brightness(1.05); } .homelab-background::after { content: ""; position: absolute; - inset: -20% -10%; + inset: 0; background: repeating-linear-gradient( 120deg, - rgba(120, 220, 255, 0.06) 0px, - rgba(120, 220, 255, 0.06) 1px, - rgba(10, 12, 24, 0.0) 1px, - rgba(10, 12, 24, 0.0) 10px - ), - radial-gradient(60% 60% at 70% 30%, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.0) 60%); + rgba(120, 220, 255, 0.035) 0px, + rgba(120, 220, 255, 0.035) 1px, + rgba(10, 12, 24, 0) 1px, + rgba(10, 12, 24, 0) 12px + ); mix-blend-mode: screen; - opacity: 0.55; - animation: homelab-grid 30s linear infinite; + opacity: 0.5; + animation: homelab-grid 32s linear infinite; pointer-events: none; } @@ -103,82 +92,140 @@ content: ""; position: absolute; inset: 0; - background: radial-gradient(80% 80% at 50% 50%, rgba(8, 12, 24, 0.0) 0%, rgba(8, 12, 24, 0.45) 70%, rgba(8, 12, 24, 0.75) 100%); + background: radial-gradient(80% 80% at 50% 50%, rgba(8, 12, 24, 0) 0%, rgba(8, 12, 24, 0.4) 70%, rgba(8, 12, 24, 0.7) 100%); pointer-events: none; } @keyframes homelab-aurora { 0% { - background-position: 0% 50%, 100% 40%, 50% 100%, 0% 0%; + background-position: 0% 50%, 100% 40%, 0% 0%; } 50% { - background-position: 80% 40%, 20% 50%, 60% 80%, 0% 0%; + background-position: 80% 40%, 20% 50%, 0% 0%; } 100% { - background-position: 0% 50%, 100% 40%, 50% 100%, 0% 0%; + background-position: 0% 50%, 100% 40%, 0% 0%; } } @keyframes homelab-grid { - 0% { - transform: translate3d(0, 0, 0); - } - 100% { - transform: translate3d(6%, 0, 0); - } + 0% { transform: translate3d(0, 0, 0); } + 100% { transform: translate3d(6%, 0, 0); } } @keyframes gradient-animation { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } } +/* Consolidated animated gradient title — shared utility across sections */ .homelab-title { - font-size: 1.8rem; + font-size: var(--text-lg); font-weight: 700; - margin-bottom: 4px; + margin: 0 auto; letter-spacing: 0.03em; background: linear-gradient(270deg, - var(--primary-color), - var(--secondary-color), - var(--tertiary-color), + var(--primary-500), + var(--secondary-500), + var(--tertiary-500), var(--primary-color-light), - var(--primary-color), - var(--secondary-color), - var(--tertiary-color)); + var(--primary-500), + var(--secondary-500), + var(--tertiary-500)); background-size: 800% 800%; -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; animation: gradient-animation 25s ease infinite; - text-shadow: - 0 0 2px rgba(255, 255, 255, 0.3), - 0 0 4px rgba(0, 255, 255, 0.1); } .homelab-buttons { display: flex; - gap: 16px; + gap: var(--space-sm); flex-wrap: wrap; justify-content: center; + margin-top: auto; + z-index: 2; } .homelab-buttons > a.homelab-btn { - display: flex; + display: inline-flex; align-items: center; justify-content: center; - padding: 10px 16px; - border: 1px solid rgba(255, 255, 255, 0.10); - border-radius: 12px; - color: #ffffff; + padding: var(--space-sm) var(--space-md); + border: 1px solid var(--surface-divider); + border-radius: var(--radius-md); + color: var(--text-color); font-weight: 600; text-decoration: none; - transition: background 0.3s ease, border-color 0.3s ease, transform 0.2s ease; -} \ No newline at end of file + min-height: 44px; + font-size: var(--text-sm); + background: var(--button-gradient); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + box-shadow: var(--shadow-sm), var(--inset-shadow); + transition: background var(--dur-fast) var(--ease-out-soft), + border-color var(--dur-fast) var(--ease-out-soft), + transform var(--dur-fast) var(--ease-out-soft), + box-shadow var(--dur-fast) var(--ease-out-soft); +} + +.homelab-buttons > a.homelab-btn i { + margin-right: var(--space-xs); + font-size: 1.1em; + transition: transform var(--dur-fast) var(--ease-out-soft); +} + +.homelab-buttons > a.homelab-btn:hover, +.homelab-buttons > a.homelab-btn:focus-visible { + background: var(--button-hover-gradient); + border-color: rgba(79, 195, 247, 0.4); + transform: translateY(-1px) scale(1.012); + box-shadow: var(--shadow-md), var(--accent-glow-ring); +} + +.homelab-buttons > a.homelab-btn:hover i, +.homelab-buttons > a.homelab-btn:focus-visible i { + transform: translateX(1px) scale(1.1); +} + +/* ------------------------------------------------------------------ */ +/* Responsive */ +/* ------------------------------------------------------------------ */ +@media (max-width: 768px) { + .homelab-section { + padding: var(--space-xl) var(--space-md); + } + + .homelab-title { + font-size: var(--text-md); + } + + .homelab-buttons { + justify-content: center; + } +} + +@media (max-width: 480px) { + .homelab-section { + padding: var(--space-lg) var(--space-md); + gap: var(--space-md); + } + + .homelab-title { + font-size: var(--text-md); + } + + .homelab-buttons { + gap: var(--space-sm); + flex-direction: column; + align-items: center; + } + + .homelab-btn { + width: 100%; + max-width: 200px; + justify-content: center; + } +} diff --git a/css/sections/profile.css b/css/sections/profile.css index c045440..117c997 100644 --- a/css/sections/profile.css +++ b/css/sections/profile.css @@ -1,19 +1,228 @@ - -.profile-info { - margin-top: 1.2rem; - display: flex; - flex-direction: column; - gap: 0.7rem; - align-items: flex-start; +.profile-container { + position: relative; } +.avatar { + width: 160px; + height: 160px; + border-radius: var(--border-radius-large); + background: linear-gradient(135deg, var(--secondary-500) 0%, var(--tertiary-500) 100%); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-color); + position: relative; + border: 1px solid var(--glass-border-strong); + box-shadow: + var(--shadow-md), + 0 0 0 1px rgba(255, 255, 255, 0.04) inset; + isolation: isolate; +} + +/* Idle glow ring — slow, low amplitude, "alive" feel */ +.avatar::after { + content: ""; + position: absolute; + inset: -1px; + border-radius: inherit; + background: conic-gradient(from 0deg, + var(--primary-500), + var(--secondary-500), + var(--tertiary-400), + var(--primary-500)); + filter: blur(8px) saturate(120%); + opacity: 0.09; + z-index: -1; + animation: avatar-glow 12s linear infinite; +} + +.avatar img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: var(--border-radius-large); + position: relative; + z-index: 2; +} + +/* ------------------------------------------------------------------ */ +/* Real glitch on hover */ +/* Two pseudo-layered clones (R + B channel shift + sliced clip-path) */ +/* are generated via JS on .avatar; styles here control the animation.*/ +/* ------------------------------------------------------------------ */ +.avatar .glitch-layer { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + border-radius: var(--border-radius-large); + pointer-events: none; + opacity: 0; + z-index: 3; + mix-blend-mode: screen; + transition: opacity 0.05s ease; +} + +.avatar:hover .glitch-layer { + opacity: 1; +} + +.avatar .glitch-layer.r { + filter: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter%20id='red'%3E%3CfeColorMatrix%20type='matrix'%20values='1%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%200%201%200'/%3E%3C/filter%3E%3C/svg%3E%23red"); +} + +.avatar:hover .glitch-layer.r { + animation: glitch-anim-r 0.6s linear infinite; +} + +.avatar .glitch-layer.b { + filter: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter%20id='cyan'%3E%3CfeColorMatrix%20type='matrix'%20values='0%200%200%200%200%200%201%200%200%200%200%200%201%200%200%200%200%200%201%200'/%3E%3C/filter%3E%3C/svg%3E%23cyan"); +} + +.avatar:hover .glitch-layer.b { + animation: glitch-anim-b 0.45s linear infinite; +} + +@keyframes glitch-anim-r { + 0% { + clip-path: inset(20% 0 60% 0); + transform: translate(-4px, -2px) skewX(-2deg); + } + + 10% { + clip-path: inset(65% 0 10% 0); + transform: translate(3px, 2px) skewX(4deg); + } + + 20% { + clip-path: inset(5% 0 85% 0); + transform: translate(-2px, -3px) skewX(-1deg); + } + + 30% { + clip-path: inset(80% 0 5% 0); + transform: translate(4px, 1px) skewX(2deg); + } + + 40% { + clip-path: inset(45% 0 45% 0); + transform: translate(-3px, 3px) skewX(-4deg); + } + + 50% { + clip-path: inset(12% 0 75% 0); + transform: translate(2px, -1px) skewX(1deg); + } + + 60% { + clip-path: inset(90% 0 2% 0); + transform: translate(-5px, 2px) skewX(-3deg); + } + + 70% { + clip-path: inset(30% 0 55% 0); + transform: translate(3px, -2px) skewX(5deg); + } + + 80% { + clip-path: inset(72% 0 18% 0); + transform: translate(-1px, 4px) skewX(-2deg); + } + + 90% { + clip-path: inset(50% 0 35% 0); + transform: translate(5px, -1px) skewX(3deg); + } + + 100% { + clip-path: inset(20% 0 60% 0); + transform: translate(-4px, -2px) skewX(-2deg); + } +} + +@keyframes glitch-anim-b { + 0% { + clip-path: inset(50% 0 30% 0); + transform: translate(4px, 2px) skewX(3deg); + } + + 10% { + clip-path: inset(15% 0 75% 0); + transform: translate(-3px, -2px) skewX(-2deg); + } + + 20% { + clip-path: inset(85% 0 5% 0); + transform: translate(2px, 3px) skewX(4deg); + } + + 30% { + clip-path: inset(35% 0 55% 0); + transform: translate(-4px, -1px) skewX(-1deg); + } + + 40% { + clip-path: inset(70% 0 15% 0); + transform: translate(3px, -3px) skewX(2deg); + } + + 50% { + clip-path: inset(5% 0 90% 0); + transform: translate(-2px, 2px) skewX(-4deg); + } + + 60% { + clip-path: inset(40% 0 45% 0); + transform: translate(5px, -2px) skewX(1deg); + } + + 70% { + clip-path: inset(60% 0 25% 0); + transform: translate(-3px, 4px) skewX(-3deg); + } + + 80% { + clip-path: inset(22% 0 68% 0); + transform: translate(1px, -4px) skewX(5deg); + } + + 90% { + clip-path: inset(95% 0 1% 0); + transform: translate(-5px, 1px) skewX(-2deg); + } + + 100% { + clip-path: inset(50% 0 30% 0); + transform: translate(4px, 2px) skewX(3deg); + } +} + +@keyframes avatar-glow { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +/* Remove leftover unused label rule; color: #222 was invisible on black */ +.profile-info { + margin-top: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-sm); + align-items: flex-start; +} .profile-info-item { display: flex; flex-direction: row; align-items: center; - gap: 0.6rem; - font-size: 0.95rem; + gap: var(--space-sm); + font-size: var(--text-sm); background: none; border-radius: 0; padding: 0; @@ -23,76 +232,42 @@ .profile-info-item i { font-size: 1.3rem; - color: var(--primary-color); - margin-right: 0.2rem; -} - -.profile-info-label { - font-weight: 600; - color: #222; - margin-right: 0.3rem; + color: var(--primary-500); + margin-right: var(--space-2xs); + flex-shrink: 0; } .profile-info-value { - color: var(--text-color-dark); + color: var(--text-color-mid); font-weight: 400; + display: flex; + flex-direction: column; + line-height: 1.3; } .profile-info-value span { display: block; } -.profile-container { - position: relative; +.profile-info-value span:first-child { + color: var(--text-color-light); + font-weight: 500; } -.avatar { - width: 160px; - height: 160px; - border-radius: var(--border-radius-large); - background: linear-gradient(135deg, var(--secondary-color) 0%, var(--tertiary-color) 100%); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-color); - position: relative; - overflow: hidden; - border: 2px solid rgba(255, 255, 255, 0.2); -} - -.avatar::before { - content: ''; - position: absolute; - top: -50%; - left: -50%; - width: 200%; - height: 200%; - background: linear-gradient(45deg, transparent 30%, rgba(255, 255, 255, 0.1) 50%, transparent 70%); -} - -.avatar img { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: var(--border-radius-large); -} - -.avatar:hover img { - animation: avatar-glitch 1s infinite linear; -} - -@keyframes avatar-glitch { - 0%, 100% { - transform: translate(0); - } +.profile-info-value span:last-child { + color: var(--text-color-mid); + font-size: var(--text-xs); } +/* ------------------------------------------------------------------ */ +/* Responsive */ +/* ------------------------------------------------------------------ */ @media (max-width: 1024px) { .profile-info { align-items: center; - margin-top: 1.5rem; + margin-top: var(--space-lg); } - + .avatar { width: 140px; height: 140px; @@ -104,17 +279,17 @@ .profile-container { text-align: center; } - + .profile-info { align-items: center; - margin-top: 1.5rem; + margin-top: var(--space-lg); } - + .profile-info-item { justify-content: center; text-align: center; } - + .avatar { width: 140px; height: 140px; @@ -124,21 +299,21 @@ @media (max-width: 480px) { .profile-info { - gap: 0.8rem; - margin-top: 1.2rem; + gap: var(--space-sm); + margin-top: var(--space-md); } - + .profile-info-item { - font-size: 0.9rem; + font-size: var(--text-xs); flex-direction: row; - gap: 0.4rem; + gap: var(--space-xs); } - + .profile-info-item i { font-size: 1.2rem; - margin-right: 0.2rem; + margin-right: var(--space-2xs); } - + .avatar { width: 120px; height: 120px; @@ -150,8 +325,8 @@ width: 100px; height: 100px; } - + .profile-info-item { - font-size: 0.85rem; + font-size: var(--text-xs); } -} +} \ No newline at end of file diff --git a/css/sections/projects.css b/css/sections/projects.css index bc9c55f..53f4a2b 100644 --- a/css/sections/projects.css +++ b/css/sections/projects.css @@ -2,13 +2,13 @@ flex: 0 0 340px; display: flex; flex-direction: column; - gap: 18px; - padding: 32px 24px; + gap: var(--space-md); + padding: var(--space-xl) var(--space-lg); background: rgba(20, 20, 30, 0.10); backdrop-filter: blur(24px) saturate(140%) brightness(1.08); -webkit-backdrop-filter: blur(24px) saturate(140%) brightness(1.08); border-radius: var(--border-radius-large); - box-shadow: 0 8px 36px 0 var(--glass-shadow), var(--inset-shadow); + box-shadow: var(--shadow-md), var(--inset-shadow); border-bottom: 1px solid var(--glass-border); min-width: 380px; max-width: 440px; @@ -16,49 +16,84 @@ } .projects-title { - font-size: 1.6rem; + font-size: var(--text-md); font-weight: 700; - margin-bottom: 10px; + margin-bottom: var(--space-sm); color: var(--text-color); letter-spacing: 0.03em; - background: linear-gradient(135deg, var(--secondary-color) 0%, var(--text-color) 100%); + background: linear-gradient(135deg, var(--secondary-500) 0%, var(--text-color) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } +/* ------------------------------------------------------------------ */ +/* Project cards — unified surface + accent left bar */ +/* nth-of-type replaces nth-child so .projects-title doesn't shift */ +/* the odd/even alternation (bug fix). */ +/* ------------------------------------------------------------------ */ .project { - padding: 18px 16px; + position: relative; + padding: var(--space-md) var(--space-md) var(--space-md) var(--space-lg); border-radius: var(--border-radius); - box-shadow: 0 2px 12px 0 rgba(31, 38, 135, 0.10); - margin-bottom: 0; + background: var(--surface-raised); + border: 1px solid var(--surface-divider); + box-shadow: var(--shadow-sm); display: flex; flex-direction: column; - gap: 6px; - transition: transform 0.28s cubic-bezier(0.4, 0.2, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0.2, 0.2, 1); + gap: var(--space-2xs); + transition: transform var(--dur-med) var(--ease-out-soft), + box-shadow var(--dur-med) var(--ease-out-soft), + border-color var(--dur-med) var(--ease-out-soft), + filter var(--dur-med) var(--ease-out-soft); + overflow: hidden; } -.project:hover { - transform: scale(1.012); - box-shadow: 0 4px 24px 0 rgba(31, 38, 135, 0.13), 0 1.5px 4px 0 rgba(255, 255, 255, 0.08) inset; - filter: brightness(1.04) saturate(1.08); +/* Thin accent bar — rotating accent color per card */ +.project::before { + content: ""; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 60%; + border-radius: var(--radius-pill); + background: var(--primary-500); + opacity: 0.6; + transition: height var(--dur-med) var(--ease-out-soft), + opacity var(--dur-med) var(--ease-out-soft); } -.project:nth-child(odd) { - background: linear-gradient(135deg, rgba(20, 30, 60, 0.75) 0%, rgba(40, 50, 120, 0.75) 100%); +.project:nth-of-type(odd)::before { + background: linear-gradient(var(--primary-500), var(--secondary-500)); } -.project:nth-child(even) { - background: linear-gradient(135deg, rgba(10, 10, 10, 0.82) 0%, rgba(30, 30, 30, 0.82) 100%); +.project:nth-of-type(even)::before { + background: linear-gradient(var(--tertiary-500), var(--primary-500)); +} + +.project:hover, +.project:focus-within { + transform: translateY(-2px); + box-shadow: var(--shadow-md), var(--accent-glow-ring); + border-color: rgba(79, 195, 247, 0.3); + filter: brightness(1.05) saturate(1.08); +} + +.project:hover::before, +.project:focus-within::before { + height: 85%; + opacity: 1; } .project-title { - font-size: 1.15rem; + font-size: var(--text-md); font-weight: 600; color: var(--text-color); - margin-bottom: 2px; + margin-bottom: var(--space-2xs); letter-spacing: 0.01em; - background: linear-gradient(90deg, var(--text-color) 0%, var(--secondary-color) 100%); + background: linear-gradient(90deg, var(--text-color) 0%, var(--secondary-400) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; @@ -66,18 +101,18 @@ display: flex; align-items: center; justify-content: space-between; - gap: 8px; + gap: var(--space-sm); } .project-icons { display: flex; - gap: 8px; + gap: var(--space-xs); align-items: center; flex: 0 0 auto; flex-wrap: nowrap; } -/* Ensure long titles don't push icons to a new line */ +/* Keep long titles from pushing icons to a new line */ .project-title > span:first-child { flex: 1 1 auto; min-width: 0; @@ -90,82 +125,89 @@ display: inline-flex; align-items: center; justify-content: center; - color: var(--text-color); - transition: all 0.2s; + width: 28px; + height: 28px; + border-radius: var(--radius-sm); + color: var(--text-color-light); + transition: background var(--dur-fast) var(--ease-out-soft), + color var(--dur-fast) var(--ease-out-soft); } -.project-icon-link:hover { - color: var(--primary-color); +.project-icon-link:hover, +.project-icon-link:focus-visible { + color: var(--primary-500); + background: rgba(79, 195, 247, 0.12); } .project-desc { - font-size: 0.98rem; + font-size: var(--text-sm); color: var(--text-color-light); font-weight: 400; line-height: 1.5; } - +/* ------------------------------------------------------------------ */ +/* Responsive */ +/* ------------------------------------------------------------------ */ @media (max-width: 1024px) { .projects-section { flex: unset; max-width: 100%; margin-left: 0; - margin-top: 24px; + margin-top: var(--space-xl); min-width: unset; } } @media (max-width: 768px) { .projects-section { - padding: 28px 20px; - gap: 16px; + padding: var(--space-xl) var(--space-md); + gap: var(--space-md); } - + .projects-title { - font-size: 1.5rem; + font-size: var(--text-md); text-align: center; - margin-bottom: 12px; + margin-bottom: var(--space-sm); } } @media (max-width: 480px) { .projects-section { - padding: 24px 16px; - gap: 14px; + padding: var(--space-lg) var(--space-md); + gap: var(--space-sm); } - + .projects-title { - font-size: 1.4rem; + font-size: var(--text-sm); } - + .project { - padding: 16px 14px; + padding: var(--space-md) var(--space-sm) var(--space-md) var(--space-md); } - + .project-title { - font-size: 1.1rem; + font-size: var(--text-sm); } - + .project-desc { - font-size: 0.95rem; + font-size: var(--text-xs); } - + .project-icons { - gap: 6px; + gap: var(--space-2xs); } } @media (max-width: 380px) { .project { - padding: 14px 12px; + padding: var(--space-sm) var(--space-xs) var(--space-sm) var(--space-sm); } - + .project-title { - font-size: 1.05rem; - /* Keep a single row layout even on very small screens */ + font-size: var(--text-xs); flex-direction: row; align-items: center; - gap: 8px; + gap: var(--space-xs); } -} \ No newline at end of file +} diff --git a/css/variables.css b/css/variables.css index d13944a..ea672ac 100644 --- a/css/variables.css +++ b/css/variables.css @@ -1,33 +1,115 @@ - :root { + /* ------------------------------------------------------------------ */ + /* Color palette */ + /* Refined from the original neon set: slightly desaturated cyan / */ + /* indigo / violet so the surface reads "premium dark" rather than */ + /* "neon". Each accent has a 3-step scale for hover / active states. */ + /* ------------------------------------------------------------------ */ --primary-color: #4fc3f7; --primary-color-light: #81d4fa; + --primary-400: #6fd0fb; + --primary-500: #4fc3f7; + --primary-600: #2aa7e6; + --secondary-color: #667eea; + --secondary-400: #889aef; + --secondary-500: #667eea; + --secondary-600: #4f63d8; + --tertiary-color: #764ba2; - - --button-gradient: linear-gradient(120deg, rgba(30, 10, 50, 0.5), rgba(10, 10, 40, 0.4)); - --button-hover-gradient: linear-gradient(120deg, rgba(30, 10, 50, 0.75), rgba(10, 10, 40, 0.4)); + --tertiary-400: #8e63bd; + --tertiary-500: #764ba2; + --tertiary-600: #5d3784; + /* Unified accent gradient + glow reused everywhere */ + --accent-gradient: linear-gradient(120deg, var(--primary-500) 0%, var(--secondary-500) 50%, var(--tertiary-500) 100%); + --accent-gradient-text: linear-gradient(135deg, var(--primary-500) 0%, var(--secondary-500) 60%, var(--tertiary-400) 100%); + --accent-glow: rgba(79, 195, 247, 0.35); + --accent-glow-soft: rgba(118, 75, 162, 0.22); + --accent-glow-ring: 0 0 0 1px rgba(79, 195, 247, 0.45), 0 0 18px -2px rgba(79, 195, 247, 0.45); + + /* Surfaces / glass */ --background-color: #000000; - --glass-background: rgba(4, 0, 10, 0.32); - --glass-border: rgba(255, 255, 255, 0.13); - --glass-shadow: rgba(31, 38, 135, 0.30); + --glass-background: rgba(6, 4, 14, 0.34); + --glass-border: rgba(255, 255, 255, 0.14); + --glass-border-strong: rgba(255, 255, 255, 0.20); + --glass-shadow: rgba(31, 38, 135, 0.36); + --surface-raised: rgba(255, 255, 255, 0.045); + --surface-raised-hover: rgba(255, 255, 255, 0.075); + --surface-divider: rgba(255, 255, 255, 0.08); + + /* Text */ --text-color: #ffffff; - --text-color-light: rgba(255, 255, 255, 0.8); - --text-color-dark: rgba(255, 255, 255, 0.45); + --text-color-light: rgba(255, 255, 255, 0.82); + --text-color-mid: rgba(255, 255, 255, 0.62); + --text-color-dark: rgba(255, 255, 255, 0.52); + /* Buttons */ + --button-gradient: linear-gradient(120deg, rgba(30, 10, 50, 0.45), rgba(10, 12, 36, 0.36)); + --button-hover-gradient: linear-gradient(120deg, rgba(40, 16, 66, 0.7), rgba(12, 14, 44, 0.5)); + + /* ------------------------------------------------------------------ */ + /* Typography */ + /* ------------------------------------------------------------------ */ --font-family: "Lexend", sans-serif; + --font-mono: "JetBrains Mono", "SF Mono", "Cascadia Mono", ui-monospace, monospace; - --border-radius: 16px; - --border-radius-large: 24px; + --text-xs: clamp(0.72rem, 0.70rem + 0.1vw, 0.78rem); + --text-sm: clamp(0.84rem, 0.82rem + 0.1vw, 0.9rem); + --text-base: clamp(0.95rem, 0.92rem + 0.15vw, 1.05rem); + --text-md: clamp(1.05rem, 1rem + 0.2vw, 1.2rem); + --text-lg: clamp(1.25rem, 1.15rem + 0.5vw, 1.5rem); + --text-xl: clamp(1.6rem, 1.3rem + 1.5vw, 2rem); + --text-2xl: clamp(2rem, 1.6rem + 2.2vw, 2.8rem); + --text-3xl: clamp(2.4rem, 1.9rem + 2.8vw, 3.2rem); - --box-shadow: 0 8px 36px 0px var(--glass-shadow); - --inset-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); - /* semi-transparent helpers for animated shadows */ - --primary-color-16: rgba(55, 187, 248, 0.16); - --tertiary-color-20: rgba(106, 50, 161, 0.2); - --dark-shadow-15: rgba(0,0,0,0.15); - --dark-shadow-18: rgba(0,0,0,0.18); - --dark-shadow-20: rgba(0,0,0,0.20); + /* ------------------------------------------------------------------ */ + /* Spacing scale */ + /* ------------------------------------------------------------------ */ + --space-2xs: 0.25rem; + --space-xs: 0.5rem; + --space-sm: 0.75rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2rem; + --space-2xl: 2.75rem; + --space-3xl: 3.5rem; + + /* ------------------------------------------------------------------ */ + /* Radii */ + /* ------------------------------------------------------------------ */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 24px; + --radius-pill: 999px; + + --border-radius: var(--radius-lg); + --border-radius-large: var(--radius-xl); + + /* ------------------------------------------------------------------ */ + /* Shadow tiers */ + /* ------------------------------------------------------------------ */ + --shadow-sm: 0 2px 8px -2px rgba(0, 0, 0, 0.4); + --shadow-md: 0 8px 24px -6px var(--glass-shadow); + --shadow-lg: 0 18px 48px -12px rgba(0, 0, 0, 0.55), 0 4px 16px -4px rgba(0, 0, 0, 0.35); + --inset-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); + --shadow-glow-accent: 0 8px 24px -6px var(--accent-glow), 0 0 0 1px rgba(79, 195, 247, 0.18); + + /* Semi-transparent helpers for animated shadows */ + --primary-color-16: rgba(79, 195, 247, 0.16); + --tertiary-color-20: rgba(118, 75, 162, 0.2); + --dark-shadow-15: rgba(0, 0, 0, 0.15); + --dark-shadow-18: rgba(0, 0, 0, 0.18); + --dark-shadow-20: rgba(0, 0, 0, 0.20); + + /* ------------------------------------------------------------------ */ + /* Motion */ + /* ------------------------------------------------------------------ */ + --dur-fast: 0.18s; + --dur-med: 0.3s; + --dur-slow: 0.55s; + --ease-out-soft: cubic-bezier(0.22, 0.61, 0.36, 1); + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); } diff --git a/index.html b/index.html index c9b715e..8482595 100644 --- a/index.html +++ b/index.html @@ -61,26 +61,27 @@

ivanch

-

- .NET Core | DevOps | Self-hosted Enthusiast -
- Life is more than just a read-through 🎶 -

+
+ .NET Core + DevOps + Self-hosted +
+ Life is more than just a read-through 🎵 @@ -89,8 +90,8 @@

Homelab

@@ -100,10 +101,10 @@
OpenCand - + - + @@ -114,7 +115,7 @@
Vision Start - + @@ -125,7 +126,7 @@
Kasbot - + @@ -136,7 +137,7 @@
Candlebright - + diff --git a/js/matrix.js b/js/matrix.js index 5d6159a..7c0de6f 100644 --- a/js/matrix.js +++ b/js/matrix.js @@ -4,6 +4,11 @@ class MatrixBackground { this.ctx = this.canvas.getContext('2d'); this.dots = []; this.connections = []; + this.mouse = { x: -9999, y: -9999, active: false }; + this.rafId = null; + this.isRunning = true; + this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + this.config = { dotCount: this.getMobileDotCount(), dotSpeed: 0.2, @@ -11,15 +16,18 @@ class MatrixBackground { connectionChance: 0.0002, maxConnections: this.getMobileMaxConnections(), dotSize: 3, + mouseRadius: 110, + mouseForce: 0.6, + // Palette synced to the UI accent tokens colors: { - dotDefault: 'rgba(255, 255, 255, 0.8)', - dotDefaultGlow: 'rgba(255, 255, 255, 0.4)', - dotConnected: 'rgba(100, 149, 237, 0.8)', - dotConnectedGlow: 'rgba(100, 149, 237, 0.6)', + dotDefault: 'rgba(230, 240, 255, 0.78)', + dotDefaultGlow: 'rgba(180, 220, 255, 0.40)', + dotConnected: 'rgba(120, 190, 245, 0.85)', + dotConnectedGlow: 'rgba(79, 195, 247, 0.55)', connection: { - start: 'rgba(100, 149, 237, 0.2)', - middle: 'rgba(138, 43, 226, 0.6)', - end: 'rgba(100, 149, 237, 0.2)' + start: 'rgba(102, 126, 234, 0.22)', + middle: 'rgba(118, 75, 162, 0.62)', + end: 'rgba(79, 195, 247, 0.22)' } } }; @@ -28,14 +36,13 @@ class MatrixBackground { } getMobileDotCount() { - // Reduce dot count on mobile devices for better performance - if (window.innerWidth <= 480) return 25; - if (window.innerWidth <= 768) return 35; + if (window.innerWidth <= 480) return 35; + if (window.innerWidth <= 768) return 45; + if (window.innerWidth >= 2100) return 100; return 50; } getMobileMaxConnections() { - // Reduce max connections on mobile devices for better performance if (window.innerWidth <= 480) return 4; if (window.innerWidth <= 768) return 6; return 8; @@ -43,20 +50,61 @@ class MatrixBackground { init() { this.handleResize(); + this.bindEvents(); this.animate(); + } - // Prevent resizing for mobile performance reasons - // window.addEventListener('resize', () => this.handleResize()); + bindEvents() { + // Pointer reactivity — push dots away from cursor for a "matrix repel" feel. + // Touch devices are included; the dot pushes happen only while touched. + window.addEventListener('pointermove', (e) => { + this.mouse.x = e.clientX; + this.mouse.y = e.clientY; + this.mouse.active = true; + }, { passive: true }); + + window.addEventListener('pointerout', () => { + this.mouse.active = false; + this.mouse.x = -9999; + this.mouse.y = -9999; + }); + + // Pause the rAF loop when the tab is hidden to save CPU / battery. + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + this.stop(); + } else { + this.start(); + } + }); + + // Resize is debounced via rAF; disabled on mobile for perf parity with original. + if (window.innerWidth > 768) { + let resizeRaf = null; + window.addEventListener('resize', () => { + if (resizeRaf) cancelAnimationFrame(resizeRaf); + resizeRaf = requestAnimationFrame(() => this.handleResize()); + }); + } + } + + start() { + if (this.rafId || !this.isRunning) return; + this.animate(); + } + + stop() { + if (this.rafId) { + cancelAnimationFrame(this.rafId); + this.rafId = null; + } } handleResize() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; - - // Update mobile-specific configurations this.config.dotCount = this.getMobileDotCount(); this.config.maxConnections = this.getMobileMaxConnections(); - this.createDots(); } @@ -71,51 +119,54 @@ class MatrixBackground { const startX = Math.random() * (this.canvas.width - 20) + 10; const startY = Math.random() * (this.canvas.height - 20) + 10; - let velocityX = 0, velocityY = 0; - let attempts = 0; - while ((Math.abs(velocityX) < 0.15 || Math.abs(velocityY) < 0.15) && attempts < 10) { - const angle = Math.random() * Math.PI * 2; - const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4); - velocityX = Math.cos(angle) * speed; - velocityY = Math.sin(angle) * speed; - attempts++; - } - if (Math.abs(velocityX) < 0.15) velocityX = velocityX < 0 ? -0.15 : 0.15; - if (Math.abs(velocityY) < 0.15) velocityY = velocityY < 0 ? -0.15 : 0.15; + const angle = Math.random() * Math.PI * 2; + const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4); + const baseVelocityX = Math.cos(angle) * speed; + const baseVelocityY = Math.sin(angle) * speed; this.dots.push({ x: startX, y: startY, - vx: velocityX, - vy: velocityY, + vx: baseVelocityX, + vy: baseVelocityY, + baseVx: baseVelocityX, + baseVy: baseVelocityY, + wanderPhase: Math.random() * Math.PI * 2, + wanderSpeed: 0.01 + Math.random() * 0.01, opacity: Math.random() * 0.3 + 0.7, size: this.config.dotSize + Math.random() * 2, - connectionCount: 0, - stuckFrames: 0 + connectionCount: 0 }); } updateDots() { + const radius = this.config.mouseRadius; + const radiusSq = radius * radius; + const force = this.config.mouseForce; + this.dots.forEach(dot => { + // Mouse repulsion + if (this.mouse.active) { + const dx = dot.x - this.mouse.x; + const dy = dot.y - this.mouse.y; + const distSq = dx * dx + dy * dy; + if (distSq < radiusSq && distSq > 0.5) { + const dist = Math.sqrt(distSq); + const strength = (1 - dist / radius) * force; + dot.vx += (dx / dist) * strength; + dot.vy += (dy / dist) * strength; + } + } + dot.x += dot.vx; dot.y += dot.vy; - if (Math.abs(dot.vx) < 0.03 && Math.abs(dot.vy) < 0.03) { - dot.stuckFrames++; - } else { - dot.stuckFrames = 0; - } - - if (dot.stuckFrames > 30) { - const angle = Math.random() * Math.PI * 2; - const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4); - dot.vx = Math.cos(angle) * speed; - dot.vy = Math.sin(angle) * speed; - dot.stuckFrames = 0; - } else { - if (Math.abs(dot.vx) < 0.05) dot.vx += (Math.random() - 0.5) * 0.1; - if (Math.abs(dot.vy) < 0.05) dot.vy += (Math.random() - 0.5) * 0.1; - } + // Ease back to a gentle autonomous drift after pointer interaction. + dot.wanderPhase += dot.wanderSpeed; + const wanderX = Math.cos(dot.wanderPhase) * 0.03; + const wanderY = Math.sin(dot.wanderPhase * 0.8) * 0.03; + dot.vx += (dot.baseVx + wanderX - dot.vx) * 0.02; + dot.vy += (dot.baseVy + wanderY - dot.vy) * 0.02; const maxX = this.canvas.width - dot.size; const maxY = this.canvas.height - dot.size; @@ -175,7 +226,7 @@ class MatrixBackground { this.connections.forEach(conn => { const { dot1, dot2, startTime, duration } = conn; const elapsed = Date.now() - startTime; - const opacity = Math.min(1, elapsed / 500); // Fade in + const opacity = Math.min(1, elapsed / 500); const gradient = this.ctx.createLinearGradient(dot1.x, dot1.y, dot2.x, dot2.y); gradient.addColorStop(0, this.config.colors.connection.start); @@ -185,15 +236,14 @@ class MatrixBackground { this.ctx.beginPath(); this.ctx.moveTo(dot1.x, dot1.y); this.ctx.lineTo(dot2.x, dot2.y); - + this.ctx.strokeStyle = gradient; this.ctx.lineWidth = 2; this.ctx.globalAlpha = opacity; - // Pulsing effect - const pulse = (Math.sin((elapsed / 2000) * Math.PI * 2) + 1) / 2; // 2-second pulse cycle + const pulse = (Math.sin((elapsed / 2000) * Math.PI * 2) + 1) / 2; this.ctx.shadowBlur = 4 + pulse * 6; - this.ctx.shadowColor = 'rgba(138, 43, 226, 0.4)'; + this.ctx.shadowColor = 'rgba(118, 75, 162, 0.5)'; this.ctx.stroke(); }); @@ -229,17 +279,19 @@ class MatrixBackground { animate() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - this.updateDots(); - this.updateConnections(); - this.tryCreateConnections(); + if (!this.prefersReducedMotion) { + this.updateDots(); + this.updateConnections(); + this.tryCreateConnections(); + } this.drawConnections(); this.drawDots(); - requestAnimationFrame(() => this.animate()); + this.rafId = requestAnimationFrame(() => this.animate()); } } document.addEventListener('DOMContentLoaded', () => { new MatrixBackground(); -}); \ No newline at end of file +}); diff --git a/js/social.js b/js/social.js index 4650c41..0a956cb 100644 --- a/js/social.js +++ b/js/social.js @@ -1,7 +1,26 @@ document.addEventListener('DOMContentLoaded', () => { - new MatrixBackground(); + // ---------------------------------------------------------------- + // Avatar glitch layers — clone the avatar image twice (R + B + // channel shift clones) so the CSS hover animation has elements + // to slice/displace. Skipped when the user prefers reduced motion. + // ---------------------------------------------------------------- + const avatar = document.querySelector('.avatar'); + const avatarImg = avatar && avatar.querySelector('img'); + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (avatar && avatarImg && !prefersReducedMotion) { + ['r', 'b'].forEach((channel) => { + const clone = avatarImg.cloneNode(false); + clone.classList.add('glitch-layer', channel); + clone.removeAttribute('alt'); + clone.setAttribute('aria-hidden', 'true'); + avatar.appendChild(clone); + }); + } + + // ---------------------------------------------------------------- // Email copy-to-clipboard and tooltip logic + // ---------------------------------------------------------------- const emailLink = document.getElementById('email-link'); const emailTooltip = document.getElementById('email-tooltip'); const obfuscatedEmail = 'Bg4aC0IJDAAeCBgbCU8AGA0PKQkBAAACQgIGAw=='; // generated through utils/xor-enc.py @@ -20,39 +39,61 @@ document.addEventListener('DOMContentLoaded', () => { const emailAddress = decodeEmail(obfuscatedEmail, xorKey); if (emailLink && emailTooltip) { - emailLink.addEventListener('click', function(e) { - e.preventDefault(); - // Copy email to clipboard - if (navigator.clipboard) { - navigator.clipboard.writeText(emailAddress); - } else { - // Fallback for older browsers - const textarea = document.createElement('textarea'); - textarea.value = emailAddress; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); + let tooltipTimeout; + + async function copyEmail() { + if (navigator.clipboard && navigator.clipboard.writeText) { + try { + await navigator.clipboard.writeText(emailAddress); + return true; + } catch (error) { + // Fall through to the legacy copy path when clipboard permissions fail. + } } - + + const textarea = document.createElement('textarea'); + textarea.value = emailAddress; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + + let copied = false; + try { + copied = document.execCommand('copy'); + } catch (error) { + copied = false; + } + document.body.removeChild(textarea); + return copied; + } + + emailLink.addEventListener('click', async function(e) { + e.preventDefault(); + + const copied = await copyEmail(); + if (!copied) return; + // Position tooltip above email link const emailRect = emailLink.getBoundingClientRect(); const socialLinksRect = emailLink.parentElement.getBoundingClientRect(); - + const left = emailRect.left - socialLinksRect.left + (emailRect.width / 2); const top = emailRect.top - socialLinksRect.top - 10; - + emailTooltip.style.left = left + 'px'; emailTooltip.style.top = top + 'px'; emailTooltip.style.transform = 'translateX(-50%) translateY(-75%)'; - - // Show tooltip - emailTooltip.style.opacity = '1'; - + + // Show tooltip only after the copy operation succeeds. + emailTooltip.classList.add('is-visible'); + // Hide after 1.5s - setTimeout(() => { - emailTooltip.style.opacity = '0'; + clearTimeout(tooltipTimeout); + tooltipTimeout = setTimeout(() => { + emailTooltip.classList.remove('is-visible'); }, 1500); }); } -}); \ No newline at end of file +}); diff --git a/nginx.conf b/nginx.conf index 8887993..4fe72e2 100644 --- a/nginx.conf +++ b/nginx.conf @@ -23,9 +23,24 @@ http { tcp_nopush on; tcp_nodelay on; keepalive_timeout 65; + keepalive_requests 100; types_hash_max_size 2048; client_max_body_size 20M; + # Buffers & Timeouts tuning + client_body_buffer_size 16k; + client_header_buffer_size 1k; + large_client_header_buffers 4 8k; + client_body_timeout 12s; + client_header_timeout 12s; + send_timeout 10s; + + # Open File Cache + open_file_cache max=1000 inactive=20s; + open_file_cache_valid 30s; + open_file_cache_min_uses 2; + open_file_cache_errors on; + # Gzip compression gzip on; gzip_vary on; @@ -40,19 +55,29 @@ http { application/javascript application/x-javascript application/xml+rss - image/svg+xml; + image/svg+xml + image/x-icon + application/wasm + font/woff + font/woff2 + application/vnd.ms-fontobject + application/x-font-ttf; gzip_min_length 256; gzip_disable "msie6"; - # Security headers + # Security headers (Global default) add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; + # HTTP (port 80) Server Block server { listen 80; - server_name localhost; + listen [::]:80; + server_name localhost ivanch.me www.ivanch.me; root /usr/share/nginx/html; index index.html; @@ -60,30 +85,48 @@ http { charset utf-8; # Custom error pages - error_page 404 /index.html; + error_page 404 /404.html; error_page 500 502 503 504 /50x.html; # Main location location / { - try_files $uri $uri/ /index.html; + try_files $uri $uri/ =404; } - # Cache static assets + # Cache static assets & redeclare security headers to bypass Nginx header override gotcha location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ { expires 1y; add_header Cache-Control "public, immutable"; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; access_log off; } location ~* \.(css|js)$ { expires 1y; add_header Cache-Control "public, immutable"; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; access_log off; } location ~* \.(woff|woff2|ttf|otf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; access_log off; } @@ -91,6 +134,12 @@ http { location ~* \.html$ { expires 1h; add_header Cache-Control "public, must-revalidate"; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; } # Deny access to hidden files @@ -108,3 +157,4 @@ http { } } } +