Science — Researchers / Team Page
A polished, self-contained research-group people page for the fictional Computational Climate Dynamics Lab. Members are grouped by career stage — Principal Investigator, Postdocs, PhD Students, and Alumni — as CSS monogram avatar cards with research-interest chips and links to email, ORCID, Scholar, and a personal site. A live search and group filters narrow the roster, and each card flips to reveal a short bio with tenure and ORCID metadata. Built with vanilla JS, fully responsive and keyboard accessible.
MCP
Code
:root {
--bg: #ffffff;
--bg-alt: #f6f8fb;
--ink: #0f1b2d;
--ink-2: #33445c;
--muted: #697892;
--accent: #1a4f8a;
--accent-d: #123a66;
--accent-50: #e9f0f9;
--teal: #0f7d78;
--teal-50: #e4f3f1;
--line: rgba(15, 27, 45, 0.12);
--line-2: rgba(15, 27, 45, 0.2);
--ok: #2f9e6f;
--warn: #c9821f;
--danger: #cf4538;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 16px;
--shadow-sm: 0 1px 2px rgba(15, 27, 45, 0.06), 0 1px 3px rgba(15, 27, 45, 0.05);
--shadow-md: 0 4px 14px rgba(15, 27, 45, 0.08), 0 2px 6px rgba(15, 27, 45, 0.05);
--font-prose: "Source Serif 4", Georgia, serif;
--font-ui: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: var(--font-prose);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: var(--accent);
color: #fff;
padding: 10px 16px;
border-radius: 0 0 var(--r-sm) 0;
font-family: var(--font-ui);
z-index: 50;
}
.skip-link:focus { left: 0; }
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
/* ---------- Masthead ---------- */
.masthead {
border-bottom: 1px solid var(--line);
background: var(--bg);
position: sticky;
top: 0;
z-index: 20;
backdrop-filter: saturate(1.2) blur(6px);
}
.masthead-inner {
max-width: 1100px;
margin: 0 auto;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
flex-wrap: wrap;
}
.brand { display: flex; align-items: center; gap: 16px; }
.brand-mark {
font-family: var(--font-mono);
font-weight: 500;
font-size: 18px;
letter-spacing: 1px;
color: #fff;
background: linear-gradient(135deg, var(--accent), var(--accent-d));
width: 52px;
height: 52px;
display: grid;
place-items: center;
border-radius: var(--r-md);
box-shadow: var(--shadow-sm);
flex: none;
}
.brand-eyebrow {
font-family: var(--font-ui);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--teal);
margin: 0 0 2px;
}
.brand-title {
font-size: 19px;
font-weight: 700;
margin: 0;
line-height: 1.2;
color: var(--ink);
}
.brand-sub {
font-family: var(--font-ui);
font-size: 12.5px;
color: var(--muted);
margin: 2px 0 0;
}
.masthead-nav {
display: flex;
gap: 4px;
font-family: var(--font-ui);
}
.masthead-nav a {
color: var(--ink-2);
text-decoration: none;
font-size: 14px;
font-weight: 500;
padding: 8px 12px;
border-radius: var(--r-sm);
}
.masthead-nav a:hover { background: var(--accent-50); color: var(--accent-d); }
/* ---------- Layout ---------- */
.wrap {
max-width: 1100px;
margin: 0 auto;
padding: 40px 24px 64px;
}
.section-title {
font-size: 30px;
font-weight: 700;
margin: 0 0 12px;
letter-spacing: -0.01em;
}
.lede {
font-size: 17.5px;
color: var(--ink-2);
max-width: 64ch;
margin: 0 0 24px;
}
/* ---------- Stat strip ---------- */
.stat-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1px;
background: var(--line);
border: 1px solid var(--line);
border-radius: var(--r-md);
overflow: hidden;
margin: 0 0 8px;
}
.stat {
background: var(--bg-alt);
padding: 16px 18px;
}
.stat dt {
font-family: var(--font-ui);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
font-weight: 600;
margin: 0 0 4px;
}
.stat dd { margin: 0; }
.stat .num {
font-family: var(--font-mono);
font-size: 18px;
font-weight: 500;
color: var(--accent-d);
}
/* ---------- Toolbar ---------- */
.toolbar {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin: 36px 0 24px;
padding-top: 20px;
border-top: 1px solid var(--line);
}
.search-field {
position: relative;
display: flex;
align-items: center;
flex: 1 1 280px;
min-width: 220px;
}
.search-ico {
position: absolute;
left: 12px;
width: 18px;
height: 18px;
color: var(--muted);
pointer-events: none;
}
#search {
width: 100%;
font-family: var(--font-ui);
font-size: 14px;
color: var(--ink);
padding: 11px 38px 11px 38px;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background: var(--bg);
}
#search:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-50); }
.clear-btn {
position: absolute;
right: 8px;
border: none;
background: var(--line);
color: var(--ink-2);
width: 22px;
height: 22px;
border-radius: 50%;
font-size: 16px;
line-height: 1;
cursor: pointer;
}
.clear-btn:hover { background: var(--line-2); }
.filters { display: flex; gap: 8px; flex-wrap: wrap; }
.chip-filter {
font-family: var(--font-ui);
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line-2);
padding: 8px 14px;
border-radius: 999px;
cursor: pointer;
transition: background 0.14s, color 0.14s, border-color 0.14s;
}
.chip-filter:hover { border-color: var(--accent); color: var(--accent-d); }
.chip-filter.is-active {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.result-count {
font-family: var(--font-mono);
font-size: 12.5px;
color: var(--muted);
margin: 0;
flex: 1 1 100%;
}
.empty-state {
font-family: var(--font-ui);
font-size: 15px;
color: var(--ink-2);
text-align: center;
padding: 48px 16px;
background: var(--bg-alt);
border: 1px dashed var(--line-2);
border-radius: var(--r-md);
}
.link-btn {
border: none;
background: none;
color: var(--accent);
font: inherit;
font-weight: 600;
cursor: pointer;
padding: 0;
text-decoration: underline;
}
/* ---------- Groups & grid ---------- */
.group { margin: 36px 0; scroll-margin-top: 90px; }
.group.is-hidden { display: none; }
.group-title {
font-family: var(--font-ui);
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--teal);
margin: 0 0 16px;
padding-bottom: 8px;
border-bottom: 2px solid var(--teal-50);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(258px, 1fr));
gap: 18px;
}
/* ---------- Person card (flip) ---------- */
.person {
perspective: 1200px;
height: 248px;
}
.person.is-hidden { display: none; }
.person-inner {
position: relative;
width: 100%;
height: 100%;
transition: transform 0.5s cubic-bezier(0.4, 0.1, 0.2, 1);
transform-style: preserve-3d;
}
.person.flipped .person-inner { transform: rotateY(180deg); }
.face {
position: absolute;
inset: 0;
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: var(--bg);
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
overflow: hidden;
}
.person:hover .face-front { box-shadow: var(--shadow-md); border-color: var(--line-2); }
.face-front { padding: 20px; }
.card-head { display: flex; align-items: flex-start; gap: 14px; }
.avatar {
width: 56px;
height: 56px;
border-radius: var(--r-md);
display: grid;
place-items: center;
font-family: var(--font-ui);
font-weight: 700;
font-size: 20px;
color: #fff;
flex: none;
letter-spacing: 0.5px;
}
.card-id { min-width: 0; }
.person-name {
font-family: var(--font-ui);
font-size: 16px;
font-weight: 700;
margin: 0;
color: var(--ink);
line-height: 1.25;
}
.person-role {
font-family: var(--font-ui);
font-size: 12.5px;
color: var(--muted);
margin: 3px 0 0;
}
.person-pron {
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--muted);
}
.interests {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 14px 0 0;
flex: 1;
align-content: flex-start;
}
.chip {
font-family: var(--font-ui);
font-size: 11px;
font-weight: 500;
color: var(--accent-d);
background: var(--accent-50);
padding: 4px 9px;
border-radius: 999px;
line-height: 1.2;
}
mark.hit { background: #fde9a8; color: inherit; border-radius: 2px; padding: 0 1px; }
.card-foot {
display: flex;
align-items: center;
gap: 6px;
margin-top: 14px;
padding-top: 12px;
border-top: 1px solid var(--line);
}
.icon-link {
width: 30px;
height: 30px;
display: grid;
place-items: center;
border-radius: var(--r-sm);
color: var(--ink-2);
text-decoration: none;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 500;
border: 1px solid var(--line);
background: var(--bg-alt);
}
.icon-link:hover { color: var(--accent-d); border-color: var(--accent); background: var(--accent-50); }
.icon-link svg { width: 15px; height: 15px; }
.flip-btn {
margin-left: auto;
font-family: var(--font-ui);
font-size: 11.5px;
font-weight: 600;
color: var(--accent);
background: none;
border: none;
cursor: pointer;
padding: 6px 4px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.flip-btn:hover { text-decoration: underline; }
/* back face */
.face-back {
transform: rotateY(180deg);
padding: 18px 20px;
background: linear-gradient(160deg, var(--accent-d), var(--accent));
color: rgba(255, 255, 255, 0.94);
}
.face-back .person-name { color: #fff; }
.bio {
font-size: 13.5px;
line-height: 1.55;
margin: 10px 0 0;
color: rgba(255, 255, 255, 0.9);
overflow: auto;
flex: 1;
}
.bio b { color: #fff; }
.meta-row {
font-family: var(--font-mono);
font-size: 11px;
color: rgba(255, 255, 255, 0.75);
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid rgba(255, 255, 255, 0.22);
display: flex;
justify-content: space-between;
gap: 8px;
}
.back-close {
align-self: flex-start;
margin-top: 10px;
font-family: var(--font-ui);
font-size: 11.5px;
font-weight: 600;
color: #fff;
background: rgba(255, 255, 255, 0.16);
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 6px 12px;
border-radius: var(--r-sm);
cursor: pointer;
}
.back-close:hover { background: rgba(255, 255, 255, 0.26); }
/* ---------- Figure ---------- */
.join-fig {
margin: 48px 0 0;
padding: 18px 22px;
background: var(--bg-alt);
border-left: 3px solid var(--teal);
border-radius: 0 var(--r-md) var(--r-md) 0;
}
.join-fig figcaption {
font-size: 14px;
color: var(--ink-2);
}
.join-fig strong { color: var(--ink); }
/* ---------- Footer ---------- */
.site-footer {
border-top: 1px solid var(--line);
padding: 28px 24px;
text-align: center;
font-family: var(--font-ui);
color: var(--muted);
font-size: 13px;
}
.site-footer p { margin: 0 0 4px; }
.footer-fine { font-size: 12px; color: var(--muted); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 24px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
font-family: var(--font-ui);
font-size: 13.5px;
padding: 11px 18px;
border-radius: var(--r-md);
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 60;
max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 640px) {
.masthead-inner { padding: 14px 16px; }
.masthead-nav { width: 100%; overflow-x: auto; }
.wrap { padding: 28px 16px 48px; }
.section-title { font-size: 24px; }
.lede { font-size: 16px; }
.stat-strip { grid-template-columns: repeat(2, 1fr); }
.toolbar { gap: 12px; }
.grid { grid-template-columns: 1fr; }
.person { height: 250px; }
}
@media (prefers-reduced-motion: reduce) {
.person-inner, .toast { transition: none; }
}(function () {
"use strict";
/* ---------- Data (fictional) ---------- */
var PEOPLE = [
{
group: "pi", name: "Dr. Imara N. Velasco", role: "Professor & Director",
pron: "she/her", color: "#1a4f8a",
interests: ["Ocean–atmosphere coupling", "Extreme-event attribution", "Ensemble dynamics"],
bio: "Leads the lab's work on <b>regional downscaling</b> of CMIP-class projections. PI on three federal grants; chairs the Pacific Decadal Variability working group.",
since: "Joined 2014", orcid: "0000-0002-7741-3120",
links: { email: "[email protected]", orcid: "0000-0002-7741-3120", scholar: "#", site: "#" }
},
{
group: "postdoc", name: "Dr. Theo R. Aaltonen", role: "Postdoctoral Fellow",
pron: "he/him", color: "#0f7d78",
interests: ["ML emulators", "Bayesian calibration", "Cloud microphysics"],
bio: "Builds neural emulators that reproduce a 50-member ensemble at <b>1/400th the cost</b>. NOAA Climate & Global Change fellow.",
since: "Joined 2023", orcid: "0000-0001-5582-9043",
links: { email: "[email protected]", orcid: "0000-0001-5582-9043", scholar: "#", site: "#" }
},
{
group: "postdoc", name: "Dr. Priya S. Ravichandran", role: "Postdoctoral Researcher",
pron: "she/her", color: "#123a66",
interests: ["Monsoon dynamics", "Teleconnections", "Reanalysis"],
bio: "Quantifies how <b>ENSO–monsoon teleconnections</b> shift under warming using a 4,000-year coupled run. Co-leads the field campaign in the Bay of Maraína.",
since: "Joined 2022", orcid: "0000-0003-2218-7765",
links: { email: "[email protected]", orcid: "0000-0003-2218-7765", scholar: "#", site: "#" }
},
{
group: "phd", name: "Lukas Bergström", role: "PhD Candidate (Y4)",
pron: "they/them", color: "#7a4ea8",
interests: ["Sea-ice albedo", "Polar amplification", "GPU solvers"],
bio: "Thesis on <b>marginal-ice-zone feedbacks</b>; ported the radiative core to GPUs for a 6× speedup. Expected defense Spring 2027.",
since: "Joined 2021", orcid: "0000-0004-9981-1207",
links: { email: "[email protected]", orcid: "0000-0004-9981-1207", scholar: "#", site: "#" }
},
{
group: "phd", name: "Amara Okonkwo", role: "PhD Student (Y2)",
pron: "she/her", color: "#c9821f",
interests: ["Heatwave attribution", "Extreme statistics", "Causal inference"],
bio: "Develops a <b>causal-attribution pipeline</b> for compound heat–humidity events across coastal megacities. NSF Graduate Research Fellow.",
since: "Joined 2024", orcid: "0000-0002-6630-8842",
links: { email: "[email protected]", orcid: "0000-0002-6630-8842", scholar: "#", site: "#" }
},
{
group: "phd", name: "Diego Fuentes-Lara", role: "PhD Student (Y1)",
pron: "he/him", color: "#2f9e6f",
interests: ["Mesoscale eddies", "Lagrangian tracking", "Visualization"],
bio: "Studies <b>submesoscale stirring</b> of heat in eastern boundary currents; builds interactive volume-rendering tools for the lab.",
since: "Joined 2025", orcid: "0000-0001-3094-5518",
links: { email: "[email protected]", orcid: "0000-0001-3094-5518", scholar: "#", site: "#" }
},
{
group: "alumni", name: "Dr. Wen-Hui Chao", role: "Now Asst. Prof., Kestrel Univ.",
pron: "she/her", color: "#697892",
interests: ["Coupled data assimilation", "Predictability"],
bio: "PhD 2021. Pioneered the lab's <b>strongly-coupled assimilation</b> scheme; now runs the Predictability Group at Kestrel University.",
since: "2016–2021", orcid: "0000-0003-7712-4490",
links: { email: "[email protected]", orcid: "0000-0003-7712-4490", scholar: "#", site: "#" }
},
{
group: "alumni", name: "Dr. Samuel Drève", role: "Now Research Scientist, OceaniQ",
pron: "he/him", color: "#697892",
interests: ["Reduced-order models", "Climate services"],
bio: "Postdoc 2018–2021. Translated lab projections into operational <b>climate-risk products</b>; now leads modeling at OceaniQ Labs.",
since: "2018–2021", orcid: "0000-0002-1145-6638",
links: { email: "[email protected]", orcid: "0000-0002-1145-6638", scholar: "#", site: "#" }
}
];
/* ---------- Helpers ---------- */
function initials(name) {
var parts = name.replace(/^Dr\.\s+/, "").trim().split(/\s+/);
var first = parts[0] || "";
var last = parts[parts.length - 1] || "";
return (first.charAt(0) + (parts.length > 1 ? last.charAt(0) : "")).toUpperCase();
}
function esc(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
function svgIcon(kind) {
var p = {
email: '<path d="M3 5h18v14H3z" fill="none" stroke="currentColor" stroke-width="2"/><path d="M3 6l9 7 9-7" fill="none" stroke="currentColor" stroke-width="2"/>',
scholar: '<path d="M12 3L1 9l11 6 9-4.9V17h2V9z" fill="currentColor"/><path d="M5 12.5V16c0 1.7 3.1 3 7 3s7-1.3 7-3v-3.5" fill="none" stroke="currentColor" stroke-width="2"/>',
site: '<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"/><path d="M3 12h18M12 3c3 3 3 15 0 18M12 3c-3 3-3 15 0 18" fill="none" stroke="currentColor" stroke-width="1.6"/>'
};
return '<svg viewBox="0 0 24 24" aria-hidden="true">' + p[kind] + "</svg>";
}
var toastTimer;
function toast(msg) {
var el = document.getElementById("toast");
el.textContent = msg;
el.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { el.classList.remove("show"); }, 2400);
}
/* ---------- Render ---------- */
function cardHTML(p, idx) {
var mono = initials(p.name);
var chips = p.interests.map(function (i) {
return '<span class="chip" data-int>' + esc(i) + "</span>";
}).join("");
var orcidLink = p.links.orcid && p.links.orcid !== "#"
? "https://orcid.org/" + p.links.orcid : "#";
return '' +
'<article class="person" data-group="' + p.group + '" data-idx="' + idx + '" ' +
'data-search="' + esc((p.name + " " + p.role + " " + p.interests.join(" ")).toLowerCase()) + '">' +
'<div class="person-inner">' +
'<div class="face face-front">' +
'<div class="card-head">' +
'<div class="avatar" style="background:linear-gradient(135deg,' + p.color + ',' + shade(p.color) + ')">' + mono + "</div>" +
'<div class="card-id">' +
'<h4 class="person-name" data-name>' + esc(p.name) + "</h4>" +
'<p class="person-role" data-role>' + esc(p.role) + ' <span class="person-pron">(' + esc(p.pron) + ")</span></p>" +
"</div>" +
"</div>" +
'<div class="interests">' + chips + "</div>" +
'<div class="card-foot">' +
'<a class="icon-link" href="mailto:' + esc(p.links.email) + '" title="Email" aria-label="Email ' + esc(p.name) + '" data-track="Email">' + svgIcon("email") + "</a>" +
'<a class="icon-link" href="' + orcidLink + '" target="_blank" rel="noopener" title="ORCID" aria-label="ORCID profile" data-track="ORCID">iD</a>' +
'<a class="icon-link" href="' + esc(p.links.scholar) + '" title="Scholar" aria-label="Google Scholar" data-track="Scholar">' + svgIcon("scholar") + "</a>" +
'<a class="icon-link" href="' + esc(p.links.site) + '" title="Website" aria-label="Personal website" data-track="Website">' + svgIcon("site") + "</a>" +
'<button class="flip-btn" type="button" data-flip aria-expanded="false">Bio ›</button>' +
"</div>" +
"</div>" +
'<div class="face face-back">' +
'<h4 class="person-name">' + esc(p.name) + "</h4>" +
'<p class="bio">' + p.bio + "</p>" +
'<div class="meta-row"><span>' + esc(p.since) + "</span><span>ORCID " + esc(p.links.orcid) + "</span></div>" +
'<button class="back-close" type="button" data-flip>‹ Back</button>' +
"</div>" +
"</div>" +
"</article>";
}
// darken a hex color slightly for gradient depth
function shade(hex) {
var n = parseInt(hex.slice(1), 16);
var r = Math.max(0, (n >> 16) - 38), g = Math.max(0, ((n >> 8) & 255) - 38), b = Math.max(0, (n & 255) - 38);
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
function render() {
PEOPLE.forEach(function (p, i) {
var grid = document.querySelector('[data-grid="' + p.group + '"]');
if (grid) grid.insertAdjacentHTML("beforeend", cardHTML(p, i));
});
}
/* ---------- Filtering / search ---------- */
var state = { filter: "all", query: "" };
function clearMarks(card) {
card.querySelectorAll("mark.hit").forEach(function (m) {
m.replaceWith(document.createTextNode(m.textContent));
});
card.querySelectorAll("[data-name],[data-role],[data-int]").forEach(function (n) { n.normalize(); });
}
function highlight(card, q) {
if (!q) return;
var rx = new RegExp("(" + q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "ig");
card.querySelectorAll("[data-name],[data-role],[data-int]").forEach(function (node) {
node.childNodes.forEach(function (child) {
if (child.nodeType !== 3) return;
var txt = child.nodeValue;
if (!rx.test(txt)) return;
rx.lastIndex = 0;
var span = document.createElement("span");
span.innerHTML = esc(txt).replace(rx, '<mark class="hit">$1</mark>');
child.replaceWith(span);
});
});
}
function apply() {
var q = state.query.trim().toLowerCase();
var shown = 0;
var perGroup = {};
document.querySelectorAll(".person").forEach(function (card) {
clearMarks(card);
var g = card.getAttribute("data-group");
var matchGroup = state.filter === "all" || state.filter === g;
var matchQuery = !q || card.getAttribute("data-search").indexOf(q) !== -1;
var visible = matchGroup && matchQuery;
card.classList.toggle("is-hidden", !visible);
if (visible) {
shown++;
perGroup[g] = (perGroup[g] || 0) + 1;
if (q) highlight(card, q);
}
});
document.querySelectorAll(".group").forEach(function (sec) {
var g = sec.getAttribute("data-group");
sec.classList.toggle("is-hidden", !perGroup[g]);
});
document.querySelector("[data-result-count]").textContent = shown;
document.querySelector("[data-empty]").hidden = shown !== 0;
}
/* ---------- Flip ---------- */
function flip(card) {
var willFlip = !card.classList.contains("flipped");
card.classList.toggle("flipped", willFlip);
var btn = card.querySelector(".flip-btn");
if (btn) btn.setAttribute("aria-expanded", String(willFlip));
}
/* ---------- Wire up ---------- */
function init() {
render();
document.querySelector("[data-count-active]").textContent =
PEOPLE.filter(function (p) { return p.group !== "alumni"; }).length;
var search = document.getElementById("search");
var clearBtn = document.getElementById("clear-search");
var debounce;
search.addEventListener("input", function () {
state.query = search.value;
clearBtn.hidden = !search.value;
clearTimeout(debounce);
debounce = setTimeout(apply, 110);
});
clearBtn.addEventListener("click", function () {
search.value = "";
state.query = "";
clearBtn.hidden = true;
apply();
search.focus();
});
document.querySelectorAll(".chip-filter").forEach(function (btn) {
btn.addEventListener("click", function () {
document.querySelectorAll(".chip-filter").forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-pressed", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
state.filter = btn.getAttribute("data-filter");
apply();
});
});
var resetAll = document.getElementById("reset-all");
if (resetAll) resetAll.addEventListener("click", function () {
search.value = ""; state.query = ""; clearBtn.hidden = true;
state.filter = "all";
document.querySelectorAll(".chip-filter").forEach(function (b) {
var on = b.getAttribute("data-filter") === "all";
b.classList.toggle("is-active", on);
b.setAttribute("aria-pressed", String(on));
});
apply();
});
// delegated flip + link tracking
document.addEventListener("click", function (e) {
var flipEl = e.target.closest("[data-flip]");
if (flipEl) {
e.preventDefault();
flip(flipEl.closest(".person"));
return;
}
var noop = e.target.closest("[data-noop]");
if (noop) { e.preventDefault(); toast("Demo navigation — section not wired up."); return; }
var link = e.target.closest(".icon-link");
if (link) {
var kind = link.getAttribute("data-track");
if (link.getAttribute("href") === "#") {
e.preventDefault();
toast(kind + " link is illustrative in this demo.");
} else if (kind === "Email") {
toast("Opening email to " + link.getAttribute("href").replace("mailto:", "") + " …");
}
}
});
// keyboard: Esc closes any flipped card
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") {
document.querySelectorAll(".person.flipped").forEach(function (c) { flip(c); });
}
});
apply();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Computational Climate Dynamics Lab — People</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&family=JetBrains+Mono:wght@400;500&family=Source+Serif+4:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<header class="masthead">
<div class="masthead-inner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">CCD</span>
<div class="brand-text">
<p class="brand-eyebrow">Department of Earth System Science</p>
<h1 class="brand-title">Computational Climate Dynamics Lab</h1>
<p class="brand-sub">Whitford Institute of Technology · Maraína, Pacific Coast</p>
</div>
</div>
<nav class="masthead-nav" aria-label="Lab sections">
<a href="#people">People</a>
<a href="#" data-noop>Research</a>
<a href="#" data-noop>Publications</a>
<a href="#" data-noop>Join us</a>
</nav>
</div>
</header>
<main id="main" class="wrap">
<section class="intro" aria-labelledby="intro-h">
<h2 id="intro-h" class="section-title">Researchers & Team</h2>
<p class="lede">
We build high-resolution ocean–atmosphere coupled models to quantify regional
climate variability and extreme-event attribution. Our group spans dynamical
meteorology, machine learning for emulation, and field oceanography. Below is the
current roster across all career stages, including former members now leading their
own programs.
</p>
<dl class="stat-strip" aria-label="Group metrics">
<div class="stat"><dt>Active members</dt><dd><span class="num" data-count-active>0</span></dd></div>
<div class="stat"><dt>Open positions</dt><dd><span class="num">2</span></dd></div>
<div class="stat"><dt>Founded</dt><dd><span class="num">2014</span></dd></div>
<div class="stat"><dt>Core grant</dt><dd><span class="num">NSF AGS–2317744</span></dd></div>
</dl>
</section>
<section class="toolbar" aria-label="Filter and search people">
<div class="search-field">
<svg class="search-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 21l-4.3-4.3M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input id="search" type="search" placeholder="Search name, role, or interest…" autocomplete="off" aria-label="Search people" />
<button id="clear-search" class="clear-btn" type="button" hidden aria-label="Clear search">×</button>
</div>
<div class="filters" role="group" aria-label="Filter by group">
<button class="chip-filter is-active" data-filter="all" aria-pressed="true">All</button>
<button class="chip-filter" data-filter="pi" aria-pressed="false">PI</button>
<button class="chip-filter" data-filter="postdoc" aria-pressed="false">Postdocs</button>
<button class="chip-filter" data-filter="phd" aria-pressed="false">PhD Students</button>
<button class="chip-filter" data-filter="alumni" aria-pressed="false">Alumni</button>
</div>
<p class="result-count" aria-live="polite"><span data-result-count>0</span> people shown</p>
</section>
<p class="empty-state" data-empty hidden>No people match your search. <button type="button" class="link-btn" id="reset-all">Reset filters</button></p>
<section class="group" data-group="pi" aria-labelledby="g-pi">
<h3 class="group-title" id="g-pi">Principal Investigator</h3>
<div class="grid" data-grid="pi"></div>
</section>
<section class="group" data-group="postdoc" aria-labelledby="g-postdoc">
<h3 class="group-title" id="g-postdoc">Postdoctoral Researchers</h3>
<div class="grid" data-grid="postdoc"></div>
</section>
<section class="group" data-group="phd" aria-labelledby="g-phd">
<h3 class="group-title" id="g-phd">PhD Students</h3>
<div class="grid" data-grid="phd"></div>
</section>
<section class="group" data-group="alumni" aria-labelledby="g-alumni">
<h3 class="group-title" id="g-alumni">Alumni</h3>
<div class="grid" data-grid="alumni"></div>
</section>
<figure class="join-fig">
<figcaption><strong>Figure 1.</strong> We recruit year-round. Prospective PhD students apply through the
Earth System Science graduate program; postdoc inquiries go directly to the PI with a CV and a one-page research statement.</figcaption>
</figure>
</main>
<footer class="site-footer">
<p>Computational Climate Dynamics Lab · Whitford Institute of Technology</p>
<p class="footer-fine">Illustrative directory — all people, affiliations, and identifiers are fictional.</p>
</footer>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Researchers / Team Page
A research-group directory for the fictional Computational Climate Dynamics Lab. The roster is organized into four landmark sections — Principal Investigator, Postdoctoral Researchers, PhD Students, and Alumni — each rendered as a grid of avatar cards. Avatars are pure CSS monograms with a per-person gradient, so no images are needed. Every card shows the person’s name, role, pronouns, a set of research-interest chips, and a row of links: email, ORCID iD, Google Scholar, and personal site.
The toolbar provides a debounced live search and a set of filter chips. Searching matches across names, roles, and interests, highlights the hits inline, hides empty groups, and updates a live result count; the filter chips constrain the view to a single career stage. When nothing matches, an empty state offers a one-click reset.
Each card is a flip card: pressing “Bio” rotates it to a colored back face with a short biography plus tenure and ORCID metadata, and Escape closes any open card. Illustrative links surface a toast instead of navigating, controls are keyboard-operable with visible focus, and the layout collapses to a single column at narrow widths. Fonts follow the house system — Source Serif 4 for prose, Inter for UI, JetBrains Mono for identifiers and numbers.
Illustrative UI only — fictional authors, data, and figures; not real scientific results.