Portfolio — 3D / Interactive Portfolio
A full one-page designer portfolio in an immersive 3D, interactive style built with pure CSS 3D transforms and vanilla JavaScript, no WebGL or libraries. A dark neon stage with a perspective grid floor, drifting glows, a perpetually rotating hero cube ringed by orbiting bands, and tilt-on-hover project cards that compute rotateX and rotateY from the pointer with a moving glare and parallaxed depth layers. The hero is gyroscope-friendly on phones, and animated counters, skill bars, scroll reveals, and a validated contact form complete the experience.
MCP
Code
:root {
/* Palette — dark + neon */
--bg: #06060d;
--bg-2: #0b0b18;
--panel: rgba(20, 20, 38, 0.66);
--panel-edge: rgba(124, 92, 255, 0.28);
--ink: #f2f1ff;
--muted: #9a98c4;
--dim: #6c6a93;
--accent: #7c5cff;
--accent-2: #22d3ee;
--accent-3: #f472b6;
--line: rgba(124, 92, 255, 0.16);
--glow: 0 0 0 1px rgba(124, 92, 255, 0.3), 0 18px 50px -20px rgba(124, 92, 255, 0.55);
--shadow-deep: 0 40px 80px -40px rgba(0, 0, 0, 0.9);
--radius: 18px;
--display: "Chakra Petch", system-ui, sans-serif;
--body: "Space Grotesk", system-ui, sans-serif;
--maxw: 1180px;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--body);
color: var(--ink);
background: var(--bg);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
/* ---------- Atmosphere ---------- */
.grid-floor {
position: fixed;
inset: 40% -20% -20% -20%;
background-image:
linear-gradient(var(--line) 1px, transparent 1px),
linear-gradient(90deg, var(--line) 1px, transparent 1px);
background-size: 56px 56px;
transform: perspective(520px) rotateX(64deg);
transform-origin: top center;
mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), transparent 70%);
-webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.55), transparent 70%);
pointer-events: none;
z-index: 0;
opacity: 0.7;
}
.glow {
position: fixed;
border-radius: 50%;
filter: blur(90px);
pointer-events: none;
z-index: 0;
opacity: 0.55;
}
.glow-a {
width: 46vmax; height: 46vmax;
top: -16vmax; left: -10vmax;
background: radial-gradient(circle, rgba(124, 92, 255, 0.5), transparent 65%);
animation: float-a 22s var(--ease) infinite alternate;
}
.glow-b {
width: 40vmax; height: 40vmax;
bottom: -14vmax; right: -8vmax;
background: radial-gradient(circle, rgba(34, 211, 238, 0.4), transparent 65%);
animation: float-b 26s var(--ease) infinite alternate;
}
@keyframes float-a { to { transform: translate3d(6vmax, 4vmax, 0) scale(1.1); } }
@keyframes float-b { to { transform: translate3d(-5vmax, -3vmax, 0) scale(1.08); } }
.noise {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 1;
opacity: 0.05;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* ---------- Skip link ---------- */
.skip-link {
position: fixed;
top: -60px; left: 16px;
z-index: 100;
background: var(--accent);
color: #fff;
padding: 10px 16px;
border-radius: 10px;
font-weight: 600;
transition: top 0.2s var(--ease);
}
.skip-link:focus-visible { top: 16px; }
/* ---------- Scroll progress ---------- */
.scroll-progress {
position: fixed;
top: 0; left: 0; right: 0;
height: 3px;
z-index: 60;
background: transparent;
}
.scroll-progress span {
display: block;
height: 100%;
width: 0;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
box-shadow: 0 0 14px var(--accent);
}
/* ---------- Nav ---------- */
.nav {
position: sticky;
top: 0;
z-index: 50;
display: flex;
align-items: center;
gap: 24px;
max-width: var(--maxw);
margin: 0 auto;
padding: 18px 24px;
backdrop-filter: blur(14px);
}
.brand {
display: inline-flex;
align-items: center;
gap: 12px;
text-decoration: none;
color: var(--ink);
font-family: var(--display);
font-weight: 700;
letter-spacing: 0.02em;
}
.brand-name { font-size: 1rem; }
/* nav cube — perpetually rotating mark */
.brand-cube {
position: relative;
width: 30px; height: 30px;
transform-style: preserve-3d;
animation: spin-cube 9s linear infinite;
}
.brand-cube .face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-size: 0.62rem;
font-weight: 700;
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.4);
background: rgba(124, 92, 255, 0.32);
}
.f-front { transform: translateZ(15px); }
.f-back { transform: rotateY(180deg) translateZ(15px); }
.f-right { transform: rotateY(90deg) translateZ(15px); background: rgba(34, 211, 238, 0.3); }
.f-left { transform: rotateY(-90deg) translateZ(15px); background: rgba(244, 114, 182, 0.3); }
.f-top { transform: rotateX(90deg) translateZ(15px); background: rgba(124, 92, 255, 0.5); }
.f-bottom { transform: rotateX(-90deg) translateZ(15px); background: rgba(124, 92, 255, 0.2); }
@keyframes spin-cube { to { transform: rotateX(360deg) rotateY(360deg); } }
.nav-links {
display: flex;
gap: 22px;
margin-left: auto;
}
.nav-links a {
color: var(--muted);
text-decoration: none;
font-size: 0.92rem;
font-weight: 500;
position: relative;
transition: color 0.2s var(--ease);
}
.nav-links a::after {
content: "";
position: absolute;
left: 0; bottom: -5px;
width: 0; height: 2px;
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
transition: width 0.25s var(--ease);
}
.nav-links a:hover,
.nav-links a:focus-visible { color: var(--ink); }
.nav-links a:hover::after,
.nav-links a:focus-visible::after { width: 100%; }
.nav-cta {
text-decoration: none;
color: var(--ink);
font-weight: 600;
font-size: 0.9rem;
padding: 9px 18px;
border-radius: 999px;
border: 1px solid var(--panel-edge);
background: rgba(124, 92, 255, 0.12);
transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease), background 0.2s var(--ease);
}
.nav-cta:hover,
.nav-cta:focus-visible {
transform: translateY(-2px);
box-shadow: var(--glow);
background: rgba(124, 92, 255, 0.22);
}
/* ---------- Layout ---------- */
main { position: relative; z-index: 2; }
.section {
max-width: var(--maxw);
margin: 0 auto;
padding: clamp(64px, 11vw, 130px) 24px;
}
.section-head { max-width: 640px; margin-bottom: 52px; }
.section-index {
display: inline-block;
font-family: var(--display);
font-size: 0.8rem;
letter-spacing: 0.32em;
color: var(--accent-2);
text-transform: uppercase;
}
.section-title {
font-family: var(--display);
font-weight: 700;
font-size: clamp(2rem, 5vw, 3.1rem);
line-height: 1.05;
margin: 10px 0 14px;
letter-spacing: -0.01em;
}
.section-lead { color: var(--muted); font-size: 1.05rem; max-width: 56ch; }
.grad {
background: linear-gradient(100deg, var(--accent), var(--accent-2) 55%, var(--accent-3));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* ---------- Hero ---------- */
.hero {
position: relative;
max-width: var(--maxw);
margin: 0 auto;
padding: clamp(40px, 8vw, 96px) 24px clamp(40px, 7vw, 90px);
display: grid;
grid-template-columns: 1.15fr 0.85fr;
gap: 40px;
align-items: center;
min-height: 78vh;
}
.hero-inner { position: relative; z-index: 3; }
.eyebrow {
display: inline-flex;
align-items: center;
gap: 9px;
font-size: 0.82rem;
color: var(--muted);
letter-spacing: 0.04em;
border: 1px solid var(--line);
padding: 6px 14px;
border-radius: 999px;
background: rgba(124, 92, 255, 0.08);
}
.dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 10px var(--accent-2);
animation: pulse 1.8s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: 0.35; transform: scale(0.75); } }
.hero-title {
font-family: var(--display);
font-weight: 700;
font-size: clamp(2.6rem, 8.5vw, 5.6rem);
line-height: 0.98;
letter-spacing: -0.02em;
margin: 22px 0 18px;
}
.hero-title .line { display: block; }
.hero-sub {
color: var(--muted);
font-size: clamp(1rem, 1.6vw, 1.18rem);
max-width: 44ch;
margin: 0 0 28px;
}
.hero-sub strong { color: var(--ink); }
.hero-actions { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 44px; }
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
font-family: var(--display);
font-weight: 600;
font-size: 0.95rem;
padding: 13px 26px;
border-radius: 12px;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease), background 0.2s var(--ease);
}
.btn-primary {
color: #fff;
background: linear-gradient(120deg, var(--accent), #5b3fff);
box-shadow: var(--glow);
}
.btn-primary:hover,
.btn-primary:focus-visible {
transform: translateY(-3px);
box-shadow: 0 0 0 1px rgba(124, 92, 255, 0.5), 0 26px 60px -22px rgba(124, 92, 255, 0.8);
}
.btn-ghost {
color: var(--ink);
border-color: var(--panel-edge);
background: rgba(124, 92, 255, 0.06);
}
.btn-ghost:hover,
.btn-ghost:focus-visible {
transform: translateY(-3px);
background: rgba(124, 92, 255, 0.16);
box-shadow: var(--glow);
}
.btn-block { width: 100%; }
.hero-stats {
display: grid;
grid-template-columns: repeat(3, auto);
gap: 30px;
margin: 0;
}
.stat dt { font-size: 0.78rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.08em; }
.stat dd {
margin: 4px 0 0;
font-family: var(--display);
font-weight: 700;
font-size: clamp(1.6rem, 4vw, 2.3rem);
color: var(--ink);
}
/* ---------- 3D hero stage ---------- */
.hero-stage {
position: relative;
z-index: 2;
display: grid;
place-items: center;
min-height: 340px;
perspective: 900px;
}
.scene {
position: relative;
width: 200px; height: 200px;
transform-style: preserve-3d;
transform: rotateX(var(--rx, -14deg)) rotateY(var(--ry, 24deg));
transition: transform 0.18s var(--ease);
}
.cube {
position: absolute;
inset: 30px;
transform-style: preserve-3d;
animation: cube-spin 16s linear infinite;
}
@keyframes cube-spin { to { transform: rotateX(360deg) rotateY(360deg); } }
.cube-face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border: 1px solid rgba(124, 92, 255, 0.55);
background: linear-gradient(135deg, rgba(124, 92, 255, 0.22), rgba(34, 211, 238, 0.12));
box-shadow: inset 0 0 24px rgba(124, 92, 255, 0.4);
backdrop-filter: blur(2px);
}
.cube-face span {
font-family: var(--display);
font-weight: 700;
font-size: 1.5rem;
color: var(--ink);
text-shadow: 0 0 18px var(--accent);
}
.cf-front { transform: translateZ(70px); }
.cf-back { transform: rotateY(180deg) translateZ(70px); }
.cf-right { transform: rotateY(90deg) translateZ(70px); border-color: rgba(34, 211, 238, 0.6); }
.cf-left { transform: rotateY(-90deg) translateZ(70px); border-color: rgba(244, 114, 182, 0.6); }
.cf-top { transform: rotateX(90deg) translateZ(70px); }
.cf-bottom { transform: rotateX(-90deg) translateZ(70px); }
.ring {
position: absolute;
top: 50%; left: 50%;
border: 1px solid rgba(34, 211, 238, 0.45);
border-radius: 50%;
box-shadow: 0 0 24px rgba(34, 211, 238, 0.3);
}
.ring-1 {
width: 280px; height: 280px;
transform: translate(-50%, -50%) rotateX(74deg);
animation: ring-spin 12s linear infinite;
}
.ring-2 {
width: 220px; height: 220px;
border-color: rgba(244, 114, 182, 0.45);
transform: translate(-50%, -50%) rotateX(70deg) rotateZ(40deg);
animation: ring-spin 9s linear infinite reverse;
}
@keyframes ring-spin {
to { transform: translate(-50%, -50%) rotateX(74deg) rotateZ(360deg); }
}
.scroll-hint {
position: absolute;
bottom: 8px;
left: 24px;
display: flex;
align-items: center;
gap: 12px;
color: var(--dim);
font-size: 0.74rem;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.scroll-line {
width: 56px; height: 1px;
background: var(--dim);
position: relative;
overflow: hidden;
}
.scroll-line::after {
content: "";
position: absolute;
inset: 0;
width: 18px;
background: var(--accent-2);
box-shadow: 0 0 8px var(--accent-2);
animation: slide 2.2s var(--ease) infinite;
}
@keyframes slide { from { transform: translateX(-20px); } to { transform: translateX(60px); } }
/* ---------- Marquee ---------- */
.marquee {
overflow: hidden;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
background: rgba(124, 92, 255, 0.04);
white-space: nowrap;
}
.marquee-track {
display: inline-flex;
gap: 26px;
padding: 16px 0;
animation: marquee 26s linear infinite;
}
.marquee-track span {
font-family: var(--display);
font-size: 1rem;
color: var(--muted);
letter-spacing: 0.04em;
}
.marquee-track span:nth-child(even) { color: var(--accent); }
@keyframes marquee { to { transform: translateX(-50%); } }
/* ---------- Work / projects ---------- */
.projects {
list-style: none;
margin: 0; padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 26px;
}
.project { perspective: 1100px; }
.project-link {
--rx: 0deg; --ry: 0deg; --mx: 50%; --my: 50%;
position: relative;
display: block;
text-decoration: none;
color: var(--ink);
border-radius: var(--radius);
border: 1px solid var(--panel-edge);
background: var(--panel);
padding: 0;
overflow: hidden;
transform-style: preserve-3d;
transform: rotateX(var(--rx)) rotateY(var(--ry)) translateZ(0);
transition: transform 0.22s var(--ease), box-shadow 0.22s var(--ease), border-color 0.22s var(--ease);
box-shadow: var(--shadow-deep);
}
.project-link::before {
/* glare following the pointer */
content: "";
position: absolute;
inset: 0;
background: radial-gradient(360px circle at var(--mx) var(--my), rgba(255, 255, 255, 0.16), transparent 45%);
opacity: 0;
transition: opacity 0.25s var(--ease);
pointer-events: none;
z-index: 4;
}
.project-link:hover,
.project-link:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 40px 80px -36px var(--accent), var(--shadow-deep);
}
.project-link:hover::before,
.project-link:focus-visible::before { opacity: 1; }
.project-visual {
display: block;
position: relative;
height: 180px;
overflow: hidden;
transform-style: preserve-3d;
}
.pv-layer {
position: absolute;
border-radius: 14px;
transform: translateZ(0);
transition: transform 0.22s var(--ease);
will-change: transform;
}
.pv-l1 { inset: 22px; opacity: 0.95; }
.pv-l2 { inset: 44px; opacity: 0.85; }
.pv-l3 { inset: 66px; opacity: 0.95; }
.visual-1 { background: radial-gradient(120% 120% at 0 0, #1b1240, #0a0820); }
.visual-1 .pv-l1 { background: linear-gradient(135deg, #7c5cff, #4b2fd6); }
.visual-1 .pv-l2 { background: linear-gradient(135deg, #b6a3ff, #7c5cff); }
.visual-1 .pv-l3 { background: linear-gradient(135deg, #fff, #d9d0ff); }
.visual-2 { background: radial-gradient(120% 120% at 100% 0, #0a2733, #04141c); }
.visual-2 .pv-l1 { background: linear-gradient(135deg, #22d3ee, #0e7490); }
.visual-2 .pv-l2 { background: linear-gradient(135deg, #a5f3fc, #22d3ee); }
.visual-2 .pv-l3 { background: linear-gradient(135deg, #fff, #cffafe); }
.visual-3 { background: radial-gradient(120% 120% at 0 100%, #3a2008, #1a0d02); }
.visual-3 .pv-l1 { background: linear-gradient(135deg, #f97316, #c2410c); }
.visual-3 .pv-l2 { background: linear-gradient(135deg, #fdba74, #f97316); }
.visual-3 .pv-l3 { background: linear-gradient(135deg, #fff, #ffedd5); }
.visual-4 { background: radial-gradient(120% 120% at 100% 100%, #07291c, #02140d); }
.visual-4 .pv-l1 { background: linear-gradient(135deg, #34d399, #047857); }
.visual-4 .pv-l2 { background: linear-gradient(135deg, #6ee7b7, #34d399); }
.visual-4 .pv-l3 { background: linear-gradient(135deg, #fff, #d1fae5); }
.visual-5 { background: radial-gradient(120% 120% at 0 0, #36092a, #190413); }
.visual-5 .pv-l1 { background: linear-gradient(135deg, #f472b6, #be185d); }
.visual-5 .pv-l2 { background: linear-gradient(135deg, #fbcfe8, #f472b6); }
.visual-5 .pv-l3 { background: linear-gradient(135deg, #fff, #fce7f3); }
.visual-6 { background: radial-gradient(120% 120% at 100% 0, #3a3208, #1a1602); }
.visual-6 .pv-l1 { background: linear-gradient(135deg, #facc15, #ca8a04); }
.visual-6 .pv-l2 { background: linear-gradient(135deg, #fde68a, #facc15); }
.visual-6 .pv-l3 { background: linear-gradient(135deg, #fff, #fef9c3); }
.project-body {
display: block;
padding: 22px 22px 26px;
position: relative;
z-index: 3;
transform: translateZ(40px);
}
.project-num {
font-family: var(--display);
font-size: 0.74rem;
letter-spacing: 0.22em;
color: var(--accent);
}
.project-name {
display: block;
font-family: var(--display);
font-weight: 700;
font-size: 1.4rem;
margin: 6px 0 8px;
}
.project-desc { display: block; color: var(--muted); font-size: 0.95rem; }
.project-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 14px 0 16px;
}
.project-tags span {
font-size: 0.74rem;
color: var(--muted);
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 999px;
background: rgba(124, 92, 255, 0.06);
}
.project-go {
display: inline-flex;
align-items: center;
gap: 7px;
font-family: var(--display);
font-weight: 600;
font-size: 0.9rem;
color: var(--accent-2);
}
.project-go span { transition: transform 0.2s var(--ease); }
.project-link:hover .project-go span,
.project-link:focus-visible .project-go span { transform: translateX(5px); }
/* ---------- About ---------- */
.about-grid {
display: grid;
grid-template-columns: 0.8fr 1.2fr;
gap: clamp(32px, 6vw, 70px);
align-items: center;
}
.about-portrait { perspective: 900px; display: grid; place-items: center; }
.portrait-frame {
--rx: 0deg; --ry: 0deg;
position: relative;
width: 240px; height: 300px;
border-radius: 22px;
transform-style: preserve-3d;
transform: rotateX(var(--rx)) rotateY(var(--ry));
transition: transform 0.22s var(--ease);
}
.portrait-glow {
position: absolute;
inset: -14px;
border-radius: 28px;
background: linear-gradient(135deg, var(--accent), var(--accent-2), var(--accent-3));
filter: blur(26px);
opacity: 0.55;
transform: translateZ(-30px);
}
.portrait-face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 22px;
border: 1px solid var(--panel-edge);
background: radial-gradient(130% 120% at 30% 20%, #211a4d, #0a0820);
font-family: var(--display);
font-weight: 700;
font-size: 3.6rem;
color: var(--ink);
text-shadow: 0 0 24px var(--accent);
transform: translateZ(30px);
}
.portrait-edge {
position: absolute;
inset: 0;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.12);
transform: translateZ(60px);
pointer-events: none;
}
.about-text p { color: var(--muted); margin: 0 0 16px; }
.about-text p:first-of-type { margin-top: 18px; }
.about-pills {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 22px 0 0;
padding: 0;
}
.about-pills li {
font-size: 0.82rem;
color: var(--ink);
padding: 7px 14px;
border-radius: 999px;
border: 1px solid var(--line);
background: rgba(124, 92, 255, 0.08);
}
/* ---------- Experience timeline ---------- */
.timeline {
list-style: none;
margin: 0; padding: 0;
display: grid;
gap: 18px;
}
.t-item {
position: relative;
display: grid;
grid-template-columns: 160px 1fr;
gap: 8px 24px;
padding: 22px 24px 22px 44px;
border-radius: var(--radius);
border: 1px solid var(--panel-edge);
background: var(--panel);
transform-style: preserve-3d;
transition: transform 0.22s var(--ease), box-shadow 0.22s var(--ease), border-color 0.22s var(--ease);
box-shadow: var(--shadow-deep);
}
.t-item:hover,
.t-item:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent), 0 30px 60px -34px var(--accent);
}
.t-dot {
position: absolute;
left: 22px; top: 30px;
width: 11px; height: 11px;
border-radius: 50%;
background: var(--accent-2);
box-shadow: 0 0 12px var(--accent-2);
}
.t-period {
grid-row: span 3;
font-family: var(--display);
font-size: 0.86rem;
color: var(--accent-2);
letter-spacing: 0.04em;
}
.t-role { font-family: var(--display); font-weight: 600; font-size: 1.08rem; }
.t-org { color: var(--dim); font-size: 0.88rem; }
.t-desc { color: var(--muted); font-size: 0.94rem; }
/* ---------- Skills ---------- */
.skill-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 22px; }
.skill { display: grid; gap: 10px; }
.skill-name { font-family: var(--display); font-weight: 600; font-size: 1rem; }
.skill-bar {
height: 8px;
border-radius: 999px;
background: rgba(124, 92, 255, 0.12);
overflow: hidden;
}
.skill-fill {
display: block;
height: 100%;
width: 0;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent), var(--accent-2));
box-shadow: 0 0 12px var(--accent);
transition: width 1.1s var(--ease);
}
/* ---------- Contact ---------- */
.contact { perspective: 1200px; }
.contact-inner {
--rx: 0deg; --ry: 0deg;
position: relative;
max-width: 720px;
margin: 0 auto;
padding: clamp(32px, 6vw, 56px);
border-radius: 24px;
border: 1px solid var(--panel-edge);
background: var(--panel);
text-align: center;
transform-style: preserve-3d;
transform: rotateX(var(--rx)) rotateY(var(--ry));
transition: transform 0.22s var(--ease);
box-shadow: var(--shadow-deep), var(--glow);
}
.contact-title {
font-family: var(--display);
font-weight: 700;
font-size: clamp(1.8rem, 5vw, 3rem);
line-height: 1.05;
margin: 12px 0 12px;
}
.contact-lead { color: var(--muted); max-width: 46ch; margin: 0 auto 30px; }
.contact-form {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
text-align: left;
}
.field { display: grid; gap: 6px; }
.field-wide { grid-column: 1 / -1; }
.field label { font-size: 0.82rem; color: var(--muted); font-weight: 500; }
.field input,
.field textarea {
font-family: var(--body);
font-size: 0.95rem;
color: var(--ink);
background: rgba(8, 8, 18, 0.7);
border: 1px solid var(--line);
border-radius: 12px;
padding: 12px 14px;
resize: vertical;
transition: border-color 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.field input::placeholder,
.field textarea::placeholder { color: var(--dim); }
.field input:focus-visible,
.field textarea:focus-visible {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(124, 92, 255, 0.28);
}
.field.invalid input,
.field.invalid textarea { border-color: var(--accent-3); }
.err { font-size: 0.78rem; color: var(--accent-3); min-height: 1em; }
.contact-form .btn-block { grid-column: 1 / -1; }
.contact-links {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px 22px;
margin-top: 26px;
}
.contact-links a {
color: var(--muted);
text-decoration: none;
font-weight: 500;
font-size: 0.92rem;
position: relative;
transition: color 0.2s var(--ease);
}
.contact-links a:hover,
.contact-links a:focus-visible { color: var(--accent-2); }
/* ---------- Footer ---------- */
.footer {
position: relative;
z-index: 2;
max-width: var(--maxw);
margin: 0 auto;
padding: 30px 24px 50px;
display: flex;
flex-wrap: wrap;
gap: 14px;
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--line);
color: var(--dim);
font-size: 0.86rem;
}
.to-top {
font-family: var(--display);
font-weight: 600;
font-size: 0.86rem;
color: var(--ink);
background: rgba(124, 92, 255, 0.1);
border: 1px solid var(--panel-edge);
border-radius: 999px;
padding: 9px 18px;
cursor: pointer;
transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.to-top:hover,
.to-top:focus-visible { transform: translateY(-2px); box-shadow: var(--glow); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
z-index: 90;
background: rgba(20, 20, 40, 0.95);
color: var(--ink);
border: 1px solid var(--accent);
border-radius: 12px;
padding: 12px 20px;
font-size: 0.92rem;
box-shadow: var(--glow);
opacity: 0;
pointer-events: none;
transition: opacity 0.28s var(--ease), transform 0.28s var(--ease);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Reveal on scroll ---------- */
[data-reveal] {
opacity: 0;
transform: translateY(26px);
transition: opacity 0.7s var(--ease), transform 0.7s var(--ease);
}
[data-reveal].in { opacity: 1; transform: none; }
/* ---------- Focus ---------- */
:focus-visible { outline: 2px solid var(--accent-2); outline-offset: 3px; border-radius: 6px; }
/* ---------- Responsive ---------- */
@media (max-width: 900px) {
.hero { grid-template-columns: 1fr; min-height: auto; }
.hero-stage { order: -1; min-height: 280px; }
.scroll-hint { position: static; margin-top: 18px; }
.about-grid { grid-template-columns: 1fr; }
.about-portrait { order: -1; }
.contact-form { grid-template-columns: 1fr; }
}
@media (max-width: 680px) {
.nav-links { display: none; }
.t-item { grid-template-columns: 1fr; padding-left: 44px; }
.t-period { grid-row: auto; }
.hero-stats { gap: 18px; }
}
@media (max-width: 400px) {
.section { padding-left: 18px; padding-right: 18px; }
.hero { padding-left: 18px; padding-right: 18px; }
.hero-stats { grid-template-columns: repeat(2, auto); }
}
/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
[data-reveal] { opacity: 1; transform: none; }
.scene, .project-link, .portrait-frame, .contact-inner { transform: none !important; }
}(function () {
"use strict";
var reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------------- Toast ---------------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2600);
}
/* ---------------- 3D tilt on hover (pointer-driven) ---------------- */
// Every [data-tilt] gets perspective tilt from the pointer position.
// The inner transformed element is the first child that owns --rx/--ry,
// falling back to the [data-tilt] element itself.
function tiltTargets(host) {
// Prefer a known inner element; otherwise the host.
return (
host.querySelector(
".scene, .project-link, .portrait-frame, .contact-inner"
) || host
);
}
var MAX_TILT = 12; // degrees
function bindTilt(host) {
var target = tiltTargets(host);
function apply(e) {
if (reduceMotion) return;
var rect = host.getBoundingClientRect();
var px = (e.clientX - rect.left) / rect.width; // 0..1
var py = (e.clientY - rect.top) / rect.height; // 0..1
px = Math.min(1, Math.max(0, px));
py = Math.min(1, Math.max(0, py));
var ry = (px - 0.5) * 2 * MAX_TILT; // left/right
var rx = (0.5 - py) * 2 * MAX_TILT; // up/down
target.style.setProperty("--rx", rx.toFixed(2) + "deg");
target.style.setProperty("--ry", ry.toFixed(2) + "deg");
// glare position for project cards
target.style.setProperty("--mx", (px * 100).toFixed(1) + "%");
target.style.setProperty("--my", (py * 100).toFixed(1) + "%");
// parallax inner layers
var layers = host.querySelectorAll(".pv-layer");
for (var i = 0; i < layers.length; i++) {
var depth = parseFloat(layers[i].getAttribute("data-depth")) || 0;
var tx = (px - 0.5) * depth;
var ty = (py - 0.5) * depth;
layers[i].style.transform =
"translate3d(" + tx.toFixed(1) + "px," + ty.toFixed(1) + "px," + depth + "px)";
}
}
function reset() {
target.style.setProperty("--rx", "0deg");
target.style.setProperty("--ry", "0deg");
var layers = host.querySelectorAll(".pv-layer");
for (var i = 0; i < layers.length; i++) {
layers[i].style.transform = "translate3d(0,0,0)";
}
}
host.addEventListener("pointermove", apply);
host.addEventListener("pointerleave", reset);
// Keyboard users: gentle tilt while focused inside
host.addEventListener("focusin", function () {
if (reduceMotion) return;
target.style.setProperty("--rx", "-4deg");
target.style.setProperty("--ry", "5deg");
});
host.addEventListener("focusout", reset);
}
var tiltHosts = document.querySelectorAll("[data-tilt]");
for (var t = 0; t < tiltHosts.length; t++) bindTilt(tiltHosts[t]);
/* ---------------- Device orientation (gyro) for the hero stage ---------------- */
// On phones, tilt the hero scene with the device gyroscope.
var heroStage = document.querySelector(".hero-stage[data-tilt]");
if (heroStage && !reduceMotion && window.DeviceOrientationEvent) {
var heroTarget = heroStage.querySelector(".scene");
var gyroActive = false;
window.addEventListener(
"deviceorientation",
function (e) {
if (e.beta == null || e.gamma == null) return;
gyroActive = true;
var rx = Math.max(-18, Math.min(18, (e.beta - 45) * 0.4));
var ry = Math.max(-18, Math.min(18, e.gamma * 0.4));
if (heroTarget) {
heroTarget.style.setProperty("--rx", rx.toFixed(2) + "deg");
heroTarget.style.setProperty("--ry", ry.toFixed(2) + "deg");
}
},
true
);
// mark so we know gyro may be in play (no-op fallback to pointer otherwise)
void gyroActive;
}
/* ---------------- Scroll progress bar ---------------- */
var progressBar = document.getElementById("progressBar");
function updateProgress() {
if (!progressBar) return;
var doc = document.documentElement;
var scrolled = doc.scrollTop || document.body.scrollTop;
var height = doc.scrollHeight - doc.clientHeight;
var pct = height > 0 ? (scrolled / height) * 100 : 0;
progressBar.style.width = pct + "%";
}
window.addEventListener("scroll", updateProgress, { passive: true });
updateProgress();
/* ---------------- Reveal on scroll + counters + skill bars ---------------- */
function animateCount(el) {
var target = parseInt(el.getAttribute("data-count"), 10) || 0;
if (reduceMotion) {
el.textContent = String(target);
return;
}
var start = null;
var dur = 1100;
function step(ts) {
if (start === null) start = ts;
var p = Math.min(1, (ts - start) / dur);
var eased = 1 - Math.pow(1 - p, 3);
el.textContent = String(Math.round(target * eased));
if (p < 1) requestAnimationFrame(step);
}
requestAnimationFrame(step);
}
function fillSkill(el) {
var pct = parseInt(el.getAttribute("data-skill"), 10) || 0;
el.style.width = pct + "%";
}
// Tag the things that should reveal/animate.
var revealNodes = document.querySelectorAll(
".section-head, .project, .about-portrait, .about-text > *, .t-item, .skill, .contact-inner, .hero-inner > *"
);
for (var r = 0; r < revealNodes.length; r++) {
revealNodes[r].setAttribute("data-reveal", "");
}
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (!entry.isIntersecting) return;
var el = entry.target;
el.classList.add("in");
var counters = el.querySelectorAll(".count");
for (var c = 0; c < counters.length; c++) animateCount(counters[c]);
var fills = el.querySelectorAll(".skill-fill");
for (var f = 0; f < fills.length; f++) fillSkill(fills[f]);
io.unobserve(el);
});
},
{ threshold: 0.18, rootMargin: "0px 0px -8% 0px" }
);
for (var n = 0; n < revealNodes.length; n++) io.observe(revealNodes[n]);
} else {
// Fallback: just show everything.
for (var m = 0; m < revealNodes.length; m++) {
revealNodes[m].classList.add("in");
var cc = revealNodes[m].querySelectorAll(".count");
for (var k = 0; k < cc.length; k++) animateCount(cc[k]);
var ff = revealNodes[m].querySelectorAll(".skill-fill");
for (var j = 0; j < ff.length; j++) fillSkill(ff[j]);
}
}
/* ---------------- Toast links (fictional) ---------------- */
var toastLinks = document.querySelectorAll("[data-toast]");
for (var tl = 0; tl < toastLinks.length; tl++) {
toastLinks[tl].addEventListener("click", function (e) {
e.preventDefault();
toast(this.getAttribute("data-toast"));
});
}
/* ---------------- Smooth in-page nav (respects reduced motion) ---------------- */
var anchors = document.querySelectorAll('a[href^="#"]');
for (var a = 0; a < anchors.length; a++) {
anchors[a].addEventListener("click", function (e) {
var id = this.getAttribute("href");
if (id === "#" || id.length < 2) return;
var dest = document.querySelector(id);
if (!dest) return;
e.preventDefault();
dest.scrollIntoView({
behavior: reduceMotion ? "auto" : "smooth",
block: "start"
});
if (history.replaceState) history.replaceState(null, "", id);
});
}
/* ---------------- Back to top ---------------- */
var toTop = document.getElementById("toTop");
if (toTop) {
toTop.addEventListener("click", function () {
window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" });
});
}
/* ---------------- Contact form validation ---------------- */
var form = document.getElementById("contactForm");
if (form) {
function setError(field, msg) {
var wrap = field.closest(".field");
var err = wrap ? wrap.querySelector(".err") : null;
if (wrap) wrap.classList.toggle("invalid", !!msg);
if (err) err.textContent = msg || "";
field.setAttribute("aria-invalid", msg ? "true" : "false");
}
function validEmail(v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
}
form.addEventListener("submit", function (e) {
e.preventDefault();
var name = form.querySelector("#cf-name");
var email = form.querySelector("#cf-email");
var msg = form.querySelector("#cf-msg");
var ok = true;
if (!name.value.trim()) {
setError(name, "Please enter your name.");
ok = false;
} else setError(name, "");
if (!email.value.trim()) {
setError(email, "Please enter your email.");
ok = false;
} else if (!validEmail(email.value.trim())) {
setError(email, "That email doesn't look right.");
ok = false;
} else setError(email, "");
if (!msg.value.trim()) {
setError(msg, "Tell me a little about the project.");
ok = false;
} else setError(msg, "");
if (!ok) {
var firstBad = form.querySelector(".field.invalid input, .field.invalid textarea");
if (firstBad) firstBad.focus();
return;
}
toast("Thanks, " + name.value.trim() + "! This is a demo — no message sent.");
form.reset();
});
// Clear inline error as the user fixes it.
var inputs = form.querySelectorAll("input, textarea");
for (var i = 0; i < inputs.length; i++) {
inputs[i].addEventListener("input", function () {
var wrap = this.closest(".field");
if (wrap && wrap.classList.contains("invalid")) {
wrap.classList.remove("invalid");
var err = wrap.querySelector(".err");
if (err) err.textContent = "";
this.setAttribute("aria-invalid", "false");
}
});
}
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Maya Okafor — Spatial Product Designer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="grid-floor" aria-hidden="true"></div>
<div class="glow glow-a" aria-hidden="true"></div>
<div class="glow glow-b" aria-hidden="true"></div>
<div class="noise" aria-hidden="true"></div>
<a class="skip-link" href="#work">Skip to work</a>
<div class="scroll-progress" aria-hidden="true"><span id="progressBar"></span></div>
<header class="nav" role="banner">
<a class="brand" href="#top">
<span class="brand-cube" aria-hidden="true">
<span class="face f-front">MO</span>
<span class="face f-back">MO</span>
<span class="face f-right"></span>
<span class="face f-left"></span>
<span class="face f-top"></span>
<span class="face f-bottom"></span>
</span>
<span class="brand-name">Maya Okafor</span>
</a>
<nav class="nav-links" aria-label="Primary">
<a href="#work">Work</a>
<a href="#about">About</a>
<a href="#experience">Experience</a>
<a href="#skills">Skills</a>
</nav>
<a href="#contact" class="nav-cta">Let’s talk</a>
</header>
<main id="top">
<!-- HERO -->
<section class="hero" aria-labelledby="hero-title">
<div class="hero-inner">
<p class="eyebrow"><span class="dot"></span> Available for select work — Q3 2026</p>
<h1 class="hero-title" id="hero-title">
<span class="line">Design that has</span>
<span class="line"><span class="grad">depth.</span></span>
</h1>
<p class="hero-sub">
I’m <strong>Maya Okafor</strong>, a spatial product designer building interfaces
that live in three dimensions — layered, tactile, and alive to your every move.
</p>
<div class="hero-actions">
<a href="#work" class="btn btn-primary">View selected work</a>
<a href="#contact" class="btn btn-ghost">Start a project</a>
</div>
<dl class="hero-stats">
<div class="stat">
<dt>Years designing</dt>
<dd><span class="count" data-count="9">0</span></dd>
</div>
<div class="stat">
<dt>Products shipped</dt>
<dd><span class="count" data-count="42">0</span></dd>
</div>
<div class="stat">
<dt>Avg. NPS lift</dt>
<dd>+<span class="count" data-count="27">0</span></dd>
</div>
</dl>
</div>
<!-- 3D rotating hero object -->
<div class="hero-stage" data-tilt aria-hidden="true">
<div class="scene">
<div class="cube" id="heroCube">
<span class="cube-face cf-front"><span>UI</span></span>
<span class="cube-face cf-back"><span>UX</span></span>
<span class="cube-face cf-right"><span>3D</span></span>
<span class="cube-face cf-left"><span>VR</span></span>
<span class="cube-face cf-top"><span>★</span></span>
<span class="cube-face cf-bottom"><span>◆</span></span>
</div>
<span class="ring ring-1"></span>
<span class="ring ring-2"></span>
</div>
</div>
<div class="scroll-hint" aria-hidden="true">
<span>Scroll</span>
<span class="scroll-line"></span>
</div>
</section>
<!-- MARQUEE -->
<div class="marquee" aria-hidden="true">
<div class="marquee-track">
<span>Spatial UI</span><span>◆</span>
<span>Depth & Parallax</span><span>◆</span>
<span>Prototyping</span><span>◆</span>
<span>Design Systems</span><span>◆</span>
<span>Interaction</span><span>◆</span>
<span>Spatial UI</span><span>◆</span>
<span>Depth & Parallax</span><span>◆</span>
<span>Prototyping</span><span>◆</span>
<span>Design Systems</span><span>◆</span>
<span>Interaction</span><span>◆</span>
</div>
</div>
<!-- WORK -->
<section class="section work" id="work" aria-labelledby="work-title">
<div class="section-head">
<span class="section-index">01</span>
<h2 class="section-title" id="work-title">Selected work</h2>
<p class="section-lead">Six products where depth, layering, and motion turn flat screens
into spaces you can almost reach into.</p>
</div>
<ol class="projects">
<li class="project" style="--accent:#7c5cff" data-tilt>
<a class="project-link" href="#" data-toast="Case study coming soon — Lumen Health">
<span class="project-visual visual-1" aria-hidden="true">
<span class="pv-layer pv-l1" data-depth="40"></span>
<span class="pv-layer pv-l2" data-depth="22"></span>
<span class="pv-layer pv-l3" data-depth="8"></span>
</span>
<span class="project-body">
<span class="project-num">P-01</span>
<span class="project-name">Lumen Health</span>
<span class="project-desc">A care companion where layered cards lift toward you as
medication steps unfold — guidance with literal depth.</span>
<span class="project-tags"><span>Mobile</span><span>Health</span><span>Spatial</span></span>
<span class="project-go">Open case study <span aria-hidden="true">→</span></span>
</span>
</a>
</li>
<li class="project" style="--accent:#22d3ee" data-tilt>
<a class="project-link" href="#" data-toast="Case study coming soon — Drift Banking">
<span class="project-visual visual-2" aria-hidden="true">
<span class="pv-layer pv-l1" data-depth="40"></span>
<span class="pv-layer pv-l2" data-depth="22"></span>
<span class="pv-layer pv-l3" data-depth="8"></span>
</span>
<span class="project-body">
<span class="project-num">P-02</span>
<span class="project-name">Drift Banking</span>
<span class="project-desc">Stacked balance cards float in z-space; transfers slide
between planes so money feels tangible and physical.</span>
<span class="project-tags"><span>Fintech</span><span>Web</span><span>Data viz</span></span>
<span class="project-go">Open case study <span aria-hidden="true">→</span></span>
</span>
</a>
</li>
<li class="project" style="--accent:#f97316" data-tilt>
<a class="project-link" href="#" data-toast="Case study coming soon — Atlas Maps">
<span class="project-visual visual-3" aria-hidden="true">
<span class="pv-layer pv-l1" data-depth="40"></span>
<span class="pv-layer pv-l2" data-depth="22"></span>
<span class="pv-layer pv-l3" data-depth="8"></span>
</span>
<span class="project-body">
<span class="project-num">P-03</span>
<span class="project-name">Atlas Maps</span>
<span class="project-desc">A spatial design system where map, list, and detail live on
separate depth planes you tilt and dive between.</span>
<span class="project-tags"><span>Design system</span><span>Maps</span><span>Cross-platform</span></span>
<span class="project-go">Open case study <span aria-hidden="true">→</span></span>
</span>
</a>
</li>
<li class="project" style="--accent:#34d399" data-tilt>
<a class="project-link" href="#" data-toast="Case study coming soon — Verdant">
<span class="project-visual visual-4" aria-hidden="true">
<span class="pv-layer pv-l1" data-depth="40"></span>
<span class="pv-layer pv-l2" data-depth="22"></span>
<span class="pv-layer pv-l3" data-depth="8"></span>
</span>
<span class="project-body">
<span class="project-num">P-04</span>
<span class="project-name">Verdant</span>
<span class="project-desc">A carbon dashboard where forests rise out of the canvas in
3D as real reductions land — progress with volume.</span>
<span class="project-tags"><span>Climate</span><span>Dashboard</span><span>SVG depth</span></span>
<span class="project-go">Open case study <span aria-hidden="true">→</span></span>
</span>
</a>
</li>
<li class="project" style="--accent:#f472b6" data-tilt>
<a class="project-link" href="#" data-toast="Case study coming soon — Cadence">
<span class="project-visual visual-5" aria-hidden="true">
<span class="pv-layer pv-l1" data-depth="40"></span>
<span class="pv-layer pv-l2" data-depth="22"></span>
<span class="pv-layer pv-l3" data-depth="8"></span>
</span>
<span class="project-body">
<span class="project-num">P-05</span>
<span class="project-name">Cadence</span>
<span class="project-desc">A music collaboration tool where waveforms extrude into
parallax ribbons that respond as the beat plays.</span>
<span class="project-tags"><span>Audio</span><span>Web app</span><span>Realtime</span></span>
<span class="project-go">Open case study <span aria-hidden="true">→</span></span>
</span>
</a>
</li>
<li class="project" style="--accent:#facc15" data-tilt>
<a class="project-link" href="#" data-toast="Case study coming soon — Nimbus">
<span class="project-visual visual-6" aria-hidden="true">
<span class="pv-layer pv-l1" data-depth="40"></span>
<span class="pv-layer pv-l2" data-depth="22"></span>
<span class="pv-layer pv-l3" data-depth="8"></span>
</span>
<span class="project-body">
<span class="project-num">P-06</span>
<span class="project-name">Nimbus</span>
<span class="project-desc">A weather onboarding where the sky is a deep, layered scene
that tilts with your device toward the horizon.</span>
<span class="project-tags"><span>Onboarding</span><span>Mobile</span><span>Parallax</span></span>
<span class="project-go">Open case study <span aria-hidden="true">→</span></span>
</span>
</a>
</li>
</ol>
</section>
<!-- ABOUT -->
<section class="section about" id="about" aria-labelledby="about-title">
<div class="about-grid">
<div class="about-portrait" data-tilt aria-hidden="true">
<div class="portrait-frame">
<span class="portrait-glow"></span>
<div class="portrait-face">MO</div>
<span class="portrait-edge"></span>
</div>
</div>
<div class="about-text">
<span class="section-index">02</span>
<h2 class="section-title" id="about-title">About</h2>
<p>
I design products with dimension. For nine years I’ve worked at the seam between
interaction and space — first at agencies, then leading spatial design at two
venture-backed startups, now independently with teams who believe the third axis
matters as much as the first two.
</p>
<p>
My process is part prototype, part choreography. I model depth, tune perspective and
easing by hand, and obsess over the parallax that decides whether a product feels solid
or merely drawn. I care deeply about accessibility — every effect respects
reduced-motion and stays fully keyboard-usable.
</p>
<ul class="about-pills">
<li>Based in Lisbon</li>
<li>Working globally</li>
<li>Remote-first</li>
</ul>
</div>
</div>
</section>
<!-- EXPERIENCE -->
<section class="section experience" id="experience" aria-labelledby="exp-title">
<div class="section-head">
<span class="section-index">03</span>
<h2 class="section-title" id="exp-title">Experience</h2>
</div>
<ol class="timeline">
<li class="t-item" data-tilt>
<span class="t-dot" aria-hidden="true"></span>
<span class="t-period">2023 — Now</span>
<span class="t-role">Independent Spatial & Product Designer</span>
<span class="t-org">Self-employed · Lisbon</span>
<span class="t-desc">Partnering with seed-to-Series-B teams to design dimensional
interfaces and reusable depth systems.</span>
</li>
<li class="t-item" data-tilt>
<span class="t-dot" aria-hidden="true"></span>
<span class="t-period">2020 — 2023</span>
<span class="t-role">Lead Product Designer</span>
<span class="t-org">Northwind · Berlin</span>
<span class="t-desc">Built the spatial language and design system used across six
products; mentored a team of four.</span>
</li>
<li class="t-item" data-tilt>
<span class="t-dot" aria-hidden="true"></span>
<span class="t-period">2017 — 2020</span>
<span class="t-role">Senior Interaction Designer</span>
<span class="t-org">Field Studio · London</span>
<span class="t-desc">Prototyped award-winning onboarding flows for fintech and
health clients.</span>
</li>
<li class="t-item" data-tilt>
<span class="t-dot" aria-hidden="true"></span>
<span class="t-period">2015 — 2017</span>
<span class="t-role">Product Designer</span>
<span class="t-org">Cobalt Labs · London</span>
<span class="t-desc">Cut my teeth shipping consumer mobile apps end to end.</span>
</li>
</ol>
</section>
<!-- SKILLS -->
<section class="section skills" id="skills" aria-labelledby="skills-title">
<div class="section-head">
<span class="section-index">04</span>
<h2 class="section-title" id="skills-title">Skills</h2>
<p class="section-lead">Where I add the most value, with a self-assessed depth bar.</p>
</div>
<ul class="skill-list">
<li class="skill">
<span class="skill-name">Spatial & Interaction Design</span>
<span class="skill-bar"><span class="skill-fill" data-skill="96"></span></span>
</li>
<li class="skill">
<span class="skill-name">Prototyping (code & tools)</span>
<span class="skill-bar"><span class="skill-fill" data-skill="90"></span></span>
</li>
<li class="skill">
<span class="skill-name">Design Systems</span>
<span class="skill-bar"><span class="skill-fill" data-skill="88"></span></span>
</li>
<li class="skill">
<span class="skill-name">UX Research</span>
<span class="skill-bar"><span class="skill-fill" data-skill="74"></span></span>
</li>
<li class="skill">
<span class="skill-name">Front-end (HTML/CSS/JS)</span>
<span class="skill-bar"><span class="skill-fill" data-skill="82"></span></span>
</li>
</ul>
</section>
<!-- CONTACT -->
<section class="section contact" id="contact" aria-labelledby="contact-title">
<div class="contact-inner" data-tilt>
<span class="section-index">05</span>
<h2 class="contact-title" id="contact-title">
Let’s build something with <span class="grad">depth.</span>
</h2>
<p class="contact-lead">Have a product that deserves a third dimension? I take on a few
collaborations each quarter.</p>
<form class="contact-form" id="contactForm" novalidate>
<div class="field">
<label for="cf-name">Name</label>
<input id="cf-name" name="name" type="text" autocomplete="name" required />
<span class="err" data-err-for="cf-name"></span>
</div>
<div class="field">
<label for="cf-email">Email</label>
<input id="cf-email" name="email" type="email" autocomplete="email" required />
<span class="err" data-err-for="cf-email"></span>
</div>
<div class="field field-wide">
<label for="cf-msg">What are you building?</label>
<textarea id="cf-msg" name="message" rows="4" required></textarea>
<span class="err" data-err-for="cf-msg"></span>
</div>
<button class="btn btn-primary btn-block" type="submit">Send message</button>
</form>
<div class="contact-links">
<a href="#" data-toast="Fictional link — maya@okafor.design">Email</a>
<a href="#" data-toast="Fictional link — @mayaokafor">Twitter</a>
<a href="#" data-toast="Fictional link — in/mayaokafor">LinkedIn</a>
<a href="#" data-toast="Fictional link — dribbble.com/maya">Dribbble</a>
</div>
</div>
</section>
</main>
<footer class="footer" role="contentinfo">
<p>© 2026 Maya Okafor — fictional portfolio. Designed with depth in mind.</p>
<button class="to-top" id="toTop" aria-label="Back to top">↑ Top</button>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>3D / Interactive Portfolio
A complete single-page portfolio for the fictional spatial product designer Maya Okafor, re-skinned into an immersive 3D theme. The whole scene is built from CSS 3D transforms only — no WebGL, no libraries. A perspective grid floor, two drifting neon glows, and a fine noise layer set a dark, display-type mood, while the hero pairs a depth-driven headline with a perpetually rotating cube wrapped in two orbiting rings. The same content as the neutral primitives — hero, work, about, experience, skills, and contact — is composed here as interactive cards that respond to depth and movement.
The signature interaction is pointer-driven tilt. Every data-tilt surface (project cards, the portrait, the contact panel, the hero stage) computes rotateX/rotateY from the mouse position in real time, adds a glare highlight that tracks the cursor, and parallaxes its inner layers along the z-axis so artwork lifts toward you. On phones the hero cube also reacts to the device gyroscope. Keyboard users get a gentle tilt on focus, and project links, the back-to-top button, and the contact form are all fully operable without a mouse.
Supporting interactions round it out: a scroll progress bar, IntersectionObserver reveals, animated stat counters, fill-on-view skill bars, toast feedback on the fictional links, smooth in-page navigation, and a client-side validated contact form. The layout collapses gracefully from two columns to one down to roughly 360px, and a prefers-reduced-motion query disables every rotation, parallax, and float so the page stays calm and readable for visitors who ask for it.
Illustrative portfolio — fictional person and projects.