Science — Figure + Numbered Caption
A reusable scientific figure-block primitive for journal-style web articles. Each block pairs an inline-SVG chart with a bold numbered label, a descriptive caption, and a source line; an on-hover toolbar lets readers expand the figure to a focus-trapped lightbox, copy a formatted citation, or download the underlying vector as standalone SVG. Ships three layout variants — full-width, half-width, and a labelled multi-panel grid — built with vanilla JS, no charting libraries, and tuned for AA contrast and 360px screens.
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-1: 0 1px 2px rgba(15, 27, 45, 0.05), 0 1px 0 rgba(15, 27, 45, 0.02);
--shadow-2: 0 6px 24px rgba(15, 27, 45, 0.08), 0 2px 6px rgba(15, 27, 45, 0.05);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scroll-behavior: smooth;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Source Serif 4", Georgia, serif;
line-height: 1.6;
font-size: 16px;
}
.wrap {
max-width: 940px;
margin: 0 auto;
padding: 0 24px;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-feature-settings: "tnum" 1;
}
code {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-size: 0.85em;
background: var(--accent-50);
color: var(--accent-d);
padding: 0.08em 0.36em;
border-radius: var(--r-sm);
}
.muted {
color: var(--muted);
}
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: var(--accent);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
font-family: "Inter", sans-serif;
z-index: 100;
}
.skip-link:focus {
left: 12px;
top: 12px;
}
:focus-visible {
outline: 2.5px solid var(--accent);
outline-offset: 2px;
border-radius: 3px;
}
/* ---------- HEADER ---------- */
.page-head {
background: linear-gradient(180deg, var(--bg-alt), var(--bg));
border-bottom: 1px solid var(--line);
padding: 52px 0 38px;
}
.eyebrow {
font-family: "Inter", sans-serif;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.14em;
color: var(--accent);
margin: 0 0 8px;
}
.page-head h1 {
font-size: clamp(28px, 5vw, 42px);
line-height: 1.12;
font-weight: 700;
margin: 0 0 14px;
letter-spacing: -0.01em;
}
.lede {
font-size: 18px;
color: var(--ink-2);
max-width: 64ch;
margin: 0 0 22px;
}
.meta-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
font-family: "Inter", sans-serif;
font-size: 12.5px;
font-weight: 500;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
padding: 5px 11px;
border-radius: 999px;
box-shadow: var(--shadow-1);
}
.chip-doi {
font-family: "JetBrains Mono", monospace;
font-size: 11.5px;
}
.chip-ok {
color: var(--ok);
border-color: rgba(47, 158, 111, 0.35);
background: rgba(47, 158, 111, 0.07);
}
main {
padding: 44px 24px 80px;
}
/* ---------- FIGURE BLOCK ---------- */
.fig {
margin: 0 0 46px;
}
.fig-frame {
position: relative;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-1);
overflow: hidden;
transition: box-shadow 0.18s ease, border-color 0.18s ease;
}
.fig-frame:hover {
box-shadow: var(--shadow-2);
border-color: var(--line-2);
}
.fig-toolbar {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 6px;
z-index: 3;
opacity: 0;
transform: translateY(-4px);
transition: opacity 0.18s ease, transform 0.18s ease;
}
.fig-frame:hover .fig-toolbar,
.fig-frame:focus-within .fig-toolbar {
opacity: 1;
transform: translateY(0);
}
.tb-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-family: "Inter", sans-serif;
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(4px);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 6px 9px;
cursor: pointer;
transition: all 0.14s ease;
}
.tb-btn svg {
width: 14px;
height: 14px;
}
.tb-btn.icon-only {
padding: 6px;
}
.tb-btn:hover {
color: #fff;
background: var(--accent);
border-color: var(--accent);
}
.tb-btn:active {
transform: scale(0.94);
}
.fig-canvas {
padding: 20px 22px 16px;
background: var(--bg-alt);
}
.chart {
width: 100%;
height: auto;
display: block;
}
/* chart primitives */
.grid line {
stroke: var(--line);
stroke-width: 1;
}
.tick text {
font-family: "JetBrains Mono", monospace;
font-size: 10px;
fill: var(--muted);
text-anchor: end;
}
.xtick text {
text-anchor: middle;
}
.axis-label {
font-family: "Inter", sans-serif;
font-size: 11px;
font-weight: 600;
fill: var(--ink-2);
}
.band {
fill: rgba(26, 79, 138, 0.1);
}
.series {
fill: none;
stroke-width: 2.4;
stroke-linejoin: round;
stroke-linecap: round;
}
.s-a {
stroke: var(--accent);
}
.s-b {
stroke: var(--teal);
stroke-dasharray: 6 5;
}
.s-a-pt circle {
fill: var(--accent);
}
.s-b-pt circle {
fill: var(--teal);
}
.legend {
display: flex;
gap: 18px;
margin-top: 6px;
font-family: "Inter", sans-serif;
font-size: 12px;
font-weight: 500;
}
.lg {
display: inline-flex;
align-items: center;
color: var(--ink-2);
}
.lg::before {
content: "";
width: 16px;
height: 3px;
border-radius: 2px;
margin-right: 7px;
}
.lg-a::before {
background: var(--accent);
}
.lg-b::before {
background: var(--teal);
}
.bars rect {
fill: var(--accent);
transition: fill 0.15s ease;
}
.bars rect:nth-child(even) {
fill: var(--accent-d);
}
.fig-frame:hover .bars rect {
fill: var(--teal);
}
.err line {
stroke: var(--ink);
stroke-width: 1.4;
}
/* heatmap */
.heat-cell {
stroke: #fff;
stroke-width: 2;
}
.heat-scale {
display: flex;
align-items: center;
gap: 8px;
justify-content: center;
margin-top: 8px;
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--muted);
}
.heat-scale .ramp {
width: 160px;
height: 10px;
border-radius: 999px;
background: linear-gradient(90deg, var(--accent-d), var(--accent-50), var(--warn), var(--danger));
}
/* scatter / panels */
.scatter circle {
fill: var(--accent);
opacity: 0.78;
}
.fit {
stroke: var(--danger);
stroke-width: 2;
stroke-dasharray: 5 4;
}
.area {
fill: rgba(15, 125, 120, 0.16);
}
.curve {
fill: none;
stroke: var(--teal);
stroke-width: 2.2;
}
.median {
stroke: var(--ink-2);
stroke-width: 1.4;
stroke-dasharray: 4 3;
}
.st {
stroke: #fff;
stroke-width: 1;
}
.st-1 {
fill: var(--accent-d);
}
.st-2 {
fill: var(--accent);
}
.st-3 {
fill: var(--teal);
}
.panels {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
padding: 18px;
}
.panel {
position: relative;
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 12px 12px 10px;
}
.panel-tag {
position: absolute;
top: 8px;
left: 10px;
font-family: "Inter", sans-serif;
font-weight: 700;
font-size: 13px;
color: var(--ink);
}
.panel-cap {
font-size: 12.5px;
color: var(--muted);
margin: 6px 0 0;
line-height: 1.45;
}
/* ---------- FIGCAPTION ---------- */
figcaption {
font-size: 14.5px;
color: var(--ink-2);
line-height: 1.55;
padding: 14px 4px 0;
}
.fig-label {
font-weight: 700;
color: var(--ink);
margin-right: 4px;
}
.src {
display: block;
margin-top: 8px;
font-family: "Inter", sans-serif;
font-size: 12px;
color: var(--muted);
}
/* ---------- LAYOUT GRID ---------- */
.fig-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 26px;
margin-bottom: 46px;
}
.fig-grid .fig {
margin-bottom: 0;
}
/* ---------- LIGHTBOX ---------- */
.lightbox {
position: fixed;
inset: 0;
z-index: 200;
background: rgba(8, 14, 24, 0.78);
backdrop-filter: blur(6px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
animation: fade 0.18s ease;
}
.lightbox[hidden] {
display: none;
}
@keyframes fade {
from {
opacity: 0;
}
}
.lb-bar {
width: min(960px, 100%);
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
margin-bottom: 10px;
}
.lb-title {
font-family: "Inter", sans-serif;
font-weight: 600;
font-size: 14px;
letter-spacing: 0.02em;
}
.lb-close {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff;
font-size: 22px;
line-height: 1;
width: 38px;
height: 38px;
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.14s ease;
}
.lb-close:hover {
background: rgba(255, 255, 255, 0.24);
}
.lb-stage {
width: min(960px, 100%);
background: #fff;
border-radius: var(--r-md);
padding: 24px;
max-height: 70vh;
overflow: auto;
box-shadow: var(--shadow-2);
}
.lb-stage .chart {
max-height: 64vh;
}
.lb-cap {
width: min(960px, 100%);
color: rgba(255, 255, 255, 0.86);
font-size: 14px;
margin: 14px 0 0;
}
.lb-cap .fig-label {
color: #fff;
}
.lb-cap .src {
color: rgba(255, 255, 255, 0.6);
}
/* ---------- TOAST ---------- */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translate(-50%, 16px);
background: var(--ink);
color: #fff;
font-family: "Inter", sans-serif;
font-size: 13.5px;
font-weight: 500;
padding: 11px 18px;
border-radius: var(--r-md);
box-shadow: var(--shadow-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 300;
max-width: 86vw;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- RESPONSIVE ---------- */
@media (max-width: 640px) {
body {
font-size: 15px;
}
main {
padding: 30px 16px 60px;
}
.wrap {
padding: 0 16px;
}
.fig-grid {
grid-template-columns: 1fr;
gap: 30px;
}
.panels {
grid-template-columns: 1fr;
}
.fig-toolbar {
opacity: 1;
transform: none;
}
.lede {
font-size: 16px;
}
.fig-canvas {
padding: 14px 12px 12px;
overflow-x: auto;
}
}(function () {
"use strict";
/* ---------- toast helper ---------- */
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");
}, 2200);
}
/* ---------- per-figure citation metadata ---------- */
var FIG_META = {
"fig-1": {
n: "1",
title: "Rarefied taxonomic richness vs. excavation depth",
cite:
"Marwick, B. & Okonkwo, A. (2026). Figure 1: Rarefied taxonomic richness vs. excavation depth. " +
"Deepening-time signals in two Late Holocene assemblages. arXiv:2606.04417v2. DOI:10.5281/zenodo.10492.",
},
"fig-2": {
n: "2",
title: "Mean δ¹³C by stratigraphic unit",
cite:
"Marwick, B. & Okonkwo, A. (2026). Figure 2: Mean δ¹³C by stratigraphic unit. " +
"arXiv:2606.04417v2. DOI:10.5281/zenodo.10492.",
},
"fig-3": {
n: "3",
title: "Proxy correlation matrix (core QD-07)",
cite:
"Marwick, B. & Okonkwo, A. (2026). Figure 3: Pairwise Pearson correlation matrix, core QD-07. " +
"arXiv:2606.04417v2. DOI:10.5281/zenodo.10492.",
},
"fig-4": {
n: "4",
title: "QD-07 multiproxy summary (a–c)",
cite:
"Marwick, B. & Okonkwo, A. (2026). Figure 4: Multiproxy summary of the QD-07 record (a–c). " +
"arXiv:2606.04417v2. DOI:10.5281/zenodo.10492.",
},
};
/* ---------- build Figure 3 heatmap ---------- */
(function buildHeatmap() {
var g = document.getElementById("heatCells");
if (!g) return;
var labels = ["Pol", "Chr", "MS", "GS", "LOI", "δ¹⁸O"];
var n = labels.length;
var cell = 36;
var svgNS = "http://www.w3.org/2000/svg";
// deterministic pseudo-correlation matrix (symmetric, diag = 1)
var seed = function (i, j) {
var v = Math.sin((i + 1) * 12.9898 + (j + 1) * 78.233) * 43758.5453;
return v - Math.floor(v); // 0..1
};
function colorFor(r) {
// r in [-1,1] -> diverging blue (neg) to red (pos)
var t = (r + 1) / 2;
if (t < 0.5) {
var k = t / 0.5;
return mix([18, 58, 102], [233, 240, 249], k);
}
var k2 = (t - 0.5) / 0.5;
return mix([233, 240, 249], [207, 69, 56], k2);
}
function mix(a, b, t) {
return (
"rgb(" +
Math.round(a[0] + (b[0] - a[0]) * t) +
"," +
Math.round(a[1] + (b[1] - a[1]) * t) +
"," +
Math.round(a[2] + (b[2] - a[2]) * t) +
")"
);
}
for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
var r = i === j ? 1 : (seed(Math.min(i, j), Math.max(i, j)) * 2 - 1) * 0.9;
r = Math.round(r * 100) / 100;
var rect = document.createElementNS(svgNS, "rect");
rect.setAttribute("x", j * cell);
rect.setAttribute("y", i * cell);
rect.setAttribute("width", cell);
rect.setAttribute("height", cell);
rect.setAttribute("class", "heat-cell");
rect.setAttribute("fill", colorFor(r));
var tt = document.createElementNS(svgNS, "title");
tt.textContent = labels[i] + " × " + labels[j] + ": r = " + r.toFixed(2);
rect.appendChild(tt);
g.appendChild(rect);
}
}
// axis labels
for (var a = 0; a < n; a++) {
var tx = document.createElementNS(svgNS, "text");
tx.setAttribute("x", a * cell + cell / 2);
tx.setAttribute("y", n * cell + 14);
tx.setAttribute("class", "tick xtick");
tx.setAttribute("text-anchor", "middle");
tx.setAttribute("font-size", "10");
tx.setAttribute("fill", "#697892");
tx.setAttribute("font-family", "JetBrains Mono, monospace");
tx.textContent = labels[a];
g.appendChild(tx);
var ty = document.createElementNS(svgNS, "text");
ty.setAttribute("x", -8);
ty.setAttribute("y", a * cell + cell / 2 + 3);
ty.setAttribute("text-anchor", "end");
ty.setAttribute("font-size", "10");
ty.setAttribute("fill", "#697892");
ty.setAttribute("font-family", "JetBrains Mono, monospace");
ty.textContent = labels[a];
g.appendChild(ty);
}
})();
/* ---------- lightbox ---------- */
var lb = document.getElementById("lightbox");
var lbStage = document.getElementById("lbStage");
var lbCap = document.getElementById("lbCap");
var lbTitle = document.getElementById("lbTitle");
var lbClose = document.getElementById("lbClose");
var lastFocused = null;
function openLightbox(fig) {
var id = fig.getAttribute("data-fig-id");
var meta = FIG_META[id] || { n: "?", title: "Figure" };
var canvas = fig.querySelector(".fig-canvas");
var caption = fig.querySelector("figcaption");
lbStage.innerHTML = "";
if (canvas) lbStage.appendChild(canvas.cloneNode(true));
lbCap.innerHTML = caption ? caption.innerHTML : "";
lbTitle.textContent = "Figure " + meta.n + " — " + meta.title;
lastFocused = document.activeElement;
lb.hidden = false;
document.body.style.overflow = "hidden";
lbClose.focus();
}
function closeLightbox() {
lb.hidden = true;
document.body.style.overflow = "";
lbStage.innerHTML = "";
if (lastFocused && lastFocused.focus) lastFocused.focus();
}
lbClose.addEventListener("click", closeLightbox);
lb.addEventListener("click", function (e) {
if (e.target === lb) closeLightbox();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && !lb.hidden) closeLightbox();
});
/* ---------- download figure as standalone SVG ---------- */
function downloadFigure(fig) {
var svg = fig.querySelector("svg.chart");
var id = fig.getAttribute("data-fig-id");
var meta = FIG_META[id] || { n: "x" };
if (!svg) {
// multi-panel: grab the first svg as a representative export
svg = fig.querySelector("svg");
}
if (!svg) {
toast("No vector graphic to export");
return;
}
var clone = svg.cloneNode(true);
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
var src =
'<?xml version="1.0" encoding="UTF-8"?>\n' + new XMLSerializer().serializeToString(clone);
var blob = new Blob([src], { type: "image/svg+xml;charset=utf-8" });
var url = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = url;
a.download = "figure-" + meta.n + ".svg";
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(function () {
URL.revokeObjectURL(url);
}, 1000);
toast("Downloaded figure-" + meta.n + ".svg");
}
/* ---------- copy citation ---------- */
function copyCitation(fig) {
var id = fig.getAttribute("data-fig-id");
var meta = FIG_META[id];
if (!meta) return;
var text = meta.cite;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
function () {
toast("Citation copied to clipboard");
},
function () {
fallbackCopy(text);
}
);
} else {
fallbackCopy(text);
}
}
function fallbackCopy(text) {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try {
document.execCommand("copy");
toast("Citation copied to clipboard");
} catch (e) {
toast("Copy failed — select manually");
}
ta.remove();
}
/* ---------- delegate toolbar clicks ---------- */
document.addEventListener("click", function (e) {
var btn = e.target.closest ? e.target.closest(".tb-btn") : null;
if (!btn) return;
var fig = btn.closest(".fig");
if (!fig) return;
var act = btn.getAttribute("data-act");
if (act === "expand") openLightbox(fig);
else if (act === "cite") copyCitation(fig);
else if (act === "download") downloadFigure(fig);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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=Source+Serif+4:ital,wght@0,400;0,600;0,700;1,400&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
<title>Figure + Numbered Caption — Reusable Scientific Figure Block</title>
</head>
<body>
<a class="skip-link" href="#main">Skip to figures</a>
<header class="page-head">
<div class="wrap">
<p class="eyebrow">METHODS & FIGURES · v2.4</p>
<h1>Reproducible Figure Block</h1>
<p class="lede">
A self-contained <code><figure></code> primitive for scientific articles —
numbered captions, source attribution, optional sub-panels, and a per-figure toolbar
to expand, cite, and download. Below: three layout variants drawn entirely with inline
SVG and CSS.
</p>
<div class="meta-row" role="group" aria-label="Document metadata">
<span class="chip">Marwick & Okonkwo, 2026</span>
<span class="chip">Preprint · arXiv:2606.04417</span>
<span class="chip chip-doi">DOI 10.5281/zenodo.10492<span class="muted">·v2</span></span>
<span class="chip chip-ok">Peer-reviewed</span>
</div>
</div>
</header>
<main id="main" class="wrap" tabindex="-1">
<!-- ============ VARIANT 1 — FULL WIDTH ============ -->
<figure class="fig fig-full" data-fig-id="fig-1">
<div class="fig-frame">
<div class="fig-toolbar" role="toolbar" aria-label="Figure 1 actions">
<button class="tb-btn" data-act="expand" aria-label="Expand Figure 1 to fullscreen">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<span>Expand</span>
</button>
<button class="tb-btn" data-act="cite" aria-label="Copy citation for Figure 1">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 2h7a1 1 0 011 1v9M3 4h7a1 1 0 011 1v9a1 1 0 01-1 1H3a1 1 0 01-1-1V5a1 1 0 011-1z" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>
<span>Cite</span>
</button>
<button class="tb-btn" data-act="download" aria-label="Download Figure 1 as SVG">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2v8m0 0L5 7m3 3l3-3M3 13h10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>SVG</span>
</button>
</div>
<div class="fig-canvas" data-export="line">
<svg class="chart" viewBox="0 0 720 320" preserveAspectRatio="xMidYMid meet" role="img"
aria-label="Line chart: assemblage richness rising with excavation depth across two sites.">
<!-- gridlines -->
<g class="grid">
<line x1="64" y1="40" x2="64" y2="270"/>
<line x1="64" y1="270" x2="690" y2="270"/>
<line x1="64" y1="212" x2="690" y2="212"/>
<line x1="64" y1="154" x2="690" y2="154"/>
<line x1="64" y1="96" x2="690" y2="96"/>
</g>
<!-- y ticks -->
<g class="tick">
<text x="56" y="274">0</text>
<text x="56" y="216">12</text>
<text x="56" y="158">24</text>
<text x="56" y="100">36</text>
<text x="56" y="44">48</text>
</g>
<!-- x ticks -->
<g class="tick xtick">
<text x="120" y="288">0.5</text>
<text x="260" y="288">1.0</text>
<text x="400" y="288">1.5</text>
<text x="540" y="288">2.0</text>
<text x="668" y="288">2.5</text>
</g>
<!-- confidence band -->
<path class="band" d="M120 232 L260 188 L400 132 L540 92 L668 70 L668 96 L540 122 L400 162 L260 214 L120 252 Z"/>
<!-- series A -->
<polyline class="series s-a" points="120,242 260,200 400,146 540,102 668,82"/>
<!-- series B -->
<polyline class="series s-b" points="120,256 260,236 400,210 540,196 668,184"/>
<g class="pts s-a-pt"><circle cx="120" cy="242" r="4"/><circle cx="260" cy="200" r="4"/><circle cx="400" cy="146" r="4"/><circle cx="540" cy="102" r="4"/><circle cx="668" cy="82" r="4"/></g>
<g class="pts s-b-pt"><circle cx="120" cy="256" r="4"/><circle cx="260" cy="236" r="4"/><circle cx="400" cy="210" r="4"/><circle cx="540" cy="196" r="4"/><circle cx="668" cy="184" r="4"/></g>
<!-- axis labels -->
<text class="axis-label" x="377" y="312" text-anchor="middle">Excavation depth (m below datum)</text>
<text class="axis-label" x="20" y="155" transform="rotate(-90 20 155)" text-anchor="middle">Taxonomic richness (S)</text>
</svg>
<div class="legend" aria-hidden="true">
<span class="lg lg-a">Tell Qadis (n=312)</span>
<span class="lg lg-b">Wadi Suhl (n=204)</span>
</div>
</div>
</div>
<figcaption>
<span class="fig-label">Figure 1.</span>
Rarefied taxonomic richness as a function of excavation depth at two Late Holocene
sites. Shaded region denotes the 95% bootstrap confidence interval
(<span class="mono">10 000</span> resamples). Richness at Tell Qadis increases
monotonically with depth, consistent with a deepening-time hypothesis.
<span class="src">Source: Marwick & Okonkwo (2026), Supplementary Dataset S1. CC BY 4.0.</span>
</figcaption>
</figure>
<!-- ============ ROW: HALF + HALF ============ -->
<div class="fig-grid">
<!-- VARIANT 2 — HALF WIDTH, BAR CHART -->
<figure class="fig fig-half" data-fig-id="fig-2">
<div class="fig-frame">
<div class="fig-toolbar" role="toolbar" aria-label="Figure 2 actions">
<button class="tb-btn icon-only" data-act="expand" aria-label="Expand Figure 2">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</button>
<button class="tb-btn icon-only" data-act="cite" aria-label="Copy citation for Figure 2">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 2h7a1 1 0 011 1v9M3 4h7a1 1 0 011 1v9a1 1 0 01-1 1H3a1 1 0 01-1-1V5a1 1 0 011-1z" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>
</button>
<button class="tb-btn icon-only" data-act="download" aria-label="Download Figure 2 as SVG">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2v8m0 0L5 7m3 3l3-3M3 13h10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="fig-canvas" data-export="bar">
<svg class="chart" viewBox="0 0 360 280" preserveAspectRatio="xMidYMid meet" role="img"
aria-label="Bar chart comparing mean δ13C across four stratigraphic units.">
<g class="grid">
<line x1="46" y1="30" x2="46" y2="230"/>
<line x1="46" y1="230" x2="338" y2="230"/>
<line x1="46" y1="180" x2="338" y2="180"/>
<line x1="46" y1="130" x2="338" y2="130"/>
<line x1="46" y1="80" x2="338" y2="80"/>
</g>
<g class="tick">
<text x="40" y="234">-26</text>
<text x="40" y="184">-24</text>
<text x="40" y="134">-22</text>
<text x="40" y="84">-20</text>
</g>
<g class="bars">
<rect x="72" y="120" width="44" height="110"/>
<rect x="142" y="92" width="44" height="138"/>
<rect x="212" y="150" width="44" height="80"/>
<rect x="282" y="64" width="44" height="166"/>
</g>
<g class="err">
<line x1="94" y1="108" x2="94" y2="134"/><line x1="89" y1="108" x2="99" y2="108"/><line x1="89" y1="134" x2="99" y2="134"/>
<line x1="164" y1="80" x2="164" y2="106"/><line x1="159" y1="80" x2="169" y2="80"/><line x1="159" y1="106" x2="169" y2="106"/>
<line x1="234" y1="138" x2="234" y2="164"/><line x1="229" y1="138" x2="239" y2="138"/><line x1="229" y1="164" x2="239" y2="164"/>
<line x1="304" y1="52" x2="304" y2="78"/><line x1="299" y1="52" x2="309" y2="52"/><line x1="299" y1="78" x2="309" y2="78"/>
</g>
<g class="tick xtick">
<text x="94" y="248">U1</text>
<text x="164" y="248">U2</text>
<text x="234" y="248">U3</text>
<text x="304" y="248">U4</text>
</g>
<text class="axis-label" x="20" y="130" transform="rotate(-90 20 130)" text-anchor="middle">δ¹³C (‰, VPDB)</text>
<text class="axis-label" x="192" y="268" text-anchor="middle">Stratigraphic unit</text>
</svg>
</div>
</div>
<figcaption>
<span class="fig-label">Figure 2.</span>
Mean stable carbon isotope ratios (δ¹³C) by stratigraphic unit. Whiskers
show ±1 standard error (<span class="mono">n = 18–24</span> per unit). The
enrichment in U4 indicates a shift toward C₄ resources.
<span class="src">Source: author measurements, IRMS run 2026-03.</span>
</figcaption>
</figure>
<!-- VARIANT 3 — HALF WIDTH, MICROGRAPH / HEATMAP -->
<figure class="fig fig-half" data-fig-id="fig-3">
<div class="fig-frame">
<div class="fig-toolbar" role="toolbar" aria-label="Figure 3 actions">
<button class="tb-btn icon-only" data-act="expand" aria-label="Expand Figure 3">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</button>
<button class="tb-btn icon-only" data-act="cite" aria-label="Copy citation for Figure 3">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 2h7a1 1 0 011 1v9M3 4h7a1 1 0 011 1v9a1 1 0 01-1 1H3a1 1 0 01-1-1V5a1 1 0 011-1z" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>
</button>
<button class="tb-btn icon-only" data-act="download" aria-label="Download Figure 3 as SVG">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2v8m0 0L5 7m3 3l3-3M3 13h10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</button>
</div>
<div class="fig-canvas heat-canvas" data-export="heat">
<svg class="chart" viewBox="0 0 360 280" preserveAspectRatio="xMidYMid meet" role="img"
aria-label="Correlation heatmap, six by six matrix of proxy variables.">
<defs>
<pattern id="hatch" width="6" height="6" patternUnits="userSpaceOnUse" patternTransform="rotate(45)">
<rect width="6" height="6" fill="none"/><line x1="0" y1="0" x2="0" y2="6" stroke="rgba(255,255,255,.5)" stroke-width="1"/>
</pattern>
</defs>
<g id="heatCells" transform="translate(56,18)"></g>
<text class="axis-label" x="190" y="262" text-anchor="middle">Pearson r — proxy × proxy</text>
</svg>
<div class="heat-scale" aria-hidden="true">
<span>-1</span><span class="ramp"></span><span>+1</span>
</div>
</div>
</div>
<figcaption>
<span class="fig-label">Figure 3.</span>
Pairwise Pearson correlation matrix among six environmental proxies (pollen, charcoal,
magnetic susceptibility, grain size, LOI, δ¹�O). Warmer cells denote
stronger positive covariation.
<span class="src">Source: derived from core <span class="mono">QD-07</span>.</span>
</figcaption>
</figure>
</div>
<!-- ============ VARIANT 4 — MULTI-PANEL (a)(b)(c) ============ -->
<figure class="fig fig-full multi" data-fig-id="fig-4">
<div class="fig-frame">
<div class="fig-toolbar" role="toolbar" aria-label="Figure 4 actions">
<button class="tb-btn" data-act="expand" aria-label="Expand Figure 4 to fullscreen">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 6V2h4M14 6V2h-4M2 10v4h4M14 10v4h-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<span>Expand</span>
</button>
<button class="tb-btn" data-act="cite" aria-label="Copy citation for Figure 4">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 2h7a1 1 0 011 1v9M3 4h7a1 1 0 011 1v9a1 1 0 01-1 1H3a1 1 0 01-1-1V5a1 1 0 011-1z" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>
<span>Cite</span>
</button>
<button class="tb-btn" data-act="download" aria-label="Download Figure 4 as SVG">
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 2v8m0 0L5 7m3 3l3-3M3 13h10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
<span>SVG</span>
</button>
</div>
<div class="fig-canvas panels" data-export="multi">
<div class="panel">
<span class="panel-tag">(a)</span>
<svg class="chart" viewBox="0 0 240 180" role="img" aria-label="Scatter plot panel a.">
<g class="grid"><line x1="30" y1="20" x2="30" y2="150"/><line x1="30" y1="150" x2="225" y2="150"/></g>
<g class="scatter">
<circle cx="60" cy="120" r="4"/><circle cx="80" cy="108" r="4"/><circle cx="98" cy="112" r="4"/>
<circle cx="120" cy="90" r="4"/><circle cx="140" cy="78" r="4"/><circle cx="158" cy="84" r="4"/>
<circle cx="180" cy="58" r="4"/><circle cx="200" cy="46" r="4"/>
</g>
<line class="fit" x1="50" y1="126" x2="210" y2="42"/>
</svg>
<p class="panel-cap">Body mass × femur length. Allometric slope <span class="mono">β=0.71</span>.</p>
</div>
<div class="panel">
<span class="panel-tag">(b)</span>
<svg class="chart" viewBox="0 0 240 180" role="img" aria-label="Density curve panel b.">
<g class="grid"><line x1="30" y1="20" x2="30" y2="150"/><line x1="30" y1="150" x2="225" y2="150"/></g>
<path class="area" d="M30 150 C70 150 80 60 120 56 C160 52 170 150 225 150 Z"/>
<path class="curve" d="M30 150 C70 150 80 60 120 56 C160 52 170 150 225 150"/>
<line class="median" x1="120" y1="40" x2="120" y2="150"/>
</svg>
<p class="panel-cap">Kernel density of radiocarbon dates; median <span class="mono">3.42 ka</span>.</p>
</div>
<div class="panel">
<span class="panel-tag">(c)</span>
<svg class="chart" viewBox="0 0 240 180" role="img" aria-label="Stacked area panel c.">
<g class="grid"><line x1="30" y1="20" x2="30" y2="150"/><line x1="30" y1="150" x2="225" y2="150"/></g>
<path class="st st-1" d="M30 150 L30 120 L90 108 L150 116 L225 100 L225 150 Z"/>
<path class="st st-2" d="M30 120 L90 108 L150 116 L225 100 L225 72 L150 84 L90 70 L30 86 Z"/>
<path class="st st-3" d="M30 86 L90 70 L150 84 L225 72 L225 40 L150 52 L90 38 L30 50 Z"/>
</svg>
<p class="panel-cap">Vegetation composition through time (arboreal / shrub / herb).</p>
</div>
</div>
</div>
<figcaption>
<span class="fig-label">Figure 4.</span>
Three-panel summary of the QD-07 multiproxy record.
<strong>(a)</strong> log–log allometry of skeletal elements;
<strong>(b)</strong> summed-probability density of <span class="mono">¹⁴C</span> determinations;
<strong>(c)</strong> pollen-derived vegetation composition over the last 4 kyr. All
axes share a common calibrated timescale.
<span class="src">Source: Marwick & Okonkwo (2026), Figs. S4–S6.</span>
</figcaption>
</figure>
</main>
<!-- ============ LIGHTBOX OVERLAY ============ -->
<div class="lightbox" id="lightbox" role="dialog" aria-modal="true" aria-label="Expanded figure" hidden>
<div class="lb-bar">
<span class="lb-title" id="lbTitle">Figure</span>
<button class="lb-close" id="lbClose" aria-label="Close expanded figure">×</button>
</div>
<div class="lb-stage" id="lbStage"></div>
<p class="lb-cap" id="lbCap"></p>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Figure + Numbered Caption
A small but disciplined building block for research-grade web articles: a <figure> that always carries a bold “Figure N.” label, a descriptive caption, and a muted source line set in Inter — the typographic conventions readers expect from a journal. The chart artwork is drawn entirely with inline SVG and CSS (line series with a bootstrap confidence band, a bar chart with error whiskers, a JS-generated correlation heatmap, and a three-panel grid), so there is no dependency on KaTeX, Chart.js, or any runtime library.
Every figure exposes a compact toolbar that fades in on hover or keyboard focus. Expand clones the figure into a focus-trapped lightbox overlay (Escape or backdrop click to close, focus returned to the trigger). Cite writes a fully formatted citation — fictional authors, arXiv ID, and DOI — to the clipboard with a graceful execCommand fallback. Download serializes the chart’s SVG to a Blob and saves it as figure-N.svg. A single delegated click handler and a reusable toast() helper keep the script tiny.
The page demonstrates three reusable variants — full-width, two half-width side by side, and a multi-panel block with (a)(b)(c) sub-labels and per-panel captions. Variables are set in serif italics and all numbers, units, and DOIs use JetBrains Mono. Everything collapses to a single column and keeps the toolbar visible below 640px, with horizontal scroll for charts on the narrowest screens.
Illustrative UI only — fictional authors, data, and figures; not real scientific results.