Podcast — Transcript Viewer
A synced podcast transcript panel where every line carries a timestamp, clicking a line seeks the simulated player and smooth-scrolls it into view, the active line auto-highlights as playback ticks forward, and an inline search filters and marks matches across the whole conversation.
MCP
Code
:root {
--bg: #0c0c10;
--surface: #15151c;
--surface-2: #1d1d27;
--violet: #8b5cf6;
--violet-d: #7c3aed;
--cyan: #22d3ee;
--pink: #f472b6;
--ink: #f4f4f8;
--muted: #9a9aab;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--white: #fff;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
}
* {
box-sizing: border-box;
}
html {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(900px 500px at 12% -8%, rgba(139, 92, 246, 0.22), transparent 60%),
radial-gradient(760px 460px at 100% 0%, rgba(34, 211, 238, 0.14), transparent 62%),
var(--bg);
padding: 28px 18px 56px;
}
.shell {
max-width: 860px;
margin: 0 auto;
display: grid;
gap: 20px;
}
/* ---------- Player ---------- */
.player {
display: flex;
gap: 18px;
padding: 18px;
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: 0 24px 60px -30px rgba(0, 0, 0, 0.8);
}
.cover {
position: relative;
flex: 0 0 108px;
height: 108px;
border-radius: var(--r-md);
background: linear-gradient(140deg, var(--violet), var(--cyan) 130%);
display: grid;
place-items: center;
overflow: hidden;
}
.cover-glow {
position: absolute;
inset: -40% 40% 40% -40%;
background: radial-gradient(circle, rgba(244, 114, 182, 0.7), transparent 60%);
filter: blur(14px);
}
.cover-mark {
position: relative;
font-weight: 800;
font-size: 30px;
letter-spacing: 1px;
color: rgba(12, 12, 16, 0.85);
}
.player-body {
flex: 1;
min-width: 0;
}
.player-top {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.badge {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.4px;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
color: var(--muted);
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--line);
}
.badge-live {
color: var(--white);
background: linear-gradient(90deg, var(--violet-d), var(--violet));
border-color: transparent;
}
.ep-title {
margin: 0;
font-size: 21px;
font-weight: 800;
letter-spacing: -0.3px;
line-height: 1.2;
}
.ep-sub {
margin: 4px 0 0;
font-size: 13px;
color: var(--muted);
}
.controls {
display: flex;
align-items: center;
gap: 14px;
margin-top: 16px;
}
.play {
flex: 0 0 auto;
width: 48px;
height: 48px;
border-radius: 50%;
border: none;
cursor: pointer;
display: grid;
place-items: center;
color: var(--white);
background: radial-gradient(circle at 30% 25%, var(--violet), var(--violet-d));
box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.16), 0 12px 26px -8px rgba(139, 92, 246, 0.85);
transition: transform 0.12s ease, box-shadow 0.2s ease;
}
.play:hover {
transform: scale(1.06);
}
.play:active {
transform: scale(0.96);
}
.play svg {
width: 22px;
height: 22px;
fill: currentColor;
}
.play .ic-pause {
display: none;
}
.play[aria-pressed="true"] .ic-play {
display: none;
}
.play[aria-pressed="true"] .ic-pause {
display: block;
}
.scrub {
position: relative;
flex: 1;
height: 40px;
display: flex;
align-items: center;
}
.wave {
position: absolute;
inset: 0;
display: flex;
align-items: center;
gap: 3px;
padding: 0 2px;
pointer-events: none;
-webkit-mask-image: linear-gradient(90deg, #000 var(--played, 0%), rgba(0, 0, 0, 0.28) var(--played, 0%));
mask-image: linear-gradient(90deg, #000 var(--played, 0%), rgba(0, 0, 0, 0.28) var(--played, 0%));
}
.wave span {
flex: 1;
border-radius: 2px;
background: linear-gradient(180deg, var(--violet), var(--cyan));
}
.seek {
position: absolute;
inset: 0;
width: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.seek:focus-visible + .wave,
.scrub:focus-within .wave {
outline: 2px solid var(--cyan);
outline-offset: 3px;
border-radius: var(--r-sm);
}
.time {
flex: 0 0 auto;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--muted);
white-space: nowrap;
}
.time #cur {
color: var(--ink);
font-weight: 600;
}
.time-sep {
margin: 0 3px;
opacity: 0.5;
}
/* ---------- Panel ---------- */
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 18px 6px 10px;
box-shadow: 0 24px 60px -34px rgba(0, 0, 0, 0.8);
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
padding: 0 14px 12px;
}
.panel-title {
display: flex;
align-items: center;
gap: 10px;
}
.panel-title h2 {
margin: 0;
font-size: 17px;
font-weight: 700;
}
.pill {
font-size: 11px;
font-weight: 600;
color: var(--cyan);
background: rgba(34, 211, 238, 0.12);
border: 1px solid rgba(34, 211, 238, 0.28);
padding: 3px 9px;
border-radius: 999px;
white-space: nowrap;
}
.search {
position: relative;
display: flex;
align-items: center;
flex: 1;
min-width: 200px;
max-width: 320px;
}
.search-ic {
position: absolute;
left: 11px;
width: 16px;
height: 16px;
fill: var(--muted);
pointer-events: none;
}
.search input {
width: 100%;
font: inherit;
font-size: 13px;
color: var(--ink);
padding: 9px 30px 9px 34px;
background: var(--surface-2);
border: 1px solid var(--line);
border-radius: var(--r-sm);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.search input::placeholder {
color: var(--muted);
}
.search input:focus {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.22);
}
.search input[type="search"]::-webkit-search-decoration,
.search input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
}
.clear {
position: absolute;
right: 6px;
width: 22px;
height: 22px;
border-radius: 50%;
border: none;
cursor: pointer;
font-size: 16px;
line-height: 1;
color: var(--muted);
background: rgba(255, 255, 255, 0.06);
transition: color 0.15s ease, background 0.15s ease;
}
.clear:hover {
color: var(--ink);
background: rgba(255, 255, 255, 0.12);
}
.legend {
display: flex;
gap: 16px;
flex-wrap: wrap;
padding: 0 14px 8px;
border-bottom: 1px solid var(--line);
margin-bottom: 6px;
}
.who {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
font-weight: 600;
color: var(--muted);
}
.who i {
width: 9px;
height: 9px;
border-radius: 50%;
}
.who-host i {
background: var(--violet);
}
.who-guest i {
background: var(--cyan);
}
/* ---------- Lines ---------- */
.lines {
list-style: none;
margin: 0;
padding: 0;
max-height: 46vh;
min-height: 220px;
overflow-y: auto;
scroll-behavior: smooth;
scrollbar-width: thin;
scrollbar-color: var(--line-2) transparent;
}
.lines::-webkit-scrollbar {
width: 8px;
}
.lines::-webkit-scrollbar-thumb {
background: var(--line-2);
border-radius: 999px;
}
.line {
display: grid;
grid-template-columns: 58px 1fr;
gap: 12px;
align-items: start;
padding: 11px 14px;
margin: 2px 6px;
border-radius: var(--r-md);
cursor: pointer;
border: 1px solid transparent;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.1s ease;
}
.line:hover {
background: rgba(255, 255, 255, 0.04);
}
.line:focus-visible {
outline: none;
border-color: var(--violet);
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.24);
}
.line.active {
background: linear-gradient(90deg, rgba(139, 92, 246, 0.2), rgba(34, 211, 238, 0.08));
border-color: rgba(139, 92, 246, 0.45);
}
.line.active .stamp {
color: var(--white);
background: linear-gradient(90deg, var(--violet-d), var(--violet));
border-color: transparent;
}
.stamp {
font-size: 11px;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--cyan);
text-align: center;
padding: 3px 0;
border-radius: 999px;
background: rgba(34, 211, 238, 0.08);
border: 1px solid var(--line);
transition: color 0.15s ease, background 0.15s ease;
}
.body .spk {
display: block;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.3px;
text-transform: uppercase;
margin-bottom: 2px;
}
.body.host .spk {
color: var(--violet);
}
.body.guest .spk {
color: var(--cyan);
}
.body .text {
font-size: 14px;
color: var(--ink);
}
.line:not(.active) .body .text {
color: #d7d7e2;
}
mark {
background: var(--pink);
color: #1a0a12;
border-radius: 4px;
padding: 0 2px;
font-weight: 600;
}
.empty {
text-align: center;
color: var(--muted);
font-size: 14px;
padding: 30px 14px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--surface-2);
color: var(--ink);
font-size: 13px;
font-weight: 500;
padding: 10px 16px;
border-radius: 999px;
border: 1px solid var(--line-2);
box-shadow: 0 16px 40px -16px rgba(0, 0, 0, 0.9);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 20;
}
.toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
body {
padding: 18px 12px 44px;
}
.player {
flex-direction: column;
gap: 14px;
}
.cover {
flex-basis: 84px;
height: 84px;
}
.ep-title {
font-size: 18px;
}
.controls {
flex-wrap: wrap;
}
.scrub {
order: 3;
flex-basis: 100%;
}
.search {
max-width: none;
}
.line {
grid-template-columns: 50px 1fr;
gap: 9px;
}
.lines {
max-height: 52vh;
}
}(function () {
"use strict";
/* ---------- Transcript data (fictional) ---------- */
var TRANSCRIPT = [
{ t: 0, spk: "Mara Ellison", role: "host", text: "Welcome back to Signal Lines. Today we're pulling apart real-time systems — where latency stops being a number and starts being a feeling." },
{ t: 9, spk: "Dev Okafor", role: "guest", text: "Happy to be here. That framing is exactly right — users don't read your P99, they feel the stutter." },
{ t: 18, spk: "Mara Ellison", role: "host", text: "So let's ground it. When you say a system is real-time, what threshold are you actually holding yourself to?" },
{ t: 27, spk: "Dev Okafor", role: "guest", text: "For interactive audio, anything past roughly a hundred milliseconds of round-trip and the conversation starts to feel unnatural." },
{ t: 38, spk: "Dev Okafor", role: "guest", text: "People begin talking over each other, then over-correcting. Trust in the channel quietly erodes." },
{ t: 49, spk: "Mara Ellison", role: "host", text: "Trust is such an interesting word to attach to latency. Say more about that." },
{ t: 57, spk: "Dev Okafor", role: "guest", text: "If a tap doesn't respond instantly, the user assumes it failed and taps again. Now you've got duplicate writes and a support ticket." },
{ t: 69, spk: "Mara Ellison", role: "host", text: "The classic double-submit. So the fix isn't only faster networks, it's honest feedback." },
{ t: 78, spk: "Dev Okafor", role: "guest", text: "Exactly. Optimistic UI buys you enormous headroom. Show the intended state immediately, reconcile in the background." },
{ t: 90, spk: "Dev Okafor", role: "guest", text: "The edge is where this gets fun. Push compute close to the user and a hundred milliseconds becomes fifteen." },
{ t: 101, spk: "Mara Ellison", role: "host", text: "But the edge fragments your state. How do you keep everyone agreeing on the truth?" },
{ t: 110, spk: "Dev Okafor", role: "guest", text: "You stop pretending there's one truth at one instant. You lean on CRDTs and let regions converge." },
{ t: 122, spk: "Mara Ellison", role: "host", text: "Convergence over consensus. That's a whole mindset shift for teams raised on a single database." },
{ t: 132, spk: "Dev Okafor", role: "guest", text: "It is. And the hardest part isn't the algorithm, it's convincing people that eventual can still feel instant." },
{ t: 144, spk: "Mara Ellison", role: "host", text: "Let's talk failure. When the edge node dies mid-session, what does a graceful degrade look like?" },
{ t: 154, spk: "Dev Okafor", role: "guest", text: "You fail over to a warm region and you tell the user nothing dramatic. A tiny reconnecting chip, then silence." },
{ t: 166, spk: "Dev Okafor", role: "guest", text: "Loud error banners train users to distrust the whole product. Quiet recovery earns the opposite." },
{ t: 177, spk: "Mara Ellison", role: "host", text: "I love that — the best reliability work is often invisible on purpose." },
{ t: 186, spk: "Dev Okafor", role: "guest", text: "Right. Nobody praises the bridge that simply held. They just keep crossing it." },
{ t: 196, spk: "Mara Ellison", role: "host", text: "Before we wrap, one tool or habit you'd hand a team starting their first real-time feature." },
{ t: 206, spk: "Dev Okafor", role: "guest", text: "Instrument latency from day one, per user, not per average. Averages hide the people you're losing." },
{ t: 218, spk: "Mara Ellison", role: "host", text: "Measure the tail, protect the trust. That's a clean note to end on." },
{ t: 227, spk: "Dev Okafor", role: "guest", text: "Thanks for having me — this was the good kind of nerdy." },
{ t: 235, spk: "Mara Ellison", role: "host", text: "That's it for episode 142. Transcript's synced below, links in the show notes. See you next week." },
];
var DURATION = 248; // seconds
/* ---------- Elements ---------- */
var linesEl = document.getElementById("lines");
var countEl = document.getElementById("count");
var emptyEl = document.getElementById("empty");
var searchEl = document.getElementById("search");
var clearEl = document.getElementById("clear");
var playBtn = document.getElementById("playBtn");
var seekEl = document.getElementById("seek");
var waveEl = document.getElementById("wave");
var curEl = document.getElementById("cur");
var durEl = document.getElementById("dur");
var toastEl = document.getElementById("toast");
var state = { time: 0, playing: false, activeIdx: -1, query: "" };
var rowEls = [];
var timer = null;
var toastTimer = null;
/* ---------- Helpers ---------- */
function fmt(sec) {
sec = Math.max(0, Math.floor(sec));
var m = Math.floor(sec / 60);
var s = sec % 60;
return m + ":" + (s < 10 ? "0" : "") + s;
}
function esc(str) {
return str.replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function highlight(text, q) {
var safe = esc(text);
if (!q) return safe;
var re = new RegExp("(" + q.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "gi");
return safe.replace(re, "<mark>$1</mark>");
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 2000);
}
/* ---------- Waveform (decorative bars) ---------- */
function buildWave() {
var frag = document.createDocumentFragment();
for (var i = 0; i < 56; i++) {
var bar = document.createElement("span");
var h = 22 + Math.abs(Math.sin(i * 0.9) * 60) + (i % 4) * 6;
bar.style.height = Math.min(94, h) + "%";
frag.appendChild(bar);
}
waveEl.appendChild(frag);
}
/* ---------- Render transcript ---------- */
function render() {
var q = state.query.trim().toLowerCase();
linesEl.innerHTML = "";
rowEls = [];
var shown = 0;
TRANSCRIPT.forEach(function (item, idx) {
if (q && item.text.toLowerCase().indexOf(q) === -1) return;
shown++;
var li = document.createElement("li");
li.className = "line";
li.tabIndex = 0;
li.dataset.idx = idx;
li.setAttribute("role", "button");
li.setAttribute("aria-label", "Seek to " + fmt(item.t) + ", " + item.spk);
var roleCls = item.role === "host" ? "host" : "guest";
li.innerHTML =
'<span class="stamp">' + fmt(item.t) + "</span>" +
'<span class="body ' + roleCls + '">' +
'<span class="spk">' + esc(item.spk) + "</span>" +
'<span class="text">' + highlight(item.text, state.query.trim()) + "</span>" +
"</span>";
rowEls[idx] = li;
linesEl.appendChild(li);
});
emptyEl.hidden = shown !== 0;
if (q) {
countEl.textContent = shown + (shown === 1 ? " match" : " matches");
} else {
countEl.textContent = TRANSCRIPT.length + " lines";
}
paintActive();
}
/* ---------- Active line sync ---------- */
function currentIndex() {
var idx = 0;
for (var i = 0; i < TRANSCRIPT.length; i++) {
if (state.time >= TRANSCRIPT[i].t) idx = i;
else break;
}
return idx;
}
function paintActive() {
var idx = currentIndex();
rowEls.forEach(function (el) {
if (el) el.classList.remove("active");
});
if (rowEls[idx]) rowEls[idx].classList.add("active");
}
function scrollTo(idx) {
var el = rowEls[idx];
if (el && el.scrollIntoView) {
el.scrollIntoView({ behavior: "smooth", block: "center" });
}
}
/* ---------- Playback clock ---------- */
function updateTransport() {
var pct = (state.time / DURATION) * 100;
seekEl.value = pct;
waveEl.style.setProperty("--played", pct + "%");
curEl.textContent = fmt(state.time);
}
function tick() {
state.time += 0.5;
if (state.time >= DURATION) {
state.time = DURATION;
pause();
toast("End of episode");
}
updateTransport();
var idx = currentIndex();
if (idx !== state.activeIdx) {
state.activeIdx = idx;
paintActive();
if (state.playing) scrollTo(idx);
}
}
function play() {
if (state.playing) return;
if (state.time >= DURATION) state.time = 0;
state.playing = true;
playBtn.setAttribute("aria-pressed", "true");
playBtn.setAttribute("aria-label", "Pause episode");
timer = setInterval(tick, 500);
}
function pause() {
state.playing = false;
playBtn.setAttribute("aria-pressed", "false");
playBtn.setAttribute("aria-label", "Play episode");
clearInterval(timer);
timer = null;
}
function seekToTime(sec, opts) {
opts = opts || {};
state.time = Math.max(0, Math.min(DURATION, sec));
updateTransport();
var idx = currentIndex();
state.activeIdx = idx;
paintActive();
if (opts.scroll) scrollTo(idx);
}
/* ---------- Events ---------- */
playBtn.addEventListener("click", function () {
if (state.playing) {
pause();
} else {
play();
toast("Playing");
}
});
seekEl.addEventListener("input", function () {
seekToTime((seekEl.value / 100) * DURATION);
});
linesEl.addEventListener("click", function (e) {
var li = e.target.closest(".line");
if (!li) return;
activateLine(li);
});
linesEl.addEventListener("keydown", function (e) {
if (e.key !== "Enter" && e.key !== " ") return;
var li = e.target.closest(".line");
if (!li) return;
e.preventDefault();
activateLine(li);
});
function activateLine(li) {
var idx = Number(li.dataset.idx);
seekToTime(TRANSCRIPT[idx].t, { scroll: true });
toast("Seeked to " + fmt(TRANSCRIPT[idx].t));
}
function runSearch() {
state.query = searchEl.value;
clearEl.hidden = state.query.length === 0;
render();
}
searchEl.addEventListener("input", runSearch);
clearEl.addEventListener("click", function () {
searchEl.value = "";
state.query = "";
clearEl.hidden = true;
render();
searchEl.focus();
});
/* ---------- Init ---------- */
buildWave();
durEl.textContent = fmt(DURATION);
updateTransport();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Podcast — Transcript Viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell" role="main">
<section class="player" aria-label="Now playing">
<div class="cover" aria-hidden="true">
<span class="cover-glow"></span>
<span class="cover-mark">SL</span>
</div>
<div class="player-body">
<div class="player-top">
<span class="badge badge-live">EP 142</span>
<span class="badge">Signal Lines</span>
</div>
<h1 class="ep-title">Latency, Trust & the Edge of Real-Time</h1>
<p class="ep-sub">with Mara Ellison & guest Dev Okafor · 48 min</p>
<div class="controls">
<button
id="playBtn"
class="play"
type="button"
aria-pressed="false"
aria-label="Play episode"
>
<svg class="ic-play" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 5v14l11-7z" />
</svg>
<svg class="ic-pause" viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 5h4v14H6zM14 5h4v14h-4z" />
</svg>
</button>
<div class="scrub" role="group" aria-label="Scrubber">
<div class="wave" id="wave" aria-hidden="true"></div>
<input
id="seek"
class="seek"
type="range"
min="0"
max="100"
value="0"
step="0.1"
aria-label="Seek position"
/>
</div>
<div class="time">
<span id="cur">0:00</span>
<span class="time-sep">/</span>
<span id="dur">0:00</span>
</div>
</div>
</div>
</section>
<section class="panel" aria-label="Transcript">
<header class="panel-head">
<div class="panel-title">
<h2>Transcript</h2>
<span class="pill" id="count" aria-live="polite">28 lines</span>
</div>
<div class="search">
<svg class="search-ic" viewBox="0 0 24 24" aria-hidden="true">
<path
d="M10 4a6 6 0 104.47 10.03l4.25 4.25 1.41-1.41-4.25-4.25A6 6 0 0010 4zm0 2a4 4 0 110 8 4 4 0 010-8z"
/>
</svg>
<input
id="search"
type="search"
placeholder="Search the transcript…"
aria-label="Search transcript"
autocomplete="off"
/>
<button id="clear" class="clear" type="button" aria-label="Clear search" hidden>
×
</button>
</div>
</header>
<div class="legend" aria-label="Speakers">
<span class="who who-host"><i></i>Mara Ellison</span>
<span class="who who-guest"><i></i>Dev Okafor</span>
</div>
<ol class="lines" id="lines"></ol>
<p class="empty" id="empty" hidden>No lines match your search.</p>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>A dark, audio-first transcript viewer for podcast episodes. The layout pairs a glowing player header — episode art, title, speaker chips, a play/pause control and a gradient waveform scrubber — with a scrollable, timestamped transcript. Each line shows its start time as a clickable badge; tapping a line seeks the player to that moment, highlights the row, and smooth-scrolls it to center.
As the simulated clock ticks, the active line auto-advances and stays in view, mirroring how real synced transcripts follow live playback. A speaker legend colors each contributor so the conversation stays easy to scan, and hover, active, and keyboard-focus states are wired throughout for accessibility.
The search field filters the transcript in real time, wrapping matched terms in highlight marks and reporting a live result count. Clearing the query restores the full transcript, and a small toast confirms actions like seeking or copying a line — all in vanilla JavaScript with no dependencies.