Portfolio — Creative / Visual CV
A bold one-page creative resume for a fictional product designer, built with serif display type and a swappable accent color. It pairs a circular portrait orb and intro with a four-figure stats band whose numbers count up on load, honest skill bars that fill into view, an icon-led experience timeline, a selected-work grid, an about block, and a contact card. A print-friendly toggle strips color and animation for a clean, hireable printout.
MCP
Code
/* ============================================================
Maya Okafor — Creative / Visual CV
Palette + tokens FIRST. Accent is swappable via [data-accent].
============================================================ */
:root {
--accent: #ff5a5f;
--accent-soft: #ffe6e7;
--accent-deep: #d8383d;
--ink: #1c1c22;
--ink-2: #4a4a55;
--muted: #7a7a86;
--line: #e9e7e2;
--paper: #fbf9f5;
--card: #ffffff;
--halo: #f3f0ea;
--font-display: "Fraunces", Georgia, "Times New Roman", serif;
--font-body: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--r-sm: 10px;
--r-md: 16px;
--r-lg: 26px;
--shadow: 0 18px 50px -28px rgba(28, 28, 34, 0.45);
--shadow-sm: 0 8px 24px -16px rgba(28, 28, 34, 0.4);
--maxw: 1060px;
--ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}
/* Accent themes */
[data-accent="violet"] { --accent:#7c5cff; --accent-soft:#ece7ff; --accent-deep:#5b3fe0; }
[data-accent="teal"] { --accent:#10b8a6; --accent-soft:#dcf6f2; --accent-deep:#0a8d80; }
[data-accent="amber"] { --accent:#f5a524; --accent-soft:#fdf0d6; --accent-deep:#cf8408; }
[data-accent="ink"] { --accent:#3b4a6b; --accent-soft:#e4e8f1; --accent-deep:#283450; }
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--font-body);
color: var(--ink);
background:
radial-gradient(900px 520px at 12% -8%, var(--accent-soft), transparent 60%),
radial-gradient(760px 480px at 108% 4%, var(--halo), transparent 55%),
var(--paper);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
.skip-link {
position: absolute;
left: 12px; top: -48px;
background: var(--ink);
color: #fff;
padding: 10px 16px;
border-radius: 0 0 var(--r-sm) var(--r-sm);
z-index: 60;
transition: top 0.18s var(--ease);
text-decoration: none;
}
.skip-link:focus { top: 0; }
main { display: block; }
a { color: inherit; }
:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 3px;
border-radius: 6px;
}
/* ---------- Control dock ---------- */
.dock {
position: fixed;
right: 16px; bottom: 16px;
z-index: 50;
display: flex;
align-items: center;
gap: 14px;
padding: 10px 12px;
background: rgba(255, 255, 255, 0.86);
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--shadow);
flex-wrap: wrap;
}
.dock-group { display: flex; align-items: center; gap: 8px; }
.dock-label {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--muted);
}
.swatches { display: flex; gap: 6px; }
.swatch {
width: 22px; height: 22px;
border-radius: 50%;
border: 2px solid #fff;
background: var(--sw);
cursor: pointer;
box-shadow: 0 0 0 1px var(--line);
transition: transform 0.16s var(--ease), box-shadow 0.16s var(--ease);
padding: 0;
}
.swatch:hover { transform: scale(1.12); }
.swatch.is-active { box-shadow: 0 0 0 2px var(--ink); transform: scale(1.12); }
.dock-btn {
display: inline-flex;
align-items: center;
gap: 7px;
border: 1px solid var(--line);
background: #fff;
color: var(--ink);
font: inherit;
font-size: 0.84rem;
font-weight: 600;
padding: 8px 14px;
border-radius: 999px;
cursor: pointer;
transition: background 0.16s var(--ease), color 0.16s var(--ease), border-color 0.16s var(--ease);
}
.dock-btn:hover { border-color: var(--accent); color: var(--accent-deep); }
.dock-btn[aria-pressed="true"] {
background: var(--ink);
color: #fff;
border-color: var(--ink);
}
.dock-ico { font-size: 1rem; }
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font: inherit;
font-weight: 600;
font-size: 0.96rem;
padding: 13px 24px;
border-radius: 999px;
cursor: pointer;
text-decoration: none;
border: 1.5px solid transparent;
transition: transform 0.16s var(--ease), box-shadow 0.16s var(--ease),
background 0.16s var(--ease), color 0.16s var(--ease);
}
.btn:active { transform: translateY(1px); }
.btn-primary {
background: var(--accent);
color: #fff;
box-shadow: 0 12px 26px -14px var(--accent);
}
.btn-primary:hover { background: var(--accent-deep); box-shadow: 0 16px 30px -12px var(--accent); }
.btn-ghost {
background: transparent;
color: var(--ink);
border-color: var(--ink);
}
.btn-ghost:hover { background: var(--ink); color: #fff; }
/* ---------- Hero ---------- */
.hero {
max-width: var(--maxw);
margin: 0 auto;
padding: clamp(2.2rem, 6vw, 5rem) clamp(1.1rem, 4vw, 2.2rem) clamp(1.6rem, 4vw, 3rem);
}
.hero-grid {
display: grid;
grid-template-columns: 220px 1fr;
gap: clamp(1.4rem, 4vw, 3rem);
align-items: center;
}
.hero-portrait {
position: relative;
width: 220px;
height: 220px;
justify-self: center;
}
.portrait-orb {
position: absolute;
inset: 14px;
border-radius: 50%;
background:
radial-gradient(circle at 32% 26%, #fff5, transparent 45%),
linear-gradient(150deg, var(--accent), var(--accent-deep));
display: grid;
place-items: center;
box-shadow: var(--shadow);
}
.portrait-initials {
font-family: var(--font-display);
font-weight: 600;
font-size: 3.4rem;
color: #fff;
letter-spacing: 0.02em;
}
.portrait-ring {
position: absolute;
inset: 0;
border-radius: 50%;
border: 2px dashed var(--accent);
opacity: 0.55;
animation: spin 26s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.eyebrow {
margin: 0 0 0.4rem;
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--accent-deep);
}
.hero-name {
font-family: var(--font-display);
font-weight: 600;
font-size: clamp(2.8rem, 9vw, 5.2rem);
line-height: 0.98;
margin: 0 0 0.7rem;
letter-spacing: -0.02em;
}
.hero-tag {
font-size: clamp(1.05rem, 2.4vw, 1.35rem);
color: var(--ink-2);
max-width: 30ch;
margin: 0 0 1.3rem;
}
.hero-tag em { font-style: italic; color: var(--accent-deep); font-family: var(--font-display); }
.hero-meta {
list-style: none;
margin: 0 0 1.6rem;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.4rem;
font-size: 0.92rem;
color: var(--ink-2);
}
.hero-meta a { text-decoration: none; }
.hero-meta a:hover { color: var(--accent-deep); }
.meta-ico { margin-right: 5px; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 0.8rem; }
/* ---------- Stats band ---------- */
.stats {
max-width: var(--maxw);
margin: 1rem auto;
padding: 1.6rem clamp(1.1rem, 4vw, 2.2rem);
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
.stat {
background: var(--card);
padding: 1.4rem 1rem;
text-align: center;
}
.stat-num {
display: block;
font-family: var(--font-display);
font-weight: 600;
font-size: clamp(2rem, 5vw, 2.9rem);
line-height: 1;
color: var(--accent-deep);
font-variant-numeric: tabular-nums;
}
.stat-label {
display: block;
margin-top: 0.4rem;
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.02em;
color: var(--muted);
text-transform: uppercase;
}
/* ---------- Sections ---------- */
.section {
max-width: var(--maxw);
margin: clamp(2.4rem, 6vw, 4.4rem) auto;
padding: 0 clamp(1.1rem, 4vw, 2.2rem);
}
.section-head { margin-bottom: 1.8rem; }
.section-title {
font-family: var(--font-display);
font-weight: 600;
font-size: clamp(1.6rem, 4vw, 2.4rem);
margin: 0;
letter-spacing: -0.01em;
display: flex;
align-items: baseline;
gap: 0.6rem;
}
.title-num {
font-family: var(--font-body);
font-size: 0.9rem;
font-weight: 700;
color: var(--accent-deep);
background: var(--accent-soft);
padding: 3px 9px;
border-radius: 999px;
letter-spacing: 0.04em;
}
.section-sub {
margin: 0.5rem 0 0;
color: var(--muted);
font-size: 0.98rem;
}
/* ---------- Skills ---------- */
.skills {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: clamp(1.6rem, 4vw, 3rem);
}
.bars { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.25rem; }
.bar {
display: grid;
grid-template-columns: 1fr auto;
grid-template-areas: "name pct" "track track";
gap: 0.35rem 0.6rem;
align-items: center;
}
.bar-name { grid-area: name; font-weight: 600; font-size: 0.98rem; }
.bar-pct {
grid-area: pct;
font-variant-numeric: tabular-nums;
font-weight: 600;
font-size: 0.88rem;
color: var(--accent-deep);
}
.bar-track {
grid-area: track;
height: 12px;
border-radius: 999px;
background: var(--halo);
overflow: hidden;
box-shadow: inset 0 0 0 1px var(--line);
}
.bar-fill {
display: block;
height: 100%;
width: 0%;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent), var(--accent-deep));
transition: width 1.1s var(--ease);
}
.constellation-h {
font-family: var(--font-display);
font-weight: 500;
font-size: 1.05rem;
margin: 0 0 1rem;
}
.chips { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 0.6rem; }
.chip {
padding: 8px 14px;
border-radius: 999px;
background: var(--card);
border: 1px solid var(--line);
font-size: 0.88rem;
font-weight: 500;
box-shadow: var(--shadow-sm);
transition: transform 0.18s var(--ease), border-color 0.18s var(--ease), color 0.18s var(--ease);
animation: floaty 6s ease-in-out infinite;
animation-delay: calc(var(--d) * -0.7s);
}
.chip:hover { transform: translateY(-3px); border-color: var(--accent); color: var(--accent-deep); }
@keyframes floaty {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
/* ---------- Timeline ---------- */
.timeline {
list-style: none;
margin: 0;
padding: 0;
position: relative;
}
.timeline::before {
content: "";
position: absolute;
left: 22px; top: 8px; bottom: 8px;
width: 2px;
background: linear-gradient(var(--accent), var(--line));
}
.t-item {
position: relative;
display: grid;
grid-template-columns: 46px 1fr;
gap: 1rem;
padding: 0 0 1.6rem;
}
.t-item:last-child { padding-bottom: 0; }
.t-ico {
z-index: 1;
width: 46px; height: 46px;
display: grid;
place-items: center;
border-radius: 50%;
background: var(--card);
border: 2px solid var(--accent);
font-size: 1.2rem;
box-shadow: var(--shadow-sm);
}
.t-body {
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 1.1rem 1.3rem;
box-shadow: var(--shadow-sm);
}
.t-top {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
gap: 0.3rem 1rem;
}
.t-role { font-family: var(--font-display); font-weight: 600; font-size: 1.18rem; margin: 0; }
.t-when {
font-size: 0.82rem;
font-weight: 600;
color: var(--accent-deep);
background: var(--accent-soft);
padding: 3px 10px;
border-radius: 999px;
white-space: nowrap;
}
.t-org { margin: 0.25rem 0 0.5rem; color: var(--muted); font-weight: 500; font-size: 0.9rem; }
.t-desc { margin: 0; color: var(--ink-2); font-size: 0.96rem; }
/* ---------- Work grid ---------- */
.work-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.3rem;
}
.proj {
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 1.1rem;
box-shadow: var(--shadow-sm);
transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.proj:hover { transform: translateY(-5px); box-shadow: var(--shadow); }
.proj-art {
height: 150px;
border-radius: var(--r-sm);
margin-bottom: 1rem;
position: relative;
overflow: hidden;
}
.proj-art-1 { background: linear-gradient(135deg, #ff8a8e, #b34b9e); }
.proj-art-2 { background: linear-gradient(135deg, #5ad0c0, #2f7de0); }
.proj-art-3 { background: linear-gradient(135deg, #ffd479, #f59542); }
.proj-art-4 { background: linear-gradient(135deg, #9b8cff, #5b3fe0); }
.proj-art::after {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 78% 20%, rgba(255,255,255,0.45), transparent 40%),
repeating-linear-gradient(45deg, rgba(255,255,255,0.08) 0 10px, transparent 10px 20px);
}
.proj-name { font-family: var(--font-display); font-weight: 600; font-size: 1.22rem; margin: 0 0 0.2rem; }
.proj-line { margin: 0 0 0.55rem; font-size: 0.82rem; font-weight: 600; color: var(--accent-deep); text-transform: uppercase; letter-spacing: 0.04em; }
.proj-desc { margin: 0; color: var(--ink-2); font-size: 0.95rem; }
/* ---------- About ---------- */
.about-body {
display: grid;
grid-template-columns: 1fr 240px;
gap: clamp(1.4rem, 4vw, 2.6rem);
align-items: start;
}
.about-body p { margin: 0 0 1rem; font-size: 1.05rem; color: var(--ink-2); max-width: 56ch; }
.about-body strong { color: var(--ink); }
.about-facts {
list-style: none;
margin: 0;
padding: 1.2rem;
background: var(--card);
border: 1px solid var(--line);
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
display: grid;
gap: 0.85rem;
}
.about-facts li { display: grid; gap: 1px; }
.fact-k { font-size: 0.74rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.fact-v { font-family: var(--font-display); font-weight: 500; font-size: 1.05rem; }
/* ---------- Contact ---------- */
.contact-card {
background: linear-gradient(150deg, var(--ink), #2c2c38);
color: #fff;
border-radius: var(--r-lg);
padding: clamp(2rem, 6vw, 3.4rem);
text-align: center;
box-shadow: var(--shadow);
position: relative;
overflow: hidden;
}
.contact-card::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(560px 300px at 80% -10%, var(--accent), transparent 60%);
opacity: 0.5;
}
.contact-title, .contact-sub, .contact-links, .socials { position: relative; }
.contact-title {
font-family: var(--font-display);
font-weight: 600;
font-size: clamp(1.7rem, 5vw, 2.6rem);
margin: 0 0 0.6rem;
letter-spacing: -0.01em;
}
.contact-sub { margin: 0 auto 1.6rem; max-width: 46ch; color: #d8d8e0; font-size: 1.02rem; }
.contact-links { display: flex; flex-wrap: wrap; gap: 0.8rem; justify-content: center; }
.contact-card .btn-ghost { color: #fff; border-color: #fff; }
.contact-card .btn-ghost:hover { background: #fff; color: var(--ink); }
.socials {
list-style: none;
margin: 1.6rem 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 1.4rem;
justify-content: center;
font-size: 0.92rem;
}
.socials a { color: #d8d8e0; text-decoration: none; border-bottom: 1px solid transparent; transition: color 0.16s, border-color 0.16s; }
.socials a:hover { color: #fff; border-bottom-color: var(--accent); }
/* ---------- Footer ---------- */
.footer {
max-width: var(--maxw);
margin: 2.4rem auto 5.5rem;
padding: 1.4rem clamp(1.1rem, 4vw, 2.2rem);
border-top: 1px solid var(--line);
text-align: center;
color: var(--muted);
font-size: 0.85rem;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 88px;
transform: translate(-50%, 20px);
background: var(--ink);
color: #fff;
padding: 11px 20px;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 500;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s var(--ease), transform 0.22s var(--ease);
z-index: 80;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.skills { grid-template-columns: 1fr; }
.about-body { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.hero-grid { grid-template-columns: 1fr; text-align: center; }
.hero-portrait { justify-self: center; }
.hero-tag { margin-left: auto; margin-right: auto; }
.hero-meta { justify-content: center; }
.hero-actions { justify-content: center; }
.stats { grid-template-columns: repeat(2, 1fr); }
.work-grid { grid-template-columns: 1fr; }
}
@media (max-width: 420px) {
.stats { grid-template-columns: 1fr 1fr; }
.dock { right: 8px; left: 8px; bottom: 8px; justify-content: center; }
.dock-btn-label { display: none; }
}
/* ---------- Print mode (toggle + actual @media print) ---------- */
body.print-mode {
--paper: #fff;
--halo: #f3f3f3;
background: #fff;
}
body.print-mode .dock,
body.print-mode .portrait-ring,
body.print-mode .hero-actions,
body.print-mode .contact-links,
body.print-mode .socials { display: none; }
body.print-mode .chip { animation: none; box-shadow: none; }
body.print-mode .contact-card {
background: #fff;
color: var(--ink);
border: 1px solid var(--line);
box-shadow: none;
}
body.print-mode .contact-card::before { display: none; }
body.print-mode .contact-sub { color: var(--ink-2); }
body.print-mode .section,
body.print-mode .hero,
body.print-mode .stats { box-shadow: none; }
body.print-mode .proj:hover { transform: none; }
@media print {
.dock, .portrait-ring, .skip-link, .hero-actions, .toast { display: none !important; }
body { background: #fff; }
.proj, .t-body, .stats, .about-facts { box-shadow: none; }
.section { break-inside: avoid; }
}
@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;
}
}/* ============================================================
Maya Okafor — Creative / Visual CV
Vanilla JS: animated stat counters, skill bars on view,
accent picker (persisted), print-mode toggle, copy email.
============================================================ */
(function () {
"use strict";
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
/* ---------- Toast helper ---------- */
const toastEl = document.getElementById("toast");
let toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(() => toastEl.classList.remove("show"), 2200);
}
/* ---------- Animated number counter ---------- */
function animateCount(el, target, suffix) {
if (prefersReduced) {
el.textContent = target + suffix;
return;
}
const duration = 1300;
const start = performance.now();
function tick(now) {
const p = Math.min((now - start) / duration, 1);
// easeOutCubic
const eased = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(eased * target) + suffix;
if (p < 1) requestAnimationFrame(tick);
else el.textContent = target + suffix;
}
requestAnimationFrame(tick);
}
/* ---------- Skill bar fill ---------- */
function fillBar(barEl) {
const level = parseInt(barEl.getAttribute("data-level"), 10) || 0;
const fill = barEl.querySelector(".bar-fill");
const pct = barEl.querySelector("[data-bar-pct]");
if (fill) fill.style.width = level + "%";
if (pct) {
if (prefersReduced) {
pct.textContent = level + "%";
} else {
const start = performance.now();
const dur = 1100;
(function tick(now) {
const p = Math.min((now - start) / dur, 1);
const eased = 1 - Math.pow(1 - p, 3);
pct.textContent = Math.round(eased * level) + "%";
if (p < 1) requestAnimationFrame(tick);
else pct.textContent = level + "%";
})(performance.now());
}
}
}
/* ---------- IntersectionObserver to trigger on view ---------- */
const statNodes = Array.from(document.querySelectorAll(".stat"));
const barNodes = Array.from(document.querySelectorAll(".bar"));
function runStat(stat) {
const numEl = stat.querySelector("[data-stat]");
const target = parseInt(stat.getAttribute("data-target"), 10) || 0;
const suffix = stat.getAttribute("data-suffix") || "";
if (numEl) animateCount(numEl, target, suffix);
}
if ("IntersectionObserver" in window) {
const io = new IntersectionObserver(
(entries, obs) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
const el = entry.target;
if (el.classList.contains("stat")) runStat(el);
if (el.classList.contains("bar")) fillBar(el);
obs.unobserve(el);
});
},
{ threshold: 0.4 }
);
statNodes.forEach((n) => io.observe(n));
barNodes.forEach((n) => io.observe(n));
} else {
// Fallback: just run everything immediately
statNodes.forEach(runStat);
barNodes.forEach(fillBar);
}
/* ---------- Accent picker (persisted) ---------- */
const STORAGE_KEY = "maya-cv-accent";
const swatches = Array.from(document.querySelectorAll(".swatch"));
function applyAccent(name) {
if (name && name !== "coral") {
document.documentElement.setAttribute("data-accent", name);
} else {
document.documentElement.removeAttribute("data-accent");
}
swatches.forEach((s) => {
const active = s.getAttribute("data-accent") === (name || "coral");
s.classList.toggle("is-active", active);
s.setAttribute("aria-pressed", active ? "true" : "false");
});
}
swatches.forEach((s) => {
s.addEventListener("click", () => {
const name = s.getAttribute("data-accent");
applyAccent(name);
try {
localStorage.setItem(STORAGE_KEY, name);
} catch (e) {
/* storage may be unavailable in sandboxed iframe */
}
toast("Accent set to " + name);
});
});
// Restore saved accent
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) applyAccent(saved);
} catch (e) {
/* ignore */
}
/* ---------- Print-mode toggle ---------- */
const printToggle = document.getElementById("printToggle");
if (printToggle) {
printToggle.addEventListener("click", () => {
const on = document.body.classList.toggle("print-mode");
printToggle.setAttribute("aria-pressed", on ? "true" : "false");
const label = printToggle.querySelector(".dock-btn-label");
if (label) label.textContent = on ? "Color mode" : "Print mode";
toast(on ? "Print-friendly mode on" : "Back to color mode");
});
}
/* ---------- Download CV (demo) ---------- */
const downloadBtn = document.getElementById("downloadBtn");
if (downloadBtn) {
downloadBtn.addEventListener("click", () => {
toast("CV download is a demo — print mode is print-ready though.");
});
}
/* ---------- Copy email ---------- */
const copyEmail = document.getElementById("copyEmail");
const EMAIL = "[email protected]";
if (copyEmail) {
copyEmail.addEventListener("click", async () => {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(EMAIL);
} else {
throw new Error("no clipboard");
}
toast("Email copied — " + EMAIL);
} catch (e) {
// Fallback for sandboxed contexts
const ta = document.createElement("textarea");
ta.value = EMAIL;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
toast("Email copied — " + EMAIL);
} catch (err) {
toast(EMAIL);
}
document.body.removeChild(ta);
}
});
}
/* ---------- Social link toasts (demo) ---------- */
document.querySelectorAll("[data-toast]").forEach((a) => {
a.addEventListener("click", (e) => {
e.preventDefault();
toast(a.getAttribute("data-toast"));
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Maya Okafor — Creative CV</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=Fraunces:opsz,[email protected],400;9..144,500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<!-- Floating control dock -->
<div class="dock" role="region" aria-label="Display controls">
<div class="dock-group" role="group" aria-label="Accent color">
<span class="dock-label">Accent</span>
<div class="swatches">
<button class="swatch is-active" data-accent="coral" style="--sw:#ff5a5f" aria-label="Coral accent" aria-pressed="true"></button>
<button class="swatch" data-accent="violet" style="--sw:#7c5cff" aria-label="Violet accent" aria-pressed="false"></button>
<button class="swatch" data-accent="teal" style="--sw:#10b8a6" aria-label="Teal accent" aria-pressed="false"></button>
<button class="swatch" data-accent="amber" style="--sw:#f5a524" aria-label="Amber accent" aria-pressed="false"></button>
<button class="swatch" data-accent="ink" style="--sw:#3b4a6b" aria-label="Ink blue accent" aria-pressed="false"></button>
</div>
</div>
<button class="dock-btn" id="printToggle" type="button" aria-pressed="false">
<span class="dock-ico" aria-hidden="true">🖨</span>
<span class="dock-btn-label">Print mode</span>
</button>
</div>
<main id="main">
<!-- ===== HERO ===== -->
<header class="hero" aria-label="Introduction">
<div class="hero-grid">
<div class="hero-portrait" aria-hidden="true">
<div class="portrait-orb">
<span class="portrait-initials">MO</span>
</div>
<div class="portrait-ring"></div>
</div>
<div class="hero-copy">
<p class="eyebrow">Product & Brand Designer</p>
<h1 class="hero-name">Maya<span class="name-break"> </span>Okafor</h1>
<p class="hero-tag">
I turn fuzzy ideas into <em>warm, usable</em> products — design systems,
motion, and the occasional rogue side project.
</p>
<ul class="hero-meta">
<li><span class="meta-ico" aria-hidden="true">📍</span> Lisbon · Remote-friendly</li>
<li><span class="meta-ico" aria-hidden="true">✦</span> Available May 2026</li>
<li><a href="mailto:[email protected]"><span class="meta-ico" aria-hidden="true">✉</span> [email protected]</a></li>
</ul>
<div class="hero-actions">
<a class="btn btn-primary" href="#contact">Work with me</a>
<button class="btn btn-ghost" id="downloadBtn" type="button">Download CV</button>
</div>
</div>
</div>
</header>
<!-- ===== STATS BAND ===== -->
<section class="stats" aria-label="Career at a glance">
<div class="stat" data-target="9" data-suffix="">
<span class="stat-num" data-stat>0</span>
<span class="stat-label">Years designing</span>
</div>
<div class="stat" data-target="48" data-suffix="">
<span class="stat-num" data-stat>0</span>
<span class="stat-label">Products shipped</span>
</div>
<div class="stat" data-target="23" data-suffix="">
<span class="stat-num" data-stat>0</span>
<span class="stat-label">Happy clients</span>
</div>
<div class="stat" data-target="6" data-suffix="">
<span class="stat-num" data-stat>0</span>
<span class="stat-label">Design awards</span>
</div>
</section>
<!-- ===== SKILLS ===== -->
<section class="section" aria-labelledby="skills-h">
<div class="section-head">
<h2 id="skills-h" class="section-title"><span class="title-num">01</span> What I'm good at</h2>
<p class="section-sub">Rated honestly — the bars fill on view.</p>
</div>
<div class="skills">
<ul class="bars" aria-label="Skill proficiency">
<li class="bar" data-level="95">
<span class="bar-name">Design systems</span>
<span class="bar-track"><span class="bar-fill"></span></span>
<span class="bar-pct" data-bar-pct>0%</span>
</li>
<li class="bar" data-level="88">
<span class="bar-name">Product strategy</span>
<span class="bar-track"><span class="bar-fill"></span></span>
<span class="bar-pct" data-bar-pct>0%</span>
</li>
<li class="bar" data-level="82">
<span class="bar-name">Motion & prototyping</span>
<span class="bar-track"><span class="bar-fill"></span></span>
<span class="bar-pct" data-bar-pct>0%</span>
</li>
<li class="bar" data-level="74">
<span class="bar-name">Front-end (CSS)</span>
<span class="bar-track"><span class="bar-fill"></span></span>
<span class="bar-pct" data-bar-pct>0%</span>
</li>
</ul>
<div class="constellation" aria-label="Tools and topics">
<h3 class="constellation-h">Orbit of tools</h3>
<ul class="chips">
<li class="chip" style="--d:0">Figma</li>
<li class="chip" style="--d:1">Design tokens</li>
<li class="chip" style="--d:2">Accessibility</li>
<li class="chip" style="--d:3">Framer</li>
<li class="chip" style="--d:4">Research ops</li>
<li class="chip" style="--d:5">After Effects</li>
<li class="chip" style="--d:6">Storybook</li>
<li class="chip" style="--d:7">Workshops</li>
<li class="chip" style="--d:8">Brand systems</li>
</ul>
</div>
</div>
</section>
<!-- ===== EXPERIENCE TIMELINE ===== -->
<section class="section" aria-labelledby="exp-h">
<div class="section-head">
<h2 id="exp-h" class="section-title"><span class="title-num">02</span> Where I've been</h2>
<p class="section-sub">A short, mostly-linear path.</p>
</div>
<ol class="timeline">
<li class="t-item">
<span class="t-ico" aria-hidden="true">🛰</span>
<div class="t-body">
<div class="t-top">
<h3 class="t-role">Principal Designer</h3>
<span class="t-when">2023 — Now</span>
</div>
<p class="t-org">Orbital Labs · Lisbon</p>
<p class="t-desc">Lead the design org for a B2B analytics suite. Built “Nova”, a 240-component design system now used by 40 engineers across 5 squads.</p>
</div>
</li>
<li class="t-item">
<span class="t-ico" aria-hidden="true">🌿</span>
<div class="t-body">
<div class="t-top">
<h3 class="t-role">Senior Product Designer</h3>
<span class="t-when">2020 — 2023</span>
</div>
<p class="t-org">Fernlea Health · Remote</p>
<p class="t-desc">Redesigned the patient onboarding flow, cutting drop-off by 31%. Owned the mobile booking experience end-to-end.</p>
</div>
</li>
<li class="t-item">
<span class="t-ico" aria-hidden="true">✏️</span>
<div class="t-body">
<div class="t-top">
<h3 class="t-role">Product Designer</h3>
<span class="t-when">2017 — 2020</span>
</div>
<p class="t-org">Maker & Co. · Porto</p>
<p class="t-desc">First design hire at an e-commerce studio. Shipped 30+ storefronts and set up the agency's first component library.</p>
</div>
</li>
<li class="t-item">
<span class="t-ico" aria-hidden="true">🎓</span>
<div class="t-body">
<div class="t-top">
<h3 class="t-role">BA, Communication Design</h3>
<span class="t-when">2013 — 2017</span>
</div>
<p class="t-org">University of the Arts · London</p>
<p class="t-desc">Graduated with honours. Thesis on calm interfaces and the ethics of notification design.</p>
</div>
</li>
</ol>
</section>
<!-- ===== SELECTED WORK ===== -->
<section class="section" aria-labelledby="work-h">
<div class="section-head">
<h2 id="work-h" class="section-title"><span class="title-num">03</span> Selected work</h2>
<p class="section-sub">A few projects I'm proud of.</p>
</div>
<div class="work-grid">
<article class="proj">
<div class="proj-art proj-art-1" aria-hidden="true"></div>
<h3 class="proj-name">Nova Design System</h3>
<p class="proj-line">Orbital Labs · 2024</p>
<p class="proj-desc">A token-driven system spanning web, iOS, and a marketing site — themed in one source of truth.</p>
</article>
<article class="proj">
<div class="proj-art proj-art-2" aria-hidden="true"></div>
<h3 class="proj-name">Fernlea Booking</h3>
<p class="proj-line">Fernlea Health · 2022</p>
<p class="proj-desc">Mobile-first appointment booking that turned a 6-step form into a 90-second flow.</p>
</article>
<article class="proj">
<div class="proj-art proj-art-3" aria-hidden="true"></div>
<h3 class="proj-name">Tideline (side project)</h3>
<p class="proj-line">Self · 2023</p>
<p class="proj-desc">A tiny weather app with hand-drawn icons and a focus on calm, glanceable mornings.</p>
</article>
<article class="proj">
<div class="proj-art proj-art-4" aria-hidden="true"></div>
<h3 class="proj-name">Maker Storefront Kit</h3>
<p class="proj-line">Maker & Co. · 2019</p>
<p class="proj-desc">A reusable e-commerce theme that powered 30+ launches with a shared visual language.</p>
</article>
</div>
</section>
<!-- ===== ABOUT ===== -->
<section class="section about" aria-labelledby="about-h">
<div class="section-head">
<h2 id="about-h" class="section-title"><span class="title-num">04</span> A little about me</h2>
</div>
<div class="about-body">
<p>
I'm a product designer who likes the unglamorous middle of the process — the messy
mapping, the third round of feedback, the moment a fuzzy idea finally clicks into a
system. I care about <strong>clarity over cleverness</strong> and interfaces that
feel calm.
</p>
<p>
Outside work you'll find me letterpress printing, over-watering plants, and sketching
tiny weather icons for apps nobody asked for. Always up for a coffee and a good problem.
</p>
<ul class="about-facts">
<li><span class="fact-k">Based in</span><span class="fact-v">Lisbon, PT</span></li>
<li><span class="fact-k">Speaks</span><span class="fact-v">EN · PT · FR</span></li>
<li><span class="fact-k">Currently</span><span class="fact-v">Reading on motion</span></li>
</ul>
</div>
</section>
<!-- ===== CONTACT ===== -->
<section class="section contact" id="contact" aria-labelledby="contact-h">
<div class="contact-card">
<h2 id="contact-h" class="contact-title">Let's make something good.</h2>
<p class="contact-sub">Open to senior & principal product design roles, and the odd freelance brand sprint.</p>
<div class="contact-links">
<a class="btn btn-primary" href="mailto:[email protected]">Say hello</a>
<button class="btn btn-ghost" id="copyEmail" type="button">Copy email</button>
</div>
<ul class="socials">
<li><a href="#" data-toast="Opening Dribbble…">Dribbble</a></li>
<li><a href="#" data-toast="Opening LinkedIn…">LinkedIn</a></li>
<li><a href="#" data-toast="Opening Read.cv…">Read.cv</a></li>
</ul>
</div>
</section>
<footer class="footer">
<p>© 2026 Maya Okafor · Designed & built with care · Fictional portfolio</p>
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Creative / Visual CV
A full one-page creative resume for Maya Okafor, a fictional product and brand designer. The hero pairs a gradient portrait orb (with a slowly rotating dashed ring) against a serif headline, a short positioning line, and quick contact metadata. Below it, a four-cell stats band reports years, products shipped, clients, and awards — each number animating up from zero the first time it scrolls into view.
The body re-skins one set of content into the usual portfolio sections: rated skill bars that fill on view alongside a floating constellation of tool chips, an icon-led experience timeline, a two-up selected-work grid with CSS-gradient cover art, an about block with quick facts, and a dark contact card. A floating dock holds a five-color accent picker (persisted to localStorage) and a print-mode toggle that drops color, animation, and chrome for a clean, printable page. Copy-email and social links fall back gracefully inside sandboxed iframes, and reduced-motion preferences are respected throughout.
Illustrative portfolio — fictional person and projects.