Science — Experiment / Methods Timeline
An ordered methods timeline that walks a fictional photothermal hydrogel drug-release assay from preparation through analysis. Steps are grouped into four colour-coded phases with status dots, wall-clock and hands-on durations, and per-step reagents and instruments. Each step expands to a recorded-parameters table with units and significant figures, a Play control sequentially highlights steps while a progress bar fills, phase chips filter the track, and a proportional phase-duration figure summarises the run.
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);
--shadow-md: 0 4px 14px rgba(15, 27, 45, 0.08);
/* phase colors */
--p-prep: #1a4f8a;
--p-treat: #c9821f;
--p-meas: #0f7d78;
--p-ana: #6b4fb0;
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font-family: "Source Serif 4", Georgia, serif;
line-height: 1.6;
font-size: 16px;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.mono {
font-family: "JetBrains Mono", ui-monospace, monospace;
font-variant-numeric: tabular-nums;
}
.skip-link {
position: absolute;
left: -999px;
top: 0;
background: var(--accent);
color: #fff;
padding: 8px 14px;
border-radius: 0 0 var(--r-sm) 0;
z-index: 50;
font-family: "Inter", sans-serif;
font-size: 14px;
}
.skip-link:focus {
left: 0;
}
.wrap {
max-width: 980px;
margin: 0 auto;
padding: 40px 24px 64px;
}
/* ---------- masthead ---------- */
.masthead {
border-bottom: 1px solid var(--line);
padding-bottom: 26px;
margin-bottom: 28px;
}
.masthead__id {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px;
}
.badge {
font-family: "Inter", sans-serif;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.02em;
padding: 4px 10px;
border-radius: 999px;
border: 1px solid var(--line-2);
color: var(--ink-2);
background: var(--bg-alt);
}
.badge--proto {
color: #fff;
background: var(--accent);
border-color: var(--accent);
}
.badge--rev {
color: var(--teal);
background: var(--teal-50);
border-color: rgba(15, 125, 120, 0.3);
}
.badge--prereg .mono {
font-size: 11px;
}
h1 {
font-weight: 700;
font-size: clamp(1.5rem, 3.5vw, 2.1rem);
line-height: 1.25;
margin: 0 0 12px;
letter-spacing: -0.01em;
}
.masthead__sub {
color: var(--ink-2);
margin: 0 0 14px;
max-width: 68ch;
}
.masthead__byline {
font-size: 12.5px;
color: var(--muted);
margin: 0;
}
.masthead__byline span {
color: var(--accent);
}
/* ---------- controls ---------- */
.controls {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 14px;
margin-bottom: 22px;
}
.controls__group {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.btn {
font-family: "Inter", sans-serif;
font-size: 14px;
font-weight: 600;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 8px 14px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 7px;
transition: background 0.15s, border-color 0.15s, transform 0.05s;
}
.btn:hover {
background: var(--bg-alt);
border-color: var(--accent);
}
.btn:active {
transform: translateY(1px);
}
.btn--primary {
background: var(--accent);
color: #fff;
border-color: var(--accent);
}
.btn--primary:hover {
background: var(--accent-d);
border-color: var(--accent-d);
}
.btn__icon {
font-size: 11px;
}
.btn:focus-visible,
.chip:focus-visible,
.step__head:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.chip {
font-family: "Inter", sans-serif;
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
padding: 6px 13px;
cursor: pointer;
transition: all 0.15s;
}
.chip:hover {
border-color: var(--accent);
}
.chip--active {
background: var(--ink);
color: #fff;
border-color: var(--ink);
}
/* ---------- summary ---------- */
.summary {
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-bottom: 30px;
}
.stat {
background: var(--bg);
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 3px;
}
.stat__k {
font-size: 20px;
font-weight: 500;
color: var(--ink);
}
.stat__l {
font-family: "Inter", sans-serif;
font-size: 11.5px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.summary__progress {
grid-column: 1 / -1;
background: var(--bg-alt);
padding: 12px 18px;
display: flex;
align-items: center;
gap: 14px;
}
.summary__bar {
flex: 1;
height: 7px;
background: var(--line);
border-radius: 999px;
overflow: hidden;
}
.summary__bar span {
display: block;
height: 100%;
width: 0;
background: linear-gradient(90deg, var(--accent), var(--teal));
border-radius: 999px;
transition: width 0.4s ease;
}
.summary__pct {
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
min-width: 38px;
text-align: right;
}
/* ---------- timeline ---------- */
.timeline {
margin-bottom: 36px;
}
.track {
list-style: none;
margin: 0;
padding: 0;
position: relative;
}
.track::before {
content: "";
position: absolute;
left: 19px;
top: 8px;
bottom: 8px;
width: 2px;
background: var(--line);
}
.phase-head {
display: flex;
align-items: center;
gap: 10px;
margin: 26px 0 12px 0;
padding-left: 50px;
position: relative;
}
.phase-head:first-child {
margin-top: 0;
}
.phase-head__dot {
position: absolute;
left: 12px;
width: 16px;
height: 16px;
border-radius: 50%;
border: 3px solid var(--bg);
box-shadow: 0 0 0 2px currentColor;
}
.phase-head__name {
font-family: "Inter", sans-serif;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.phase-head__meta {
font-family: "JetBrains Mono", monospace;
font-size: 12px;
color: var(--muted);
}
.step {
position: relative;
padding-left: 50px;
margin-bottom: 10px;
}
.step.is-hidden {
display: none;
}
.step__dot {
position: absolute;
left: 12px;
top: 18px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--bg);
border: 2px solid var(--line-2);
z-index: 1;
transition: all 0.2s;
}
.step__dot::after {
content: "";
position: absolute;
inset: 3px;
border-radius: 50%;
background: transparent;
transition: background 0.2s;
}
.step[data-status="done"] .step__dot {
border-color: var(--ok);
}
.step[data-status="done"] .step__dot::after {
background: var(--ok);
}
.step[data-status="active"] .step__dot {
border-color: var(--accent);
box-shadow: 0 0 0 4px var(--accent-50);
animation: pulse 1.3s ease-in-out infinite;
}
.step[data-status="active"] .step__dot::after {
background: var(--accent);
}
.step[data-status="skipped"] .step__dot {
border-color: var(--warn);
border-style: dashed;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 4px var(--accent-50); }
50% { box-shadow: 0 0 0 7px rgba(26, 79, 138, 0.08); }
}
.step__card {
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--bg);
box-shadow: var(--shadow-sm);
overflow: hidden;
transition: border-color 0.2s, box-shadow 0.2s;
}
.step.is-current .step__card {
border-color: var(--accent);
box-shadow: var(--shadow-md);
}
.step__head {
width: 100%;
text-align: left;
background: none;
border: 0;
padding: 14px 16px;
cursor: pointer;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 12px;
font-family: inherit;
}
.step__no {
font-family: "JetBrains Mono", monospace;
font-size: 13px;
font-weight: 500;
color: #fff;
background: var(--accent);
width: 30px;
height: 30px;
border-radius: var(--r-sm);
display: grid;
place-items: center;
flex-shrink: 0;
}
.step[data-phase="treat"] .step__no { background: var(--p-treat); }
.step[data-phase="meas"] .step__no { background: var(--p-meas); }
.step[data-phase="ana"] .step__no { background: var(--p-ana); }
.step__titlewrap { min-width: 0; }
.step__title {
font-family: "Inter", sans-serif;
font-size: 15px;
font-weight: 600;
color: var(--ink);
margin: 0 0 2px;
}
.step__tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
}
.tag {
font-family: "JetBrains Mono", monospace;
font-size: 11px;
color: var(--ink-2);
background: var(--bg-alt);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 2px 7px;
}
.tag--instr { color: var(--teal); background: var(--teal-50); border-color: rgba(15,125,120,.25); }
.step__right {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.step__dur {
font-family: "JetBrains Mono", monospace;
font-size: 13px;
font-weight: 500;
color: var(--ink-2);
text-align: right;
}
.step__dur small {
display: block;
font-size: 10px;
color: var(--muted);
font-weight: 400;
}
.step__caret {
font-family: "Inter", sans-serif;
color: var(--muted);
transition: transform 0.2s;
font-size: 12px;
}
.step.is-open .step__caret {
transform: rotate(180deg);
}
.step__detail {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.28s ease;
}
.step.is-open .step__detail {
grid-template-rows: 1fr;
}
.step__detail-inner {
overflow: hidden;
}
.step__body {
padding: 0 16px 16px 16px;
border-top: 1px solid var(--line);
}
.step__desc {
font-size: 14.5px;
color: var(--ink-2);
margin: 14px 0 16px;
}
.params {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.params caption {
text-align: left;
font-family: "Inter", sans-serif;
font-size: 11.5px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
margin-bottom: 6px;
}
.params th,
.params td {
text-align: left;
padding: 7px 10px;
border-bottom: 1px solid var(--line);
}
.params th {
font-family: "Inter", sans-serif;
font-weight: 500;
color: var(--muted);
width: 42%;
}
.params td {
font-family: "JetBrains Mono", monospace;
color: var(--ink);
}
.params tr:last-child th,
.params tr:last-child td {
border-bottom: 0;
}
.eqn {
display: flex;
align-items: center;
gap: 12px;
background: var(--bg-alt);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 12px 16px;
margin: 14px 0 4px;
}
.eqn__body {
flex: 1;
font-family: "Source Serif 4", serif;
font-style: italic;
font-size: 16px;
color: var(--ink);
}
.eqn__body .op,
.eqn__body .num {
font-family: "JetBrains Mono", monospace;
font-style: normal;
}
.eqn__no {
font-family: "JetBrains Mono", monospace;
font-size: 13px;
color: var(--muted);
}
/* ---------- figure ---------- */
.fig {
margin: 0 0 32px;
}
.fig__phasebar {
display: flex;
height: 34px;
border-radius: var(--r-sm);
overflow: hidden;
border: 1px solid var(--line);
margin-bottom: 10px;
}
.phaseseg {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: "Inter", sans-serif;
font-size: 11px;
font-weight: 600;
color: #fff;
white-space: nowrap;
overflow: hidden;
transition: filter 0.15s;
position: relative;
}
.phaseseg:hover {
filter: brightness(1.08);
}
.phaseseg--idle {
background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(255,255,255,.22) 5px, rgba(255,255,255,.22) 10px);
}
figcaption {
font-size: 13px;
color: var(--muted);
line-height: 1.55;
}
figcaption strong {
color: var(--ink);
}
/* ---------- footer ---------- */
.foot {
border-top: 1px solid var(--line);
padding-top: 18px;
}
.foot p {
font-size: 11.5px;
color: var(--muted);
margin: 0;
}
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
background: var(--ink);
color: #fff;
font-family: "Inter", sans-serif;
font-size: 13.5px;
padding: 10px 18px;
border-radius: var(--r-sm);
box-shadow: var(--shadow-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 60;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- responsive ---------- */
@media (max-width: 640px) {
.wrap {
padding: 26px 14px 48px;
}
.summary {
grid-template-columns: repeat(2, 1fr);
}
.controls {
flex-direction: column;
align-items: stretch;
}
.step__head {
grid-template-columns: auto 1fr;
grid-template-areas:
"no title"
"right right";
}
.step__no { grid-area: no; }
.step__titlewrap { grid-area: title; }
.step__right {
grid-area: right;
justify-content: space-between;
margin-top: 4px;
}
.params {
display: block;
overflow-x: auto;
}
.phaseseg { font-size: 0; }
}(function () {
"use strict";
/* ---------- data ---------- */
var PHASES = {
prep: { name: "Preparation", color: "var(--p-prep)" },
treat: { name: "Treatment", color: "var(--p-treat)" },
meas: { name: "Measurement", color: "var(--p-meas)" },
ana: { name: "Analysis", color: "var(--p-ana)" }
};
// mins = wall-clock minutes; active = hands-on minutes
var STEPS = [
{
phase: "prep", status: "done", title: "Synthesise PNIPAM–Au nanocomposite hydrogel",
mins: 95, active: 40,
tags: ["NIPAM 1.2 M", "MBAA crosslinker", "HAuCl₄"],
instr: ["UV reactor λ=365 nm", "N₂ glovebox"],
desc: "Free-radical polymerisation of N-isopropylacrylamide in the presence of 4 nm citrate-capped gold seeds. Degas under N₂ for 20 min before initiation to suppress O₂ inhibition.",
params: [
["Monomer (NIPAM)", "1.20 mol L⁻¹"],
["Crosslinker (MBAA)", "2.0 mol % (rel. NIPAM)"],
["Photoinitiator (I-2959)", "0.50 wt %"],
["Au seed loading", "0.18 mg mL⁻¹"],
["Cure time / dose", "60 min @ 8.0 mW cm⁻²"]
]
},
{
phase: "prep", status: "done", title: "Purify & equilibrate gels in PBS",
mins: 720, active: 25,
tags: ["PBS pH 7.4", "dialysis MWCO 12 kDa"],
instr: ["Orbital shaker 60 rpm"],
desc: "Dialyse against 4 × 2 L PBS over 12 h to remove unreacted monomer and free initiator. Confirm equilibrium swelling gravimetrically before proceeding.",
params: [
["Buffer", "1× PBS, pH 7.40 ± 0.02"],
["Exchanges", "4 × (every 3 h)"],
["Equilib. swelling ratio Q", "8.6 ± 0.4"],
["Temperature", "21.0 °C"]
]
},
{
phase: "prep", status: "done", title: "Load model drug (rhodamine-B surrogate)",
mins: 180, active: 30,
tags: ["RhB 0.5 mM", "passive soak"],
instr: ["UV-Vis NanoDrop"],
desc: "Passive loading by immersion in 0.5 mM rhodamine-B (fluorescein-class surrogate for hydrophilic small-molecule API). Loading capacity quantified by absorbance depletion at 554 nm.",
params: [
["Loading conc.", "0.50 mmol L⁻¹"],
["Soak time", "3.0 h"],
["λ_abs (RhB)", "554 nm"],
["Loading efficiency", "72.4 % ± 3.1"]
]
},
{
phase: "treat", status: "active", title: "Apply NIR photothermal trigger",
mins: 30, active: 30,
tags: ["λ=808 nm", "1.5 W cm⁻²", "ΔT → 41 °C"],
instr: ["808 nm diode laser", "IR thermal camera"],
desc: "Irradiate hydrogel discs to drive plasmonic heating above the lower critical solution temperature (LCST ≈ 32 °C), collapsing the network and expelling loaded drug. Surface temperature tracked at 5 Hz.",
params: [
["Wavelength", "808 nm"],
["Power density", "1.50 W cm⁻²"],
["Pulse schedule", "5 min ON / 5 min OFF × 3"],
["Peak surface T", "41.3 °C"],
["LCST (DSC)", "31.8 °C"]
],
eqn: { body: "ΔC/C₀ = 1 − e<sup class='num'>−k t</sup>", k: "k = 0.038 min⁻¹", no: "(1)" }
},
{
phase: "treat", status: "pending", title: "Sham (no-NIR) control hold",
mins: 30, active: 5,
tags: ["dark control", "37 °C bath"],
instr: ["Thermostatic bath"],
desc: "Matched control discs held at body temperature without irradiation to isolate passive diffusion from triggered release. Identical sampling cadence.",
params: [
["Bath temperature", "37.0 °C"],
["Irradiation", "none (n = 12)"],
["Sampling cadence", "every 5 min"]
]
},
{
phase: "meas", status: "pending", title: "Sample release supernatant (kinetics)",
mins: 40, active: 35,
tags: ["aliquot 100 µL", "t = 0–35 min"],
instr: ["Microplate reader", "96-well plate"],
desc: "Withdraw 100 µL aliquots at fixed intervals, replacing with fresh PBS to maintain sink conditions. Quantify cumulative release fluorometrically.",
params: [
["Aliquot volume", "100 µL"],
["Time points", "0,2,5,10,15,20,30,35 min"],
["λ_ex / λ_em", "554 / 576 nm"],
["Sink correction", "applied (replacement)"]
]
},
{
phase: "meas", status: "pending", title: "Rheology & swelling re-measurement",
mins: 55, active: 45,
tags: ["G′ / G″", "frequency sweep"],
instr: ["Rheometer 20 mm plate", "Analytical balance"],
desc: "Oscillatory frequency sweep (0.1–10 Hz) at 1 % strain to capture network stiffening post-collapse; re-weigh discs to compute deswelling ratio.",
params: [
["Geometry", "20 mm parallel plate"],
["Strain (LVR)", "1.0 %"],
["Frequency range", "0.1 – 10 Hz"],
["G′ (1 Hz, post-NIR)", "4.7 kPa"],
["Deswelling ratio", "0.41"]
]
},
{
phase: "ana", status: "pending", title: "Fit release kinetics (Korsmeyer–Peppas)",
mins: 60, active: 30,
tags: ["n exponent", "R² ≥ 0.98"],
instr: ["Python 3.12 / SciPy"],
desc: "Non-linear least-squares fit of fractional release to the Korsmeyer–Peppas power law; the diffusional exponent n classifies transport regime (Fickian vs anomalous).",
params: [
["Model", "Mₜ/M∞ = k·tⁿ"],
["n (NIR group)", "0.62 ± 0.04"],
["k", "0.21 min⁻ⁿ"],
["R²", "0.991"],
["Software", "SciPy 1.13 curve_fit"]
],
eqn: { body: "Mₜ/M∞ = <span class='op'>k</span> · t<sup class='num'>n</sup>", k: "n = 0.62 (anomalous)", no: "(2)" }
},
{
phase: "ana", status: "pending", title: "Statistics & figure export",
mins: 45, active: 25,
tags: ["two-way ANOVA", "α = 0.05"],
instr: ["JASP 0.19", "Matplotlib"],
desc: "Two-way ANOVA (trigger × time) with Holm–Bonferroni correction; export publication figures at 600 dpi. Pre-registered primary endpoint: 30-min cumulative release.",
params: [
["Test", "2-way ANOVA + Holm"],
["α", "0.05"],
["Effect (NIR vs sham, 30 min)", "p = 0.003"],
["Cohen's d", "1.42"],
["Export DPI", "600"]
]
}
];
/* ---------- helpers ---------- */
var track = document.getElementById("track");
var toastEl = document.getElementById("toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2200);
}
function fmtDur(m) {
if (m < 60) return m + " min";
var h = Math.floor(m / 60), mm = m % 60;
return h + " h" + (mm ? " " + String(mm).padStart(2, "0") + " min" : "");
}
function esc(s) {
return String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
/* ---------- render ---------- */
function paramRows(rows) {
return rows.map(function (r) {
return "<tr><th>" + r[0] + "</th><td>" + r[1] + "</td></tr>";
}).join("");
}
function render() {
var html = "";
var lastPhase = null;
STEPS.forEach(function (s, i) {
if (s.phase !== lastPhase) {
lastPhase = s.phase;
var ph = PHASES[s.phase];
var phaseSteps = STEPS.filter(function (x) { return x.phase === s.phase; });
var phaseMins = phaseSteps.reduce(function (a, x) { return a + x.mins; }, 0);
html +=
'<li class="phase-head" data-phase="' + s.phase + '" style="color:' + ph.color + '">' +
'<span class="phase-head__dot"></span>' +
'<span class="phase-head__name">' + ph.name + '</span>' +
'<span class="phase-head__meta">' + phaseSteps.length + ' steps · ' + fmtDur(phaseMins) + '</span>' +
"</li>";
}
var no = i + 1;
var tags = s.tags.map(function (t) { return '<span class="tag">' + esc(t) + "</span>"; }).join("");
var instr = s.instr.map(function (t) {
return '<span class="tag tag--instr">⚙ ' + esc(t) + "</span>";
}).join("");
var eqn = "";
if (s.eqn) {
eqn =
'<div class="eqn"><div class="eqn__body">' + s.eqn.body +
' <span class="mono" style="font-size:12px;color:var(--muted)"> ' + esc(s.eqn.k) + "</span></div>" +
'<div class="eqn__no">' + s.eqn.no + "</div></div>";
}
html +=
'<li class="step" data-phase="' + s.phase + '" data-status="' + s.status + '" data-idx="' + i + '">' +
'<span class="step__dot"></span>' +
'<div class="step__card">' +
'<button class="step__head" aria-expanded="false" id="head-' + i + '" aria-controls="body-' + i + '">' +
'<span class="step__no">' + no + "</span>" +
'<span class="step__titlewrap">' +
'<span class="step__title">' + esc(s.title) + "</span>" +
'<span class="step__tags">' + tags + instr + "</span>" +
"</span>" +
'<span class="step__right">' +
'<span class="step__dur">' + fmtDur(s.mins) + '<small>' + fmtDur(s.active) + ' hands-on</small></span>' +
'<span class="step__caret" aria-hidden="true">▾</span>' +
"</span>" +
"</button>" +
'<div class="step__detail"><div class="step__detail-inner"><div class="step__body" id="body-' + i + '" role="region" aria-labelledby="head-' + i + '">' +
'<p class="step__desc">' + esc(s.desc) + "</p>" +
'<table class="params"><caption>Recorded parameters</caption><tbody>' + paramRows(s.params) + "</tbody></table>" +
eqn +
"</div></div></div>" +
"</div>" +
"</li>";
});
track.innerHTML = html;
bindSteps();
renderPhaseBar();
updateProgress();
}
function bindSteps() {
track.querySelectorAll(".step__head").forEach(function (btn) {
btn.addEventListener("click", function () {
var step = btn.closest(".step");
var open = step.classList.toggle("is-open");
btn.setAttribute("aria-expanded", open ? "true" : "false");
});
});
}
/* ---------- phase bar figure ---------- */
function renderPhaseBar() {
var bar = document.getElementById("phasebar");
var total = STEPS.reduce(function (a, s) { return a + s.mins; }, 0);
var byPhase = {};
STEPS.forEach(function (s) {
if (!byPhase[s.phase]) byPhase[s.phase] = { mins: 0, idle: 0 };
byPhase[s.phase].mins += s.mins;
byPhase[s.phase].idle += (s.mins - s.active);
});
var html = "";
Object.keys(PHASES).forEach(function (key) {
var d = byPhase[key];
if (!d) return;
var ph = PHASES[key];
var pct = (d.mins / total) * 100;
var idlePct = d.mins ? Math.round((d.idle / d.mins) * 100) : 0;
html +=
'<div class="phaseseg' + (idlePct > 50 ? " phaseseg--idle" : "") +
'" style="width:' + pct.toFixed(2) + "%;background:" + ph.color +
'" title="' + ph.name + " — " + fmtDur(d.mins) + " (" + pct.toFixed(0) + "% of run, " + idlePct + '% idle)">' +
(pct > 8 ? ph.name : "") + "</div>";
});
bar.innerHTML = html;
}
/* ---------- progress ---------- */
function updateProgress() {
var steps = STEPS;
var done = steps.filter(function (s) { return s.status === "done"; }).length;
var pct = Math.round((done / steps.length) * 100);
document.getElementById("progress-fill").style.width = pct + "%";
document.getElementById("progress-pct").textContent = pct + "%";
track.querySelectorAll(".step").forEach(function (el) {
var idx = +el.dataset.idx;
el.dataset.status = steps[idx].status;
el.classList.toggle("is-current", steps[idx].status === "active");
});
}
/* ---------- filtering ---------- */
var chips = document.querySelectorAll(".chip");
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
chips.forEach(function (c) {
c.classList.remove("chip--active");
c.setAttribute("aria-pressed", "false");
});
chip.classList.add("chip--active");
chip.setAttribute("aria-pressed", "true");
var phase = chip.dataset.phase;
track.querySelectorAll(".step").forEach(function (el) {
el.classList.toggle("is-hidden", phase !== "all" && el.dataset.phase !== phase);
});
track.querySelectorAll(".phase-head").forEach(function (el) {
el.style.display = (phase === "all" || el.dataset.phase === phase) ? "" : "none";
});
});
});
/* ---------- expand all ---------- */
var expandBtn = document.getElementById("expand-all");
var allOpen = false;
expandBtn.addEventListener("click", function () {
allOpen = !allOpen;
track.querySelectorAll(".step").forEach(function (el) {
el.classList.toggle("is-open", allOpen);
var head = el.querySelector(".step__head");
if (head) head.setAttribute("aria-expanded", allOpen ? "true" : "false");
});
expandBtn.textContent = allOpen ? "Collapse all" : "Expand all";
});
/* ---------- play / reset ---------- */
var playBtn = document.getElementById("play");
var playLabel = document.getElementById("play-label");
var resetBtn = document.getElementById("reset");
var playing = false;
var playIdx = 0;
var playTimer;
function setAllPending() {
STEPS.forEach(function (s) { s.status = "pending"; });
updateProgress();
}
function stopPlay() {
playing = false;
clearTimeout(playTimer);
playBtn.setAttribute("aria-pressed", "false");
playBtn.querySelector(".btn__icon").textContent = "▶";
playLabel.textContent = "Play protocol";
}
function advance() {
if (playIdx >= STEPS.length) {
STEPS[STEPS.length - 1].status = "done";
updateProgress();
stopPlay();
toast("Protocol complete — all 9 steps logged.");
return;
}
// mark previous done
if (playIdx > 0) STEPS[playIdx - 1].status = "done";
STEPS[playIdx].status = "active";
updateProgress();
var current = track.querySelector('.step[data-idx="' + playIdx + '"]');
if (current) current.scrollIntoView({ behavior: "smooth", block: "center" });
toast("Step " + (playIdx + 1) + ": " + STEPS[playIdx].title);
playIdx++;
playTimer = setTimeout(advance, 1400);
}
playBtn.addEventListener("click", function () {
if (playing) {
stopPlay();
return;
}
playing = true;
playBtn.setAttribute("aria-pressed", "true");
playBtn.querySelector(".btn__icon").textContent = "❚❚";
playLabel.textContent = "Pause";
if (playIdx >= STEPS.length) playIdx = 0;
if (playIdx === 0) setAllPending();
advance();
});
resetBtn.addEventListener("click", function () {
stopPlay();
playIdx = 0;
// restore original statuses (first 3 done, step 4 active)
STEPS.forEach(function (s, i) {
s.status = i < 3 ? "done" : (i === 3 ? "active" : "pending");
});
updateProgress();
toast("Timeline reset to recorded state.");
});
/* ---------- init ---------- */
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Experiment / Methods Timeline — Protocol P-2047</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="#timeline">Skip to timeline</a>
<main class="wrap">
<header class="masthead">
<div class="masthead__id">
<span class="badge badge--proto">Protocol P-2047</span>
<span class="badge badge--rev">Rev. 3.1</span>
<span class="badge badge--prereg">Preregistered · OSF <span class="mono">10.17605/osf.io/x9q4d</span></span>
</div>
<h1>Methods Timeline — Photothermal Hydrogel Drug-Release Assay</h1>
<p class="masthead__sub">
Ordered experimental protocol for a near-infrared triggered release study on
thermoresponsive PNIPAM–gold nanocomposite hydrogels. Phases run from sample
preparation through analysis; expand any step to inspect reagents, instruments, and
recorded parameters.
</p>
<p class="masthead__byline mono">
A, R. Okonjo · L. Mertens · P. Vásquez-Ríos — Voss Institute for Soft Matter, Lab 4B ·
contact: <span>methods@vossinstitute.example</span>
</p>
</header>
<section class="controls" aria-label="Timeline controls">
<div class="controls__group">
<button id="play" class="btn btn--primary" aria-pressed="false">
<span class="btn__icon" aria-hidden="true">▶</span><span id="play-label">Play protocol</span>
</button>
<button id="reset" class="btn">Reset</button>
<button id="expand-all" class="btn">Expand all</button>
</div>
<div class="controls__group controls__filters" role="group" aria-label="Filter by phase">
<button class="chip chip--active" data-phase="all" aria-pressed="true">All</button>
<button class="chip" data-phase="prep" aria-pressed="false">Preparation</button>
<button class="chip" data-phase="treat" aria-pressed="false">Treatment</button>
<button class="chip" data-phase="meas" aria-pressed="false">Measurement</button>
<button class="chip" data-phase="ana" aria-pressed="false">Analysis</button>
</div>
</section>
<section class="summary" aria-label="Protocol summary">
<div class="stat">
<span class="stat__k mono" id="stat-steps">9</span>
<span class="stat__l">Steps</span>
</div>
<div class="stat">
<span class="stat__k mono" id="stat-total">7 h 25 min</span>
<span class="stat__l">Total active + idle</span>
</div>
<div class="stat">
<span class="stat__k mono" id="stat-active">3 h 05 min</span>
<span class="stat__l">Hands-on time</span>
</div>
<div class="stat">
<span class="stat__k mono">n = 24</span>
<span class="stat__l">Replicates (4×6)</span>
</div>
<div class="summary__progress">
<div class="summary__bar"><span id="progress-fill"></span></div>
<span class="summary__pct mono" id="progress-pct">0%</span>
</div>
</section>
<section id="timeline" class="timeline" aria-label="Experiment timeline">
<ol class="track" id="track"><!-- steps injected by script.js --></ol>
</section>
<figure class="fig">
<div class="fig__phasebar" id="phasebar" aria-hidden="true"></div>
<figcaption>
<strong>Figure 1.</strong> Relative duration of each protocol phase across the full run
(idle incubation included). Bar widths are proportional to wall-clock minutes; hatched
segments mark unattended incubation. Illustrative values only.
</figcaption>
</figure>
<footer class="foot">
<p class="mono">P-2047 · generated for the Voss Institute electronic lab notebook (eLN) · CC BY 4.0 figure license</p>
</footer>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Experiment / Methods Timeline
A vertical protocol timeline that documents an experiment the way an electronic lab notebook would. Nine ordered steps are grouped into four phases — Preparation, Treatment, Measurement, and Analysis — each with its own accent colour, a status dot (done, active, pending), and two durations: wall-clock minutes and hands-on minutes. A summary strip totals the steps, the full run time, the hands-on time, and the replicate count, while a progress bar tracks how much of the protocol has completed.
Every step is an expandable disclosure. Opening one reveals its method narrative, the reagents and instruments tagged on that step, and a recorded-parameters table with realistic units and significant figures — monomer concentrations, cure doses, laser power densities, and so on. The phase chips filter the track to a single phase, Expand all opens every step at once, and a Play protocol control sequentially highlights each step in order while advancing the progress bar, then settles back to the recorded state on Reset.
The layout is keyboard-usable with visible focus rings and aria-pressed controls, a closing figure renders the relative duration of each phase as proportional bars, and the whole page reflows to a single column with horizontally scrolling parameter tables down to 360px.
Illustrative UI only — fictional authors, data, and figures; not real scientific results.