diff --git a/css/base.css b/css/base.css index 1ff230e..9fae2d7 100644 --- a/css/base.css +++ b/css/base.css @@ -1,252 +1,268 @@ +/* ========================================================= + BASE — Brad Cooley Portfolio + Design System: Japanese Minimalism × Swiss Typography + ========================================================= */ + +/* ── Custom Properties: Light Mode ─────────────────────── */ :root { - /* Color system */ - --text-color: #1e293b; - --text-color-dark: #f1f5f9; - --text-color-secondary: #64748b; - --background-color: #e2e8f0; - --background-color-dark: #0f172a; - - /* Glass effect variables */ - --glass-primary: rgba(255, 255, 255, 0.4); - --glass-primary-dark: rgba(15, 23, 42, 0.4); - --glass-secondary: rgba(255, 255, 255, 0.2); - --glass-secondary-dark: rgba(15, 23, 42, 0.2); - --glass-border: rgba(255, 255, 255, 0.5); - --glass-border-dark: rgba(255, 255, 255, 0.15); - --glass-shadow: 0 8px 32px rgba(31, 38, 135, 0.25); - --glass-shadow-dark: 0 8px 32px rgba(0, 0, 0, 0.4); - - /* Gradients */ - --accent-gradient: linear-gradient( - 135deg, - #3b82f6 0%, - #8b5cf6 50%, - #ec4899 100% - ); - --accent-gradient-dark: linear-gradient( - 135deg, - #4f46e5 0%, - #7c3aed 50%, - #ec4899 100% - ); - --gradient-colors: linear-gradient( - 45deg, - #6366f1 0%, - #8b5cf6 33%, - #ec4899 66%, - #f59e0b 100% - ); + /* Color */ + --bg: #F8F7F4; + --bg-raised: #FFFFFF; + --ink: #141412; + --ink-2: #6B6A67; + --ink-3: #B4B2AE; + --line: #E5E3DF; + --accent: #1246F0; + --accent-sub: rgba(18, 70, 240, 0.06); + --cosmic-orange: #F07218; + --cosmic-orange-deep: #BF4800; + --section-title-color: var(--cosmic-orange); /* Typography */ - --font-heading: "Poppins", -apple-system, BlinkMacSystemFont, "SF Pro Display", - "Helvetica Neue", sans-serif; - --font-body: "Lato", -apple-system, BlinkMacSystemFont, "SF Pro Text", - "Helvetica Neue", sans-serif; - - /* Transitions and timing */ - --transition-fast: 0.2s ease; - --transition-smooth: 0.3s ease; - --transition-slow: 0.6s cubic-bezier(0.25, 0.1, 0.25, 1); - --animation-duration: 8s; -} - -/* Modern CSS reset */ -*, -*::before, -*::after { + --font-display: 'Syne', sans-serif; + --font-body: 'DM Sans', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + + /* Type scale */ + --text-xs: 0.6875rem; + --text-sm: 0.8125rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.375rem; + --text-2xl: 1.75rem; + --text-3xl: 2.25rem; + --text-4xl: 3rem; + --text-5xl: 4rem; + --text-hero: clamp(4.5rem, 10vw, 10rem); + + /* Spacing (4px base) */ + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 20px; + --sp-6: 24px; + --sp-8: 32px; + --sp-10: 40px; + --sp-12: 48px; + --sp-16: 64px; + --sp-20: 80px; + --sp-24: 96px; + + /* Motion */ + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + --t-fast: 120ms; + --t-base: 280ms; + --t-slow: 500ms; + --t-enter: 700ms; + --t-section: 600ms; +} + +/* ── Dark / Light via data-theme attribute ──────────────── */ +/* Applied before paint by inline script to prevent flash */ +[data-theme="light"] { + --bg: #F8F7F4; + --bg-raised: #FFFFFF; + --ink: #141412; + --ink-2: #6B6A67; + --ink-3: #B4B2AE; + --line: #E5E3DF; + --accent: #1246F0; + --accent-sub: rgba(18, 70, 240, 0.06); + --cosmic-orange: #F07218; + --cosmic-orange-deep: #BF4800; + --cosmic-orange-sub: rgba(240, 114, 24, 0.08); + --section-title-color: var(--cosmic-orange); +} + +[data-theme="dark"] { + --bg: #0F0F0E; + --bg-raised: #1A1A18; + --ink: #F0EFE9; + --ink-2: #8A8883; + --ink-3: #555350; + --line: #2D2C2A; + --accent: #4F7BF7; + --accent-sub: rgba(79, 123, 247, 0.08); + --cosmic-orange: #CC6420; + --cosmic-orange-deep: #A04200; + --cosmic-orange-sub: rgba(204, 100, 32, 0.10); + --section-title-color: var(--cosmic-orange-deep); +} + +/* Fallback for browsers without JS / no stored preference */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme]) { + --bg: #0F0F0E; + --bg-raised: #1A1A18; + --ink: #F0EFE9; + --ink-2: #8A8883; + --ink-3: #555350; + --line: #2D2C2A; + --accent: #4F7BF7; + --accent-sub: rgba(79, 123, 247, 0.08); + --cosmic-orange: #CC6420; + --cosmic-orange-deep: #A04200; + --cosmic-orange-sub: rgba(204, 100, 32, 0.10); + } +} + +/* ── Smooth theme transition class ──────────────────────── */ +/* Added briefly by JS during theme toggle */ +.is-theme-transitioning, +.is-theme-transitioning * { + transition: + background-color 500ms cubic-bezier(0.16, 1, 0.3, 1), + color 500ms cubic-bezier(0.16, 1, 0.3, 1), + border-color 500ms cubic-bezier(0.16, 1, 0.3, 1), + box-shadow 500ms cubic-bezier(0.16, 1, 0.3, 1) !important; +} + +/* ── Reset ──────────────────────────────────────────────── */ +*, *::before, *::after { + box-sizing: border-box; margin: 0; padding: 0; - box-sizing: border-box; } html { - height: 100%; font-size: 16px; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; } body { - height: 100%; - overflow: hidden; font-family: var(--font-body); + background: var(--bg); + color: var(--ink); line-height: 1.6; - color: var(--text-color); - background: var(--background-color); - position: relative; - transition: var(--transition-smooth); + height: 100%; + overflow: hidden; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; } -/* Background system - Static gradient base */ -body::before { - content: ""; - position: fixed; - inset: 0; - background: var(--accent-gradient); - z-index: -2; +/* Hide default cursor on fine-pointer (mouse) devices */ +@media (pointer: fine) { + * { cursor: none !important; } } -/* Background overlay for glass effect */ -body::after { - content: ""; +/* ── Custom Cursor ──────────────────────────────────────── */ +.cursor { + display: none; position: fixed; - inset: 0; - background: var(--background-color); - opacity: 0.75; - z-index: -1; + top: 0; + left: 0; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--ink); + pointer-events: none; + z-index: 99999; + will-change: transform; + transition: + width var(--t-base) var(--ease-out), + height var(--t-base) var(--ease-out), + background var(--t-base) var(--ease-out), + box-shadow var(--t-base) var(--ease-out), + opacity var(--t-fast); } -/* Core animations */ -@keyframes textGradient { - 0%, - 100% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } +@media (pointer: fine) { + .cursor { display: block; } } -@keyframes fadeInUp { - from { - opacity: 0; - transform: translateY(40px); - } - to { - opacity: 1; - transform: translateY(0); - } +.cursor.is-hovering { + width: 36px; + height: 36px; + background: transparent; + box-shadow: inset 0 0 0 1.5px var(--ink); } -@keyframes gradientShift { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } +.cursor.is-dragging { + width: 52px; + height: 52px; + background: transparent; + box-shadow: inset 0 0 0 1px var(--ink); + opacity: 0.45; } -/* Utility classes for common patterns */ -.gradient-text { - background: var(--gradient-colors); - background-size: 200% 200%; - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - animation: textGradient var(--animation-duration) ease infinite; +/* ── Grain Texture Overlay ──────────────────────────────── */ +.grain { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9998; + opacity: 0.032; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + background-size: 180px; + background-repeat: repeat; } -.glass-effect { - background: var(--glass-primary); - backdrop-filter: blur(25px); - -webkit-backdrop-filter: blur(25px); - border: 1px solid var(--glass-border); - box-shadow: var(--glass-shadow); +/* ── Keyframes ──────────────────────────────────────────── */ +@keyframes fadeUp { + from { opacity: 0; transform: translateY(18px); } + to { opacity: 1; transform: translateY(0); } } -.glass-effect-secondary { - background: var(--glass-secondary); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - border: 1px solid var(--glass-border); +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } } -/* Dark mode support */ -@media (prefers-color-scheme: dark) { - :root { - --text-color: var(--text-color-dark); - --background-color: var(--background-color-dark); - --glass-primary: var(--glass-primary-dark); - --glass-secondary: var(--glass-secondary-dark); - --glass-border: var(--glass-border-dark); - --glass-shadow: var(--glass-shadow-dark); - --accent-gradient: var(--accent-gradient-dark); - } - - body::after { - background: var(--background-color-dark); - opacity: 0.85; - } +@keyframes lineExpand { + from { transform: scaleX(0); } + to { transform: scaleX(1); } } -/* Accessibility improvements */ -@media (prefers-reduced-motion: reduce) { - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } - - .scroll-hint { - display: none; - } +/* Name breathing: mostly --ink, quietly visits --accent, returns */ +@keyframes nameColor { + 0%, 30% { color: var(--ink); } + 50% { color: var(--accent); } + 70%, 100% { color: var(--ink); } } -/* High contrast mode support */ -@media (prefers-contrast: high) { - :root { - --glass-border: rgba(255, 255, 255, 0.8); - --glass-border-dark: rgba(255, 255, 255, 0.6); - } - - .nav-item:focus, - .social-link:focus, - .scroll-menu-item:focus { - outline: 3px solid currentColor; - outline-offset: 2px; - } +/* Cosmic Orange gradient sweeps left→right, holds, returns */ +@keyframes nameShimmer { + 0%, 8% { background-position: 100% center; } + 40%, 60% { background-position: 0% center; } + 92%, 100% { background-position: 100% center; } } -/* Print styles */ -@media print { - body::before, - body::after { - display: none; - } - - .bottom-nav, - .scroll-indicator, - .scroll-menu, - .scroll-hint { - display: none; - } - - .section { - page-break-inside: avoid; - } +/* ── Utility ────────────────────────────────────────────── */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; } -/* Focus management for keyboard users */ -.js-focus-visible :focus:not(.focus-visible) { - outline: none; +:focus-visible { + outline: 2px solid var(--cosmic-orange); + outline-offset: 3px; + border-radius: 2px; } -/* Smooth scrolling for supported browsers */ -@supports (scroll-behavior: smooth) { - html { - scroll-behavior: smooth; +/* ── Reduced Motion ─────────────────────────────────────── */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; } + .cursor { display: none !important; } } -/* Modern backdrop-filter fallback */ -@supports not (backdrop-filter: blur()) { - .glass-effect, - .glass-effect-secondary { - background: rgba(255, 255, 255, 0.9); - } - - @media (prefers-color-scheme: dark) { - .glass-effect, - .glass-effect-secondary { - background: rgba(15, 23, 42, 0.9); - } +/* ── Print ──────────────────────────────────────────────── */ +@media print { + .cursor, .grain, .progress-bar, + .bottom-nav, .scroll-indicator, .scroll-menu, .scroll-hint { + display: none !important; } } diff --git a/css/desktop.css b/css/desktop.css index c700940..37c11da 100644 --- a/css/desktop.css +++ b/css/desktop.css @@ -1,371 +1,394 @@ +/* ========================================================= + DESKTOP — Brad Cooley Portfolio + ≥ 768px · Horizontal Scroll Navigation + ========================================================= */ + @media (min-width: 768px) { - .mobile-portrait-layout, - .mobile-landscape-layout { + /* ── Layout ─────────────────────────────────────────── */ + .mobile-portrait-layout { display: none; } .desktop-layout { display: block; + width: 100vw; + height: 100vh; + overflow: hidden; } - /* Main container and sections */ .app-container { height: 100vh; width: 100vw; - position: relative; overflow: hidden; } + /* ── Sections Container ─────────────────────────────── */ .sections-container { - height: 100vh; - width: 500vw; display: flex; - transition: var(--transition-slow); - will-change: transform; + width: 500vw; + height: calc(100vh - 56px); transform: translateX(0); - cursor: grab; + transition: transform var(--t-section) var(--ease-out); + will-change: transform; user-select: none; } - .sections-container:active { - cursor: grabbing; - } - .sections-container.no-transition { transition: none; } - .sections-container.at-start { - cursor: e-resize; - } - - .sections-container.at-end { - cursor: w-resize; - } - + /* ── Individual Section ─────────────────────────────── */ .section { width: 100vw; - height: 100vh; + height: 100%; display: flex; align-items: center; - justify-content: center; - text-align: center; - padding: 2rem; - position: relative; + justify-content: flex-start; flex-shrink: 0; - pointer-events: auto; - transform: translateY(-2rem); - } - - /* Glass containers */ - .glass-container { - background: var(--glass-primary); - backdrop-filter: blur(25px); - -webkit-backdrop-filter: blur(25px); - border-radius: 32px; - border: 1px solid var(--glass-border); - box-shadow: var(--glass-shadow); position: relative; overflow: hidden; - transition: var(--transition-smooth); + padding: var(--sp-10) clamp(var(--sp-10), 8vw, var(--sp-24)); } - .glass-container::before { - content: ""; + /* ── Ghost Section Number ───────────────────────────── */ + .section-ghost-num { position: absolute; - inset: 0; - border-radius: 32px; - padding: 1px; - background: var(--accent-gradient); - mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - mask-composite: subtract; - -webkit-mask-composite: subtract; - opacity: 0.6; + bottom: var(--sp-6); + right: clamp(var(--sp-10), 6vw, var(--sp-20)); + font-family: var(--font-display); + font-size: clamp(7rem, 14vw, 16rem); + font-weight: 800; + color: var(--ink); + opacity: 0.038; + line-height: 1; + user-select: none; pointer-events: none; + letter-spacing: -0.04em; } - .home-glass, - .section-glass { - padding: 4rem 3rem; - max-width: 700px; - width: 90%; - margin: 0 auto; - min-height: 500px; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - - /* Home section specific styles */ + /* ── Home Section ───────────────────────────────────── */ .home-content { - opacity: 0; - animation: fadeInUp 1.2s ease-out forwards; - text-align: center; - width: 100%; + width: min(960px, 90%); display: flex; flex-direction: column; - justify-content: space-between; - align-items: center; - height: 100%; - min-height: 400px; - } - - .name-display { - margin-bottom: 2rem; - } - - .name-display h1 { - font-family: var(--font-heading); - font-size: clamp(4rem, 12vw, 8rem); - font-weight: 700; - letter-spacing: -0.03em; - text-rendering: optimizeLegibility; - background: var(--gradient-colors); - background-size: 200% 200%; + gap: var(--sp-6); + } + + .home-name h1 { + font-family: var(--font-display); + font-size: var(--text-hero); + font-weight: 800; + line-height: 0.91; + letter-spacing: -0.04em; + /* Cosmic Orange fades left→right into background; metallic noise + grain is layered on top via SVG feTurbulence, both clipped to text. + mask-image fades the whole rendering so noise opacity tracks the + color gradient — grain vanishes exactly where the color does. */ + background: + url("data:image/svg+xml,") + repeat, + linear-gradient( + to right, + var(--cosmic-orange) 0%, + var(--cosmic-orange-deep) 20%, + var(--bg) 94% + ); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; - animation: textGradient 8s ease infinite, fadeInUp 1.2s ease-out forwards; - margin-bottom: 0.5rem; - line-height: 0.9; + -webkit-mask-image: linear-gradient(to right, black 0%, transparent 94%); + mask-image: linear-gradient(to right, black 0%, transparent 94%); + animation: fadeUp var(--t-enter) var(--ease-out) 0.1s both; } - .tagline { - font-size: clamp(1.2rem, 3vw, 1.6rem); - color: var(--text-color); - opacity: 0.8; - font-weight: 300; - margin-bottom: 2.5rem; - animation: fadeInUp 1.2s ease-out 0.3s both; + .home-rule { + width: 100%; + height: 1px; + background: var(--line); + transform-origin: left; + animation: lineExpand var(--t-slow) var(--ease-out) 0.35s both; } - .social-section { - animation: fadeInUp 1.2s ease-out 0.6s both; + .home-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--sp-8); + animation: fadeUp var(--t-enter) var(--ease-out) 0.5s both; } - /* Social links */ - .social-links { - display: flex; - gap: 1.5rem; - justify-content: center; - flex-wrap: wrap; - } - - .social-link { - background: var(--glass-secondary); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - border: 1px solid var(--glass-border); - border-radius: 20px; - padding: 1rem; - width: 60px; - height: 60px; + .home-title { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-2); + text-transform: uppercase; + letter-spacing: 0.14em; + line-height: 1; + } + + .home-links { display: flex; - align-items: center; - justify-content: center; + gap: var(--sp-6); + } + + .home-links a { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-2); text-decoration: none; - color: var(--text-color); - font-size: 1.5rem; - transition: var(--transition-smooth); + text-transform: uppercase; + letter-spacing: 0.1em; position: relative; - overflow: hidden; + padding-bottom: 1px; + transition: color var(--t-base) var(--ease-out); } - .social-link::before { + .home-links a::after { content: ""; position: absolute; - inset: 0; - background: var(--accent-gradient); - opacity: 0; - transition: var(--transition-smooth); + bottom: 0; + left: 0; + width: 100%; + height: 1px; + background: var(--cosmic-orange); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--t-slow) var(--ease-out); } - .social-link:hover::before { - opacity: 0.1; + .home-links a:hover { + color: var(--ink); } - .social-link:hover { - transform: translateY(-3px) scale(1.05); - border-color: rgba(255, 255, 255, 0.3); - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + .home-links a:hover::after { + transform: scaleX(1); } - .social-link i { - position: relative; - z-index: 1; + .link-arrow { + display: inline-block; + margin-left: 2px; + transition: transform var(--t-base) var(--ease-spring); + } + + .home-links a:hover .link-arrow { + transform: translate(2px, -2px); + } + + /* ── Section Inner (About / Projects / Writing / Resume) */ + .section-inner { + width: min(960px, 90%); + display: grid; + grid-template-columns: 1fr 1.4fr; + gap: clamp(var(--sp-12), 8vw, var(--sp-20)); + align-items: center; } - /* Section content */ - .section-content { - opacity: 1; - transform: translateY(0); + .section-label-col { display: flex; flex-direction: column; - justify-content: space-between; - align-items: center; - height: 100%; - min-height: 400px; + gap: var(--sp-5); } - .section-icon { - font-size: 5rem; - color: var(--text-color); - margin-bottom: 2rem; - opacity: 0.8; + .section-eyebrow { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-3); + text-transform: uppercase; + letter-spacing: 0.14em; } .section-title { - font-family: var(--font-heading); - font-size: clamp(2.5rem, 6vw, 3.5rem); - font-weight: 600; - margin-bottom: 1.5rem; - background: var(--gradient-colors); - background-size: 200% 200%; + font-family: var(--font-display); + font-size: clamp(var(--text-3xl), 4.8vw, var(--text-5xl)); + font-weight: 800; + line-height: 0.95; + letter-spacing: -0.03em; + background: + url("data:image/svg+xml,") + repeat, + var(--section-title-color); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; - animation: textGradient 8s ease infinite; - } - - .section-description { - font-size: clamp(1.1rem, 2.5vw, 1.3rem); - color: var(--text-color); - margin-bottom: 2.5rem; - line-height: 1.6; - opacity: 0.9; - } - - /* Coming soon badge */ - .coming-soon-badge { - display: inline-flex; - align-items: center; - gap: 0.8rem; - padding: 1rem 2rem; - background: var(--glass-secondary); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - border-radius: 50px; - color: var(--text-color); - font-weight: 500; - border: 1px solid var(--glass-border); - transition: var(--transition-smooth); - position: relative; - overflow: hidden; - } - - .coming-soon-badge::before { - content: ""; - position: absolute; - inset: 0; - background: var(--accent-gradient); - opacity: 0; - transition: var(--transition-smooth); } - .coming-soon-badge:hover::before { - opacity: 0.05; + .section-text-col { + display: flex; + flex-direction: column; + gap: var(--sp-6); + padding-top: var(--sp-2); } - .coming-soon-badge:hover { - transform: translateY(-2px); - border-color: rgba(255, 255, 255, 0.3); + .section-body-text { + font-family: var(--font-body); + font-size: clamp(var(--text-lg), 1.4vw, var(--text-xl)); + font-weight: 300; + color: var(--ink-2); + line-height: 1.75; + max-width: 480px; } - .coming-soon-badge i { - position: relative; - z-index: 1; + .section-wip { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-3); + letter-spacing: 0.06em; } - /* Bottom navigation */ + /* ── Navigation Bar ─────────────────────────────────── */ .bottom-nav { position: fixed; - bottom: 2rem; - left: 50%; - transform: translateX(-50%); + bottom: 0; + left: 0; + right: 0; + height: 56px; z-index: 1000; - background: var(--glass-primary); - backdrop-filter: blur(25px); - -webkit-backdrop-filter: blur(25px); - border-radius: 25px; - border: 1px solid var(--glass-border); - box-shadow: var(--glass-shadow); - padding: 0.8rem 1.5rem; - transition: var(--transition-smooth); + background: var(--bg); + border-top: 1px solid var(--line); + display: flex; + align-items: stretch; } .nav-container { display: flex; - gap: 0.5rem; - align-items: center; + align-items: stretch; + padding: 0 clamp(var(--sp-8), 5vw, var(--sp-16)); + flex: 1; + position: relative; + } + + /* Sliding active indicator */ + .nav-indicator { + position: absolute; + top: -1px; + height: 2px; + background: var(--cosmic-orange); + transition: left var(--t-slow) var(--ease-out), width var(--t-slow) var(--ease-out); + pointer-events: none; } .nav-item { display: flex; - flex-direction: column; align-items: center; - gap: 0.3rem; - padding: 0.8rem 1rem; - border-radius: 18px; - cursor: pointer; - transition: var(--transition-smooth); + gap: var(--sp-2); + padding: 0 var(--sp-5); + height: 100%; position: relative; - overflow: hidden; - min-width: 60px; + color: var(--ink-3); + transition: color var(--t-base) var(--ease-out); + flex-shrink: 0; + background: none; + border: none; + cursor: none; } + /* Animated top-edge indicator line */ .nav-item::before { content: ""; position: absolute; - inset: 0; - background: var(--accent-gradient); - opacity: 0; - transition: var(--transition-smooth); + top: -1px; + left: 0; + right: 0; + height: 2px; + background: var(--cosmic-orange); + transform: scaleX(0); + transform-origin: left; + transition: transform var(--t-slow) var(--ease-out); } - .nav-item.active::before, - .nav-item:hover::before { - opacity: 0.1; + .nav-item.active { + color: var(--cosmic-orange); } - .nav-item.active { - background: var(--glass-secondary); + .nav-item:hover:not(.active) { + color: var(--ink-2); } - .nav-icon { - font-size: 1.2rem; - color: var(--text-color); - position: relative; - z-index: 1; - transition: var(--transition-smooth); + .nav-item:hover:not(.active)::before { + transform: scaleX(0.3); } - .nav-item.active .nav-icon { - background: var(--gradient-colors); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-size: 200% 200%; - animation: textGradient 8s ease infinite; + .nav-num { + font-family: var(--font-mono); + font-size: var(--text-xs); + letter-spacing: 0.04em; + opacity: 0.5; + line-height: 1; } .nav-label { - font-size: 0.7rem; - color: var(--text-color); - opacity: 0.8; - font-weight: 500; + font-family: var(--font-body); + font-size: var(--text-sm); + font-weight: 400; + letter-spacing: 0.01em; + line-height: 1; + } + + /* Subtle right-side year stamp */ + .nav-year { + margin-left: auto; + display: flex; + align-items: center; + padding-right: var(--sp-12); + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-3); + letter-spacing: 0.08em; + user-select: none; + } + + /* ── Theme Toggle — desktop positioning ─────────────── */ + .theme-toggle { + position: fixed; + bottom: 0; + right: clamp(var(--sp-8), 5vw, var(--sp-16)); + height: 56px; + width: 40px; + z-index: 1001; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + cursor: none; + padding: 0; + flex-shrink: 0; + } + + .theme-toggle-icon { + width: 18px; + height: 18px; + border-radius: 50%; + border: 1.5px solid var(--ink-2); position: relative; - z-index: 1; + overflow: hidden; + transition: + rotate var(--t-slow) cubic-bezier(0.16, 1, 0.3, 1), + border-color var(--t-base) var(--ease-out); + } + + /* Left half filled = light-mode indicator at rest */ + .theme-toggle-icon::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; + background: var(--ink-2); + transition: background var(--t-base) var(--ease-out); + } + + /* Dark mode: flip so filled half is on the right */ + [data-theme="dark"] .theme-toggle-icon { + rotate: 180deg; } - .nav-item.active .nav-label { - opacity: 1; - font-weight: 600; + .theme-toggle:hover .theme-toggle-icon { + border-color: var(--ink); } - /* Focus states for accessibility */ - .nav-item:focus { - outline: 2px solid var(--glass-border); - outline-offset: 2px; + .theme-toggle:hover .theme-toggle-icon::before { + background: var(--ink); } } diff --git a/css/mobile-portrait.css b/css/mobile-portrait.css index cc91f2b..8006819 100644 --- a/css/mobile-portrait.css +++ b/css/mobile-portrait.css @@ -1,6 +1,11 @@ +/* ========================================================= + MOBILE PORTRAIT — Brad Cooley Portfolio + ≤ 767px · Portrait · Vertical Scroll Snap + ========================================================= */ + @media (max-width: 767px) and (orientation: portrait) { - .desktop-layout, - .mobile-landscape-layout { + /* ── Layout ─────────────────────────────────────────── */ + .desktop-layout { display: none; } @@ -11,12 +16,9 @@ overflow-y: auto; overflow-x: hidden; scroll-snap-type: y mandatory; - scroll-behavior: smooth; -webkit-overflow-scrolling: touch; - /* Hide scrollbar */ -ms-overflow-style: none; scrollbar-width: none; - /* Improve scroll performance */ will-change: scroll-position; } @@ -24,231 +26,191 @@ display: none; } - /* Mobile sections */ + /* ── Mobile Sections ────────────────────────────────── */ .mobile-section { height: 100vh; width: 100vw; display: flex; - align-items: center; - justify-content: center; - padding: 2rem 0; - padding-left: 2rem; - padding-right: 2.75rem; + align-items: flex-start; + justify-content: flex-start; + padding: 22vh var(--sp-8) var(--sp-12); + padding-right: calc(var(--sp-8) + 24px); scroll-snap-align: start; scroll-snap-stop: always; position: relative; } - /* Mobile cards */ - .mobile-card { - width: 100%; - height: 75%; - background: var(--glass-primary); - backdrop-filter: blur(25px); - border-radius: 24px; - border: 1px solid var(--glass-border); - box-shadow: var(--glass-shadow); + /* ── Mobile Content Block ───────────────────────────── */ + .mobile-content { display: flex; flex-direction: column; - align-items: center; - justify-content: space-between; - padding: 2rem 1.5rem; - text-align: center; - position: relative; - overflow: hidden; - animation: mobileCardIn 0.8s ease-out forwards; + gap: var(--sp-5); + width: 100%; + max-width: 340px; } - .mobile-card::before { - content: ""; - position: absolute; - inset: 0; - border-radius: 24px; - padding: 1px; - background: var(--accent-gradient); - mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); - mask-composite: subtract; - -webkit-mask-composite: subtract; - opacity: 0.6; - pointer-events: none; + .mobile-eyebrow { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-3); + text-transform: uppercase; + letter-spacing: 0.14em; } - .mobile-section:last-child { - scroll-snap-align: end; + /* Home: large name */ + .mobile-name { + font-family: var(--font-display); + font-size: clamp(3.25rem, 13vw, 4.5rem); + font-weight: 800; + line-height: 0.91; + letter-spacing: -0.04em; + color: var(--ink); } - /* Card content */ - .mobile-icon { - font-size: 3.5rem; - margin-bottom: 1.5rem; - background: var(--gradient-colors); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-size: 200% 200%; - animation: textGradient 8s ease infinite; - flex-shrink: 0; + .mobile-rule { + width: 32px; + height: 1px; + background: var(--line); } - .mobile-title { - font-family: var(--font-heading); - font-size: 2.2rem; - font-weight: 600; - margin-bottom: 1rem; - background: var(--gradient-colors); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-size: 200% 200%; - animation: textGradient 8s ease infinite; - flex-shrink: 0; + .mobile-role { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-2); + text-transform: uppercase; + letter-spacing: 0.14em; + line-height: 1; } - .mobile-description { - font-size: 1rem; - color: var(--text-color); - opacity: 0.9; - line-height: 1.5; - margin-bottom: 2rem; - flex-grow: 1; + .mobile-links { display: flex; - align-items: center; - text-align: center; + gap: var(--sp-5); + margin-top: var(--sp-1); } - .mobile-badge { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.8rem 1.5rem; - background: var(--glass-secondary); - backdrop-filter: blur(15px); - border-radius: 50px; - color: var(--text-color); - font-weight: 500; - border: 1px solid var(--glass-border); - font-size: 0.9rem; - flex-shrink: 0; - transition: var(--transition-smooth); + .mobile-links a { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-2); + text-decoration: none; + text-transform: uppercase; + letter-spacing: 0.1em; + -webkit-tap-highlight-color: transparent; + transition: color var(--t-fast); } - .mobile-badge:active { - transform: scale(0.95); + .mobile-links a:active { + color: var(--ink); } - /* Home section specific styles */ - .mobile-section.home .mobile-title { - font-size: 2.8rem; - margin-bottom: 0.5rem; + .link-arrow { + display: inline-block; + margin-left: 2px; } - .mobile-section.home .mobile-description { - font-size: 1.1rem; - margin-bottom: 2rem; + /* Non-home sections */ + .mobile-section-title { + font-family: var(--font-display); + font-size: clamp(2.25rem, 10vw, 3rem); + font-weight: 800; + line-height: 0.95; + letter-spacing: -0.03em; + background: + url("data:image/svg+xml,") + repeat, + var(--section-title-color); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; } - /* Social links in mobile */ - .mobile-section.home .social-links { - display: flex; - gap: 1rem; - justify-content: center; - flex-shrink: 0; + /* Cosmic Orange fades left→right into background; metallic noise + grain layered via SVG feTurbulence, both clipped to text. + mask-image fades the whole rendering so noise opacity tracks the + color gradient — grain vanishes exactly where the color does. */ + .mobile-name { + background: + url("data:image/svg+xml,") + repeat, + linear-gradient( + to right, + var(--cosmic-orange) 0%, + var(--cosmic-orange-deep) 20%, + var(--bg) 94% + ); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + -webkit-mask-image: linear-gradient(to right, black 0%, transparent 94%); + mask-image: linear-gradient(to right, black 0%, transparent 94%); } - .mobile-section.home .social-link { - background: var(--glass-secondary); - backdrop-filter: blur(15px); - border: 1px solid var(--glass-border); - border-radius: 16px; - padding: 0.8rem; - width: 45px; - height: 45px; - display: flex; - align-items: center; - justify-content: center; - text-decoration: none; - color: var(--text-color); - font-size: 1.2rem; - transition: var(--transition-fast); + .mobile-body-text { + font-family: var(--font-body); + font-size: var(--text-base); + font-weight: 300; + color: var(--ink-2); + line-height: 1.72; } - .mobile-section.home .social-link:active { - transform: scale(0.9); - background: var(--glass-border); + .mobile-wip { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-3); + letter-spacing: 0.06em; } - /* Scroll indicator */ + /* ── Scroll Indicator Dots ──────────────────────────── */ .scroll-indicator { position: fixed; - right: 0; + right: var(--sp-3); top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; - gap: 0.75rem; + gap: var(--sp-2); z-index: 1000; + padding: var(--sp-3); cursor: pointer; - padding: 1rem; - border-radius: 16px; - transition: var(--transition-smooth); - } - - .scroll-indicator:hover { - background: var(--glass-secondary); - backdrop-filter: blur(15px); + -webkit-tap-highlight-color: transparent; } .scroll-dot { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--glass-border); - transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); - opacity: 0.4; + width: 2px; + height: 2px; + border-radius: 0; + background: var(--ink-3); + transition: + height 220ms var(--ease-out), + background 220ms var(--ease-out); + opacity: 1; pointer-events: none; - position: relative; - overflow: hidden; - } - - .scroll-dot::before { - content: ""; - position: absolute; - inset: 0; - background: var(--accent-gradient); - border-radius: inherit; - transform: scale(0); - transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); } .scroll-dot.active { - width: 10px; - height: 24px; - border-radius: 12px; + height: 20px; + background: var(--cosmic-orange); opacity: 1; } - .scroll-dot.active::before { - transform: scale(1); - } - - /* Slide-out navigation menu */ + /* ── Slide-out Navigation Menu ──────────────────────── */ .scroll-menu { position: fixed; - right: 3rem; + right: var(--sp-8); top: 50%; - transform: translateY(-50%) translateX(10px); - background: var(--glass-primary); - backdrop-filter: blur(25px); - -webkit-backdrop-filter: blur(25px); - border: 1px solid var(--glass-border); - border-radius: 16px; - padding: 1rem; - min-width: 200px; + transform: translateY(-50%) translateX(8px); + background: var(--bg); + border: 1px solid var(--line); + border-radius: 0; + padding: var(--sp-2) 0; + min-width: 172px; opacity: 0; visibility: hidden; - transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + transition: + opacity var(--t-base) var(--ease-out), + transform var(--t-base) var(--ease-out), + visibility var(--t-base); z-index: 999; - box-shadow: var(--glass-shadow); } .scroll-menu.active { @@ -260,67 +222,67 @@ .scroll-menu-item { display: flex; align-items: center; - gap: 1rem; - padding: 0.75rem 1rem; - border-radius: 12px; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + border-left: 2px solid transparent; + color: var(--ink-3); + transition: + border-color var(--t-fast), + color var(--t-fast); cursor: pointer; - transition: var(--transition-fast); - color: var(--text-color); - font-size: 0.9rem; - font-weight: 500; - margin-bottom: 0.5rem; + -webkit-tap-highlight-color: transparent; } - .scroll-menu-item:last-child { + .scroll-menu-item:not(:last-child) { margin-bottom: 0; } .scroll-menu-item:hover { - background: var(--glass-secondary); - transform: translateX(2px); + background: none; + border-left-color: var(--cosmic-orange); + color: var(--ink); } .scroll-menu-item.active { - background: var(--glass-secondary); + background: none; + border-left-color: var(--cosmic-orange); } - .scroll-menu-item.active .scroll-menu-icon { - background: var(--gradient-colors); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-size: 200% 200%; - animation: textGradient 8s ease infinite; + .scroll-menu-num { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-3); + letter-spacing: 0.04em; + flex-shrink: 0; + transition: color var(--t-fast); } - .scroll-menu-icon { - font-size: 1.1rem; - width: 20px; - text-align: center; - transition: var(--transition-fast); + .scroll-menu-item.active .scroll-menu-num { + color: var(--cosmic-orange); } .scroll-menu-label { - flex: 1; - opacity: 0.9; + font-family: var(--font-body); + font-size: var(--text-sm); + font-weight: 400; + transition: color var(--t-fast); } .scroll-menu-item.active .scroll-menu-label { - opacity: 1; - font-weight: 600; + font-weight: 500; + color: var(--cosmic-orange); } - /* Menu backdrop */ + /* ── Backdrop ───────────────────────────────────────── */ .scroll-menu-backdrop { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.2); - backdrop-filter: blur(2px); - -webkit-backdrop-filter: blur(2px); z-index: 998; opacity: 0; visibility: hidden; - transition: var(--transition-smooth); + transition: + opacity var(--t-base), + visibility var(--t-base); } .scroll-menu-backdrop.active { @@ -328,109 +290,129 @@ visibility: visible; } - /* Scroll hint */ + /* ── Scroll Hint ────────────────────────────────────── */ .scroll-hint { position: fixed; - bottom: 2rem; - left: 50%; - transform: translateX(-50%) translateY(10px); - background: var(--glass-primary); - backdrop-filter: blur(25px); - border: 1px solid var(--glass-border); - border-radius: 20px; - padding: 0.8rem 1.2rem; - font-size: 0.8rem; - color: var(--text-color); + bottom: var(--sp-8); + left: var(--sp-8); display: flex; align-items: center; - gap: 0.5rem; + gap: var(--sp-3); opacity: 0; - animation: scrollHintFade 4s ease-in-out 1s; + animation: hintFade 4s ease-in-out 1.8s; pointer-events: none; z-index: 999; } - .scroll-hint i { - animation: bounce 2s infinite; + .scroll-hint-line { + width: 20px; + height: 1px; + background: var(--ink-3); + } + + .scroll-hint span { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--ink-3); + text-transform: uppercase; + letter-spacing: 0.12em; } - /* Animations */ - @keyframes mobileCardIn { - from { - opacity: 0; - transform: translateY(30px); - } - to { - opacity: 1; - transform: translateY(0); - } + /* ── Bounce Feedback ────────────────────────────────── */ + .mobile-portrait-layout.bounce-start { + animation: bounceDown 0.3s ease-out; + } + + .mobile-portrait-layout.bounce-end { + animation: bounceUp 0.3s ease-out; } - @keyframes scrollHintFade { + /* ── Keyframes ──────────────────────────────────────── */ + @keyframes hintFade { 0%, 100% { opacity: 0; - transform: translateX(-50%) translateY(10px); + transform: translateY(6px); } 25%, 75% { opacity: 1; - transform: translateX(-50%) translateY(0); - } - } - - @keyframes bounce { - 0%, - 20%, - 50%, - 80%, - 100% { transform: translateY(0); } - 40% { - transform: translateY(-5px); - } - 60% { - transform: translateY(-3px); - } } - @keyframes bounceStart { + @keyframes bounceDown { 0% { transform: translateY(0); } 50% { - transform: translateY(10px); + transform: translateY(8px); } 100% { transform: translateY(0); } } - @keyframes bounceEnd { + @keyframes bounceUp { 0% { transform: translateY(0); } 50% { - transform: translateY(-10px); + transform: translateY(-8px); } 100% { transform: translateY(0); } } - /* Bounce effect classes */ - .mobile-portrait-layout.bounce-start { - animation: bounceStart 0.3s ease-out; + /* ── Theme Toggle — mobile positioning ─────────────── */ + .theme-toggle { + position: fixed; + bottom: var(--sp-8); + left: var(--sp-8); + width: 36px; + height: 36px; + z-index: 1001; + display: flex; + align-items: center; + justify-content: center; + background: none; + border: none; + padding: 0; + -webkit-tap-highlight-color: transparent; } - .mobile-portrait-layout.bounce-end { - animation: bounceEnd 0.3s ease-out; + .theme-toggle-icon { + width: 18px; + height: 18px; + border-radius: 50%; + border: 1.5px solid var(--ink-2); + position: relative; + overflow: hidden; + transition: rotate 600ms cubic-bezier(0.16, 1, 0.3, 1); + } + + .theme-toggle-icon::before { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 50%; + height: 100%; + background: var(--ink-2); + } + + [data-theme="dark"] .theme-toggle-icon { + rotate: 180deg; + } + + .theme-toggle-icon.is-pulsing { + animation: togglePulse 360ms cubic-bezier(0.34, 1.56, 0.64, 1); } - /* Focus states for accessibility */ - .scroll-menu-item:focus { - outline: 2px solid var(--glass-border); + /* ── Accessibility ──────────────────────────────────── */ + .scroll-menu-item:focus-visible { + outline: 2px solid var(--cosmic-orange); outline-offset: 2px; } } diff --git a/index.html b/index.html index 71272d2..7736759 100755 --- a/index.html +++ b/index.html @@ -1,155 +1,174 @@ - + - Brad Cooley | Software & Data Engineer + Brad Cooley — Software & Data Engineer + + - + - + + + + + + + + + + +
+
- +
-
-
-
-

Brad Cooley

-
-

Software & Data Engineer

-
- +
-
-
-
- -
-

About

-

+

+
+ +
+

Passionate developer creating elegant solutions to complex problems. Focused on building applications that provide exceptional user experiences.

-
- - More details coming soon -
+ — More details in progress
+
-
-
-
- -
-

Projects

-

- Explore my latest work, from web applications to open-source - contributions. Each project represents a journey of learning, - problem-solving, and innovation. +

+
+ +
+

+ A selection of work spanning web applications, data + engineering, and open-source contributions. Each project + represents a journey of learning and craft.

-
- - Showcase launching soon -
+ — Showcase launching soon
+
-
-
-
- -
-

Blog

-

- Dive into my thoughts on technology, development practices, - and industry trends. Knowledge sharing and documentation of my - learning journey. +

+
+ +
+

+ Thoughts on technology, development practices, and the craft + of building software. A documentation of my learning journey.

-
- - First posts coming soon -
+ — First posts arriving soon
+
-
-
-
- -
-

Resume

-

- Professional background, skills, and experience. A - comprehensive look at my career journey and technical - expertise. +

+
+ +
+

+ Professional background, technical skills, and experience. A + comprehensive look at my career journey as a software and data + engineer.

-
- - Full resume coming soon -
+ — Full resume arriving soon
+ - -
+ - +
+
-
- -

Brad Cooley

-

Software & Data Engineer

- - +
-
- -

About

-

+

+
02 / 05
+

About

+
+

Passionate developer creating elegant solutions to complex problems. Focused on building applications that provide exceptional user experiences.

-
- - More details coming soon -
+ — More details in progress
+
-
- -

Projects

-

- Explore my latest work, from web applications to open-source - contributions. Each project represents a journey of learning and - innovation. +

+
03 / 05
+

Projects

+
+

+ A selection of work spanning web applications, data engineering, + and open-source contributions.

-
- - Showcase launching soon -
+ — Showcase launching soon
+
-
- -

Blog

-

- Dive into my thoughts on technology, development practices, and - industry trends. Knowledge sharing and learning journey - documentation. +

+
04 / 05
+

Writing

+
+

+ Thoughts on technology, development practices, and the craft of + building software.

-
- - First posts coming soon -
+ — First posts arriving soon
+
-
- -

Resume

-

- Professional background, skills, and experience. A comprehensive - look at my career journey and technical expertise. +

+
05 / 05
+

Resume

+
+

+ Professional background, technical skills, and experience.

-
- - Full resume coming soon -
+ — Full resume arriving soon
-
+ +
@@ -298,15 +315,19 @@

Resume

- -
+ +
+
- - Scroll to explore +
+ Scroll
+
+ + diff --git a/js/main.js b/js/main.js index 45129ea..f2a40d9 100644 --- a/js/main.js +++ b/js/main.js @@ -2,9 +2,10 @@ const NAVIGATION_CONFIG = { SECTION_NAMES: ["home", "about", "projects", "blog", "resume"], DRAG_THRESHOLD: 0.2, - WHEEL_THRESHOLD: 50, + WHEEL_THRESHOLD: 25, RESIZE_DEBOUNCE: 150, ORIENTATION_DELAY: 500, + TRANSITION_LOCK_MS: 650, // matches --t-section (600ms) + small buffer }; class PortfolioNavigator { @@ -17,6 +18,8 @@ class PortfolioNavigator { initialTransform: 0, }; + this.isTransitioning = false; + this.elements = this.cacheElements(); this.totalSections = this.elements.sections.length; @@ -28,6 +31,7 @@ class PortfolioNavigator { sectionsContainer: document.querySelector(".sections-container"), sections: Array.from(document.querySelectorAll(".section")), navItems: Array.from(document.querySelectorAll(".nav-item")), + navIndicator: document.querySelector(".nav-indicator"), mobileSections: Array.from(document.querySelectorAll(".mobile-section")), scrollDots: Array.from(document.querySelectorAll(".scroll-dot")), mobileContainer: document.getElementById("mobilePortraitContainer"), @@ -67,6 +71,14 @@ class PortfolioNavigator { } // Navigation methods + startTransition() { + this.isTransitioning = true; + clearTimeout(this.transitionTimeout); + this.transitionTimeout = setTimeout(() => { + this.isTransitioning = false; + }, NAVIGATION_CONFIG.TRANSITION_LOCK_MS); + } + goToSection(index, smoothTransition = true) { if (index < 0 || index >= this.totalSections) return; @@ -148,6 +160,16 @@ class PortfolioNavigator { item.classList.toggle("active", idx === this.state.currentSection); }); + const activeItem = this.elements.navItems[this.state.currentSection]; + if (activeItem && this.elements.navIndicator) { + const indicator = this.elements.navIndicator; + const isFirstPlacement = !indicator.style.left; + if (isFirstPlacement) indicator.style.transition = "none"; + indicator.style.left = `${activeItem.offsetLeft}px`; + indicator.style.width = `${activeItem.offsetWidth}px`; + if (isFirstPlacement) requestAnimationFrame(() => { indicator.style.transition = ""; }); + } + this.elements.sections.forEach((sec, i) => { sec.classList.toggle("active", i === this.state.currentSection); }); @@ -246,9 +268,10 @@ class PortfolioNavigator { this.state.initialTransform = this.getTransformX(); if (this.elements.sectionsContainer) { - this.elements.sectionsContainer.style.cursor = "grabbing"; this.elements.sectionsContainer.classList.add("no-transition"); } + + document.querySelector(".cursor")?.classList.add("is-dragging"); } updateDrag(clientX) { @@ -270,10 +293,11 @@ class PortfolioNavigator { this.state.isDragging = false; if (this.elements.sectionsContainer) { - this.elements.sectionsContainer.style.cursor = "grab"; this.elements.sectionsContainer.classList.remove("no-transition"); } + document.querySelector(".cursor")?.classList.remove("is-dragging"); + const deltaX = this.state.currentX - this.state.startX; const threshold = window.innerWidth * NAVIGATION_CONFIG.DRAG_THRESHOLD; @@ -298,9 +322,12 @@ class PortfolioNavigator { if (this.isMobilePortrait()) return; e.preventDefault(); + if (this.isTransitioning) return; + const delta = e.deltaX || e.deltaY; if (Math.abs(delta) > NAVIGATION_CONFIG.WHEEL_THRESHOLD) { + this.startTransition(); delta > 0 ? this.nextSection() : this.previousSection(); } } @@ -310,12 +337,18 @@ class PortfolioNavigator { case "ArrowLeft": case "ArrowUp": e.preventDefault(); - this.previousSection(); + if (!this.isTransitioning) { + this.startTransition(); + this.previousSection(); + } break; case "ArrowRight": case "ArrowDown": e.preventDefault(); - this.nextSection(); + if (!this.isTransitioning) { + this.startTransition(); + this.nextSection(); + } break; default: const num = parseInt(e.key); @@ -520,6 +553,77 @@ class PortfolioNavigator { ); } + // Theme toggle: cross-fade via View Transitions API. + // The browser snapshots before/after and crossfades between them. + // Fallback for browsers without View Transitions: instant swap. + initTheme() { + const toggle = document.getElementById("themeToggle"); + if (!toggle) return; + + toggle.addEventListener("click", () => { + const root = document.documentElement; + const current = root.getAttribute("data-theme"); + const next = current === "dark" ? "light" : "dark"; + + const applyTheme = () => { + root.setAttribute("data-theme", next); + localStorage.setItem("theme", next); + }; + + // Fallback: no View Transitions support or user prefers reduced motion + if ( + !document.startViewTransition || + window.matchMedia("(prefers-reduced-motion: reduce)").matches + ) { + applyTheme(); + return; + } + + document.startViewTransition(applyTheme); + }); + } + + // Custom cursor tracking (fine-pointer / mouse only) + initCursor() { + const cursor = document.querySelector(".cursor"); + if (!cursor || !window.matchMedia("(pointer: fine)").matches) return; + + let rafPending = false; + let x = window.innerWidth / 2; + let y = window.innerHeight / 2; + + document.addEventListener("mousemove", (e) => { + x = e.clientX; + y = e.clientY; + if (!rafPending) { + rafPending = true; + requestAnimationFrame(() => { + cursor.style.transform = `translate(calc(${x}px - 50%), calc(${y}px - 50%))`; + rafPending = false; + }); + } + }); + + document.addEventListener("mouseleave", () => { + cursor.style.opacity = "0"; + }); + document.addEventListener("mouseenter", () => { + cursor.style.opacity = "1"; + }); + + const hoverTargets = document.querySelectorAll( + "a, button, [role='button'], .nav-item, .scroll-dot, .scroll-menu-item" + ); + hoverTargets.forEach((el) => { + el.addEventListener("mouseenter", () => + cursor.classList.add("is-hovering") + ); + el.addEventListener("mouseleave", () => + cursor.classList.remove("is-hovering") + ); + }); + } + // Initialization init() { const hash = location.hash.slice(1); @@ -527,6 +631,8 @@ class PortfolioNavigator { this.state.currentSection = idx !== -1 ? idx : 0; this.setupEventListeners(); + this.initTheme(); + this.initCursor(); // Initialize after a brief delay to ensure DOM readiness setTimeout(() => {