Portfolio — Project / Case-study Card
A portfolio-ready project card with a gradient SVG thumbnail, role, year, tag chips, and a one-line outcome. Ships three layouts in one grid — default, wide featured, and compact — that each lift on hover or keyboard focus and reveal a View case study link. Activating any card opens a lightweight, accessible detail modal with overview, role and impact metrics, plus a subtle pointer-tracked tilt.
MCP
Code
:root {
--ink: #14151a;
--ink-soft: #4a4d57;
--ink-faint: #797d88;
--paper: #f6f6f3;
--surface: #ffffff;
--line: #e7e7e1;
--line-strong: #d8d8d0;
--accent: #5b3df5;
--accent-soft: #ece8ff;
--accent-ink: #3a22c4;
--radius: 18px;
--radius-sm: 10px;
--shadow-1: 0 1px 2px rgba(20, 21, 26, 0.05), 0 1px 1px rgba(20, 21, 26, 0.04);
--shadow-2: 0 18px 40px -16px rgba(20, 21, 26, 0.28), 0 4px 12px -4px rgba(20, 21, 26, 0.12);
--ring: 0 0 0 3px var(--accent-soft), 0 0 0 5px var(--accent);
--ease: cubic-bezier(0.22, 1, 0.36, 1);
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background:
radial-gradient(900px 600px at 12% -10%, #efeaff 0%, transparent 55%),
radial-gradient(800px 500px at 110% 0%, #fdf3e3 0%, transparent 50%),
var(--paper);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: clamp(2rem, 6vw, 5rem) clamp(1.1rem, 4vw, 2.5rem) 5rem;
}
/* ── Page head ───────────────────────────── */
.page-head {
max-width: 40rem;
margin-bottom: clamp(1.75rem, 4vw, 3rem);
}
.page-head__eyebrow {
margin: 0 0 0.75rem;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent-ink);
}
.page-head__title {
margin: 0 0 0.85rem;
font-size: clamp(1.9rem, 5vw, 3rem);
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.05;
}
.page-head__lede {
margin: 0;
font-size: clamp(1rem, 2.4vw, 1.1rem);
color: var(--ink-soft);
}
/* ── Grid ────────────────────────────────── */
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(1rem, 2.5vw, 1.5rem);
}
/* ── Card base ───────────────────────────── */
.card {
position: relative;
display: flex;
flex-direction: column;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-1);
cursor: pointer;
overflow: hidden;
text-align: left;
transition:
transform 0.4s var(--ease),
box-shadow 0.4s var(--ease),
border-color 0.4s var(--ease);
will-change: transform;
}
.card:hover,
.card:focus-visible {
transform: translateY(-6px);
box-shadow: var(--shadow-2);
border-color: var(--line-strong);
}
.card:focus-visible {
outline: none;
box-shadow: var(--ring), var(--shadow-2);
}
.card:active {
transform: translateY(-2px) scale(0.995);
}
/* Thumb */
.card__thumb {
position: relative;
aspect-ratio: 16 / 9;
overflow: hidden;
background: #e9e9e4;
}
.thumb__art {
display: block;
width: 100%;
height: 100%;
transform: scale(1.02);
transition: transform 0.6s var(--ease);
}
.card:hover .thumb__art,
.card:focus-visible .thumb__art {
transform: scale(1.08);
}
.card__badge {
position: absolute;
top: 0.7rem;
left: 0.7rem;
padding: 0.28rem 0.6rem;
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--ink);
background: rgba(255, 255, 255, 0.92);
border-radius: 999px;
box-shadow: var(--shadow-1);
}
/* Body */
.card__body {
display: flex;
flex-direction: column;
flex: 1;
padding: clamp(1rem, 2.5vw, 1.35rem);
gap: 0.5rem;
}
.card__meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
font-size: 0.78rem;
}
.card__role {
font-weight: 600;
color: var(--accent-ink);
}
.card__year {
color: var(--ink-faint);
font-variant-numeric: tabular-nums;
font-weight: 500;
}
.card__title {
margin: 0.1rem 0 0;
font-size: clamp(1.05rem, 2.6vw, 1.25rem);
font-weight: 700;
letter-spacing: -0.015em;
line-height: 1.25;
}
.card__outcome {
margin: 0;
font-size: 0.92rem;
color: var(--ink-soft);
}
/* Chips */
.chips {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin: 0.15rem 0 0;
padding: 0;
list-style: none;
}
.chip {
padding: 0.22rem 0.6rem;
font-size: 0.74rem;
font-weight: 500;
color: var(--ink-soft);
background: var(--paper);
border: 1px solid var(--line);
border-radius: 999px;
}
/* CTA reveal */
.card__cta {
display: inline-flex;
align-items: center;
gap: 0.35rem;
margin-top: auto;
padding-top: 0.55rem;
font-size: 0.85rem;
font-weight: 600;
color: var(--accent);
opacity: 0;
transform: translateY(6px);
transition:
opacity 0.35s var(--ease),
transform 0.35s var(--ease);
}
.card:hover .card__cta,
.card:focus-visible .card__cta {
opacity: 1;
transform: translateY(0);
}
.card__arrow {
transition: transform 0.35s var(--ease);
}
.card:hover .card__arrow,
.card:focus-visible .card__arrow {
transform: translateX(4px);
}
/* ── Variant: featured / wide ────────────── */
.card--featured {
grid-column: 1 / -1;
flex-direction: row;
}
.card--featured .card__thumb {
flex: 0 0 46%;
aspect-ratio: auto;
}
.card--featured .card__body {
flex: 1;
justify-content: center;
padding: clamp(1.3rem, 3vw, 2.2rem);
gap: 0.65rem;
}
.card--featured .card__title {
font-size: clamp(1.3rem, 3vw, 1.7rem);
}
.card--featured .card__outcome {
font-size: 1rem;
}
/* ── Variant: compact ────────────────────── */
.card--compact {
flex-direction: row;
align-items: stretch;
}
.card--compact .card__thumb {
flex: 0 0 38%;
aspect-ratio: 1 / 1;
}
.card--compact .card__body {
padding: 0.95rem 1.1rem;
gap: 0.35rem;
}
.card--compact .card__title {
font-size: 1rem;
}
.card--compact .card__outcome {
font-size: 0.82rem;
}
/* ── Modal ───────────────────────────────── */
.modal[hidden] {
display: none;
}
.modal {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: 1rem;
}
.modal__backdrop {
position: absolute;
inset: 0;
background: rgba(20, 21, 26, 0.5);
backdrop-filter: blur(3px);
animation: fade 0.25s var(--ease);
}
.modal__panel {
position: relative;
width: min(620px, 100%);
max-height: min(86vh, 760px);
overflow-y: auto;
background: var(--surface);
border-radius: 22px;
box-shadow: 0 30px 80px -20px rgba(20, 21, 26, 0.55);
animation: pop 0.35s var(--ease);
}
.modal__panel:focus {
outline: none;
}
.modal__thumb {
height: 150px;
background: linear-gradient(135deg, #6d5efc, #22d3ee);
}
.modal__close {
position: absolute;
top: 0.75rem;
right: 0.75rem;
display: grid;
place-items: center;
width: 38px;
height: 38px;
font-size: 1.4rem;
line-height: 1;
color: var(--ink);
background: rgba(255, 255, 255, 0.92);
border: 1px solid var(--line);
border-radius: 50%;
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.modal__close:hover {
background: #fff;
transform: rotate(90deg);
}
.modal__close:focus-visible {
outline: none;
box-shadow: var(--ring);
}
.modal__content {
padding: clamp(1.25rem, 3vw, 2rem);
}
.modal__meta {
display: flex;
gap: 0.75rem;
align-items: center;
font-size: 0.82rem;
}
.modal__role {
font-weight: 600;
color: var(--accent-ink);
}
.modal__year {
color: var(--ink-faint);
font-variant-numeric: tabular-nums;
}
.modal__title {
margin: 0.45rem 0 0.75rem;
font-size: clamp(1.4rem, 4vw, 1.85rem);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.1;
}
.chips--modal {
margin-bottom: 1rem;
}
.modal__h {
margin: 1.25rem 0 0.35rem;
font-size: 0.74rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-faint);
}
.modal__p {
margin: 0;
color: var(--ink-soft);
}
.modal__impact {
margin: 0.35rem 0 0;
padding-left: 1.1rem;
color: var(--ink-soft);
}
.modal__impact li {
margin-bottom: 0.3rem;
}
.modal__impact strong {
color: var(--ink);
}
/* ── Toast ───────────────────────────────── */
.toast {
position: fixed;
left: 50%;
bottom: 1.5rem;
transform: translate(-50%, 1.5rem);
padding: 0.7rem 1.1rem;
font-size: 0.88rem;
font-weight: 500;
color: #fff;
background: var(--ink);
border-radius: 12px;
box-shadow: var(--shadow-2);
opacity: 0;
pointer-events: none;
z-index: 60;
transition: opacity 0.3s var(--ease), transform 0.3s var(--ease);
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ── Animations ──────────────────────────── */
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes pop {
from { opacity: 0; transform: translateY(14px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
/* ── Responsive ──────────────────────────── */
@media (max-width: 720px) {
.grid {
grid-template-columns: 1fr;
}
.card--featured {
flex-direction: column;
}
.card--featured .card__thumb {
flex: none;
aspect-ratio: 16 / 9;
}
}
@media (max-width: 420px) {
.card--compact {
flex-direction: column;
}
.card--compact .card__thumb {
flex: none;
aspect-ratio: 16 / 9;
}
.card__cta {
opacity: 1;
transform: none;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}(function () {
"use strict";
/* ── Fictional case-study data ───────────── */
var PROJECTS = {
lumen: {
role: "Lead Product Designer",
year: "2024",
title: "Lumen — banking that explains itself",
chips: ["Product UX", "Design System", "Research"],
thumb: "linear-gradient(135deg, #fbe6c9, #f3b14e)",
overview:
"Lumen is a challenger bank where every screen answers \"why am I seeing this?\". I led a 4-month redesign of the money-movement flow, replacing modal-heavy transfers with an inline, reversible timeline.",
rolep:
"Owned end-to-end UX, ran 14 moderated sessions, and built the token layer the team still ships on today.",
impact: [
["38%", "fewer support tickets about failed transfers"],
["+12", "System Usability Scale points after launch"],
["3 wks", "saved per quarter on design-to-build handoff"]
]
},
orbit: {
role: "Product Designer",
year: "2023",
title: "Orbit — onboarding for a dev platform",
chips: ["UX", "Prototyping"],
thumb: "linear-gradient(135deg, #6d5efc, #22d3ee)",
overview:
"Orbit helps engineers ship their first deploy in under ten minutes. I reworked the first-run experience into a guided, skippable checklist with contextual code samples.",
rolep:
"Designed the flow, prototyped the interactive terminal, and paired daily with two front-end engineers.",
impact: [
["41% → 67%", "activation rate within 7 days"],
["−4 min", "median time to first successful deploy"],
["x2", "week-2 retention for new accounts"]
]
},
fern: {
role: "Brand & Product",
year: "2023",
title: "Fern — a grocery app for refill stores",
chips: ["Branding", "Mobile"],
thumb: "linear-gradient(135deg, #10b981, #064e3b)",
overview:
"Fern lets zero-waste shoppers track refillable containers across local stores. I built the brand from scratch and a refill-tracking flow that maps containers to deposits.",
rolep:
"Solo designer: identity, illustration system, and the full mobile UX through a 4-store pilot.",
impact: [
["4", "pilot stores adopted the flow in month one"],
["92%", "of testers found a container in under 5 seconds"],
["0", "paper receipts — fully digital deposit ledger"]
]
},
atlas: {
role: "Design Lead",
year: "2022",
title: "Atlas — trail-planning dashboard",
chips: ["UX", "Data viz"],
thumb: "linear-gradient(135deg, #ef4444, #f97316)",
overview:
"Atlas started as a weekend project to plan multi-day hikes and grew into a client pitch. It layers elevation, weather and water sources onto a single planning canvas.",
rolep:
"Concept, interaction design, and the data-viz language for elevation and risk overlays.",
impact: [
["1", "weekend prototype that became a paid engagement"],
["6", "overlay types unified into one legend system"],
["AA", "contrast met across every map layer"]
]
},
quill: {
role: "Content Design",
year: "2021",
title: "Quill — docs that read like a friend",
chips: ["UX Writing", "System"],
thumb: "linear-gradient(135deg, #1e293b, #475569)",
overview:
"Quill is a voice-and-tone system that rewrote 200+ help pages to sound human without losing precision. I paired editorial rules with reusable component patterns.",
rolep:
"Defined the voice principles, wrote the pattern library, and trained 9 writers on it.",
impact: [
["200+", "pages migrated to the new voice"],
["−27%", "\"was this helpful? — no\" votes"],
["9", "writers onboarded onto one shared system"]
]
}
};
/* ── Toast helper ─────────────────────────── */
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
/* ── Modal wiring ─────────────────────────── */
var modal = document.getElementById("modal");
var panel = modal.querySelector(".modal__panel");
var thumbEl = document.getElementById("modal-thumb");
var roleEl = document.getElementById("modal-role");
var yearEl = document.getElementById("modal-year");
var titleEl = document.getElementById("modal-title");
var chipsEl = document.getElementById("modal-chips");
var overviewEl = document.getElementById("modal-overview");
var rolepEl = document.getElementById("modal-rolep");
var impactEl = document.getElementById("modal-impact");
var lastFocused = null;
function openModal(key) {
var p = PROJECTS[key];
if (!p) return;
thumbEl.style.background = p.thumb;
roleEl.textContent = p.role;
yearEl.textContent = p.year;
titleEl.textContent = p.title;
chipsEl.innerHTML = "";
p.chips.forEach(function (c) {
var li = document.createElement("li");
li.className = "chip";
li.textContent = c;
chipsEl.appendChild(li);
});
overviewEl.textContent = p.overview;
rolepEl.textContent = p.rolep;
impactEl.innerHTML = "";
p.impact.forEach(function (row) {
var li = document.createElement("li");
var strong = document.createElement("strong");
strong.textContent = row[0] + " ";
li.appendChild(strong);
li.appendChild(document.createTextNode(row[1]));
impactEl.appendChild(li);
});
lastFocused = document.activeElement;
modal.hidden = false;
document.body.style.overflow = "hidden";
panel.scrollTop = 0;
panel.focus();
document.addEventListener("keydown", onKeydown);
}
function closeModal() {
if (modal.hidden) return;
modal.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", onKeydown);
if (lastFocused && typeof lastFocused.focus === "function") {
lastFocused.focus();
}
}
function onKeydown(e) {
if (e.key === "Escape") {
closeModal();
return;
}
if (e.key === "Tab") {
// simple focus trap
var focusables = modal.querySelectorAll(
'button, [href], [tabindex]:not([tabindex="-1"])'
);
if (!focusables.length) return;
var first = focusables[0];
var last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
}
/* close buttons + backdrop */
modal.querySelectorAll("[data-close]").forEach(function (el) {
el.addEventListener("click", closeModal);
});
/* ── Card interactions ────────────────────── */
var cards = document.querySelectorAll(".card");
cards.forEach(function (card) {
var key = card.getAttribute("data-project");
function activate() {
openModal(key);
}
card.addEventListener("click", activate);
card.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
e.preventDefault();
activate();
}
});
});
/* ── Pointer-tracked tilt (subtle) ────────── */
var reduceMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (!reduceMotion && window.matchMedia("(hover: hover)").matches) {
cards.forEach(function (card) {
card.addEventListener("pointermove", function (e) {
var r = card.getBoundingClientRect();
var dx = (e.clientX - r.left) / r.width - 0.5;
var dy = (e.clientY - r.top) / r.height - 0.5;
card.style.transform =
"translateY(-6px) rotateX(" +
(-dy * 3).toFixed(2) +
"deg) rotateY(" +
(dx * 3).toFixed(2) +
"deg)";
});
card.addEventListener("pointerleave", function () {
card.style.transform = "";
});
});
}
toast("Hover or focus a card · activate to open its case study");
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Project / Case-study Cards — Maya Okafor</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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap" id="main">
<header class="page-head">
<p class="page-head__eyebrow">Selected work · 2021–2024</p>
<h1 class="page-head__title">Project & Case-study Cards</h1>
<p class="page-head__lede">
A portfolio-ready card with three layouts — default, featured and compact.
Hover or focus to lift the card and reveal the case-study link; activate any
card to open its detail.
</p>
</header>
<!-- FEATURED (wide) -->
<section class="grid" aria-label="Project cards">
<article
class="card card--featured"
tabindex="0"
role="button"
aria-haspopup="dialog"
data-project="lumen"
aria-labelledby="t-lumen"
>
<div class="card__thumb thumb--lumen" aria-hidden="true">
<svg class="thumb__art" viewBox="0 0 120 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="">
<defs>
<linearGradient id="g-lumen" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fbe6c9" />
<stop offset="1" stop-color="#f3b14e" />
</linearGradient>
</defs>
<rect width="120" height="80" fill="url(#g-lumen)" />
<circle cx="86" cy="22" r="26" fill="#fff" opacity="0.45" />
<rect x="14" y="50" width="44" height="6" rx="3" fill="#7a4a10" opacity="0.55" />
<rect x="14" y="62" width="30" height="5" rx="2.5" fill="#7a4a10" opacity="0.35" />
<rect x="14" y="18" width="20" height="20" rx="5" fill="#fff" opacity="0.85" />
</svg>
<span class="card__badge">Featured</span>
</div>
<div class="card__body">
<div class="card__meta">
<span class="card__role">Lead Product Designer</span>
<span class="card__year">2024</span>
</div>
<h2 class="card__title" id="t-lumen">Lumen — banking that explains itself</h2>
<p class="card__outcome">Cut support tickets 38% by redesigning the money-movement flow.</p>
<ul class="chips" aria-label="Tags">
<li class="chip">Product UX</li>
<li class="chip">Design System</li>
<li class="chip">Research</li>
</ul>
<span class="card__cta" aria-hidden="true">View case study <span class="card__arrow">→</span></span>
</div>
</article>
<!-- DEFAULT -->
<article
class="card card--default"
tabindex="0"
role="button"
aria-haspopup="dialog"
data-project="orbit"
aria-labelledby="t-orbit"
>
<div class="card__thumb thumb--orbit" aria-hidden="true">
<svg class="thumb__art" viewBox="0 0 120 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="">
<defs>
<linearGradient id="g-orbit" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#6d5efc" />
<stop offset="1" stop-color="#22d3ee" />
</linearGradient>
</defs>
<rect width="120" height="80" fill="url(#g-orbit)" />
<circle cx="60" cy="40" r="10" fill="#fff" opacity="0.9" />
<circle cx="60" cy="40" r="24" fill="none" stroke="#fff" stroke-width="1.5" opacity="0.55" />
<circle cx="60" cy="40" r="34" fill="none" stroke="#fff" stroke-width="1" opacity="0.3" />
<circle cx="84" cy="40" r="3.5" fill="#fff" />
<circle cx="36" cy="20" r="2.5" fill="#fff" opacity="0.8" />
</svg>
</div>
<div class="card__body">
<div class="card__meta">
<span class="card__role">Product Designer</span>
<span class="card__year">2023</span>
</div>
<h2 class="card__title" id="t-orbit">Orbit — onboarding for a dev platform</h2>
<p class="card__outcome">Lifted activation from 41% to 67% with a guided first-run.</p>
<ul class="chips" aria-label="Tags">
<li class="chip">UX</li>
<li class="chip">Prototyping</li>
</ul>
<span class="card__cta" aria-hidden="true">View case study <span class="card__arrow">→</span></span>
</div>
</article>
<!-- DEFAULT -->
<article
class="card card--default"
tabindex="0"
role="button"
aria-haspopup="dialog"
data-project="fern"
aria-labelledby="t-fern"
>
<div class="card__thumb thumb--fern" aria-hidden="true">
<svg class="thumb__art" viewBox="0 0 120 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="">
<defs>
<linearGradient id="g-fern" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#10b981" />
<stop offset="1" stop-color="#064e3b" />
</linearGradient>
</defs>
<rect width="120" height="80" fill="url(#g-fern)" />
<path d="M60 70 C60 45 44 36 44 18 C58 24 64 38 60 70" fill="#d1fae5" opacity="0.85" />
<path d="M60 70 C60 48 76 38 78 22 C64 28 58 42 60 70" fill="#a7f3d0" opacity="0.7" />
<rect x="56" y="66" width="8" height="10" rx="2" fill="#fffbe8" opacity="0.8" />
</svg>
</div>
<div class="card__body">
<div class="card__meta">
<span class="card__role">Brand & Product</span>
<span class="card__year">2023</span>
</div>
<h2 class="card__title" id="t-fern">Fern — a grocery app for refill stores</h2>
<p class="card__outcome">Shipped a refill-tracking flow that 4 pilot stores adopted.</p>
<ul class="chips" aria-label="Tags">
<li class="chip">Branding</li>
<li class="chip">Mobile</li>
</ul>
<span class="card__cta" aria-hidden="true">View case study <span class="card__arrow">→</span></span>
</div>
</article>
<!-- COMPACT -->
<article
class="card card--compact"
tabindex="0"
role="button"
aria-haspopup="dialog"
data-project="atlas"
aria-labelledby="t-atlas"
>
<div class="card__thumb thumb--atlas" aria-hidden="true">
<svg class="thumb__art" viewBox="0 0 80 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="">
<defs>
<linearGradient id="g-atlas" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#ef4444" />
<stop offset="1" stop-color="#f97316" />
</linearGradient>
</defs>
<rect width="80" height="80" fill="url(#g-atlas)" />
<path d="M0 56 L20 44 L40 52 L60 38 L80 50 L80 80 L0 80 Z" fill="#7c2d12" opacity="0.45" />
<path d="M0 64 L24 56 L46 62 L80 52 L80 80 L0 80 Z" fill="#fff7ed" opacity="0.4" />
<circle cx="58" cy="24" r="9" fill="#fff" opacity="0.85" />
</svg>
</div>
<div class="card__body">
<div class="card__meta">
<span class="card__role">Design Lead</span>
<span class="card__year">2022</span>
</div>
<h2 class="card__title" id="t-atlas">Atlas — trail-planning dashboard</h2>
<p class="card__outcome">Compact card · weekend project turned client pitch.</p>
<ul class="chips" aria-label="Tags">
<li class="chip">UX</li>
<li class="chip">Data viz</li>
</ul>
<span class="card__cta" aria-hidden="true">View case study <span class="card__arrow">→</span></span>
</div>
</article>
<!-- COMPACT -->
<article
class="card card--compact"
tabindex="0"
role="button"
aria-haspopup="dialog"
data-project="quill"
aria-labelledby="t-quill"
>
<div class="card__thumb thumb--quill" aria-hidden="true">
<svg class="thumb__art" viewBox="0 0 80 80" preserveAspectRatio="xMidYMid slice" role="img" aria-label="">
<defs>
<linearGradient id="g-quill" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#1e293b" />
<stop offset="1" stop-color="#475569" />
</linearGradient>
</defs>
<rect width="80" height="80" fill="url(#g-quill)" />
<rect x="18" y="20" width="44" height="6" rx="3" fill="#e2e8f0" opacity="0.9" />
<rect x="18" y="34" width="36" height="5" rx="2.5" fill="#94a3b8" opacity="0.8" />
<rect x="18" y="46" width="40" height="5" rx="2.5" fill="#94a3b8" opacity="0.6" />
<rect x="18" y="58" width="24" height="5" rx="2.5" fill="#94a3b8" opacity="0.4" />
</svg>
</div>
<div class="card__body">
<div class="card__meta">
<span class="card__role">Content Design</span>
<span class="card__year">2021</span>
</div>
<h2 class="card__title" id="t-quill">Quill — docs that read like a friend</h2>
<p class="card__outcome">Compact card · voice & tone system for 200+ pages.</p>
<ul class="chips" aria-label="Tags">
<li class="chip">UX Writing</li>
<li class="chip">System</li>
</ul>
<span class="card__cta" aria-hidden="true">View case study <span class="card__arrow">→</span></span>
</div>
</article>
</section>
</main>
<!-- DETAIL MODAL -->
<div class="modal" id="modal" hidden>
<div class="modal__backdrop" data-close></div>
<div
class="modal__panel"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
tabindex="-1"
>
<div class="modal__thumb" id="modal-thumb" aria-hidden="true"></div>
<button class="modal__close" type="button" data-close aria-label="Close case study">×</button>
<div class="modal__content">
<div class="modal__meta">
<span id="modal-role" class="modal__role"></span>
<span id="modal-year" class="modal__year"></span>
</div>
<h2 class="modal__title" id="modal-title"></h2>
<ul class="chips chips--modal" id="modal-chips" aria-label="Tags"></ul>
<h3 class="modal__h">Overview</h3>
<p id="modal-overview" class="modal__p"></p>
<h3 class="modal__h">My role</h3>
<p id="modal-rolep" class="modal__p"></p>
<h3 class="modal__h">Impact</h3>
<ul class="modal__impact" id="modal-impact"></ul>
</div>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Project / Case-study Card
A reusable project card for a single-person portfolio. Each card pairs a gradient SVG thumbnail with a tidy hierarchy: role and year up top, a bold project title, a one-line outcome, and a row of tag chips such as Product UX or Branding. The same content renders in three layouts — a wide featured card, the standard default card, and a tight compact card — so a grid stays visually varied without new markup.
Hover or keyboard-focus lifts the card, zooms the thumbnail slightly, and reveals a “View case study →” link with a nudging arrow. On devices that support hover, a subtle pointer-tracked tilt follows the cursor. Activating a card with a click, Enter, or Space opens a lightweight detail modal containing the project overview, your role, and impact metrics.
The modal is fully keyboard-accessible: it traps focus, closes on Escape or
backdrop click, restores focus to the originating card, and uses
role="dialog" with aria-modal. Everything is vanilla JS with no dependencies,
respects prefers-reduced-motion, and collapses gracefully down to ~360px.
Illustrative portfolio — fictional person and projects.