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
-
-
+
01
+
-
+
-
-
-
-
-
-
About
-
+
02
+
+
+ 02 — About
+
Who I am.
+
+
+
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.
+
03
+
+
+ 03 — Projects
+
What I build.
+
+
+
+ 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.
+
04
+
+
+ 04 — Writing
+
How I think.
+
+
+
+ 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.
+
05
+
+
+ 05 — Resume
+
What I've done.
+
+
+
+ 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
+
-
-
+
+
+
-
+ 01
Home
Resume
tabindex="0"
aria-label="About"
>
-
+ 02
About
-
+ 03
Projects
Resume
data-section="3"
role="button"
tabindex="0"
- aria-label="Blog"
+ aria-label="Writing"
>
-
- Blog
+ 04
+ Writing
Resume
tabindex="0"
aria-label="Resume"
>
-
+ 05
Resume
+
-
+
+
-
-
-
Brad Cooley
-
Software & Data Engineer
-
+
+
01 / 05
+
Brad Cooley
+
+
Lead Data Engineer @ Mutually Human
+
-
+
-
-
-
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
-
+
+
+
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(() => {