Files
cs249r_book/labs/lab-modality-catalog.html
2026-06-03 15:45:28 -04:00

1909 lines
56 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MLSysBook Lab Belts And Knobs Catalog</title>
<style>
:root {
color-scheme: light;
--ink: #172033;
--muted: #596579;
--line: #d8dee9;
--panel: #ffffff;
--soft: #f5f7fb;
--wash: #eef2f7;
--nav: #172033;
--blue: #006395;
--green: #087f5b;
--amber: #b85c00;
--red: #c33149;
--purple: #6254b2;
--teal: #007c89;
--radius: 8px;
--shadow: 0 1px 3px rgba(23, 32, 51, 0.08);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
color: var(--ink);
background: var(--wash);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.45;
}
a {
color: inherit;
}
button,
input,
select {
font: inherit;
}
.shell {
min-height: 100vh;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
}
.sidebar {
position: sticky;
top: 0;
height: 100vh;
overflow: auto;
padding: 18px 16px 24px;
background: var(--nav);
color: #f8fafc;
border-right: 1px solid #111827;
}
.brand {
display: grid;
grid-template-columns: 38px minmax(0, 1fr);
gap: 10px;
align-items: center;
margin-bottom: 18px;
}
.brand img {
width: 38px;
height: 38px;
border-radius: 6px;
background: #ffffff;
object-fit: contain;
}
.brand strong {
display: block;
font-size: 0.98rem;
letter-spacing: 0;
}
.brand span {
display: block;
margin-top: 2px;
color: #cbd5e1;
font-size: 0.78rem;
}
.nav-group {
margin-top: 16px;
}
.nav-title {
margin: 0 0 8px;
color: #a9b6c9;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.nav-links {
display: grid;
gap: 5px;
}
.nav-links a {
display: block;
padding: 8px 10px;
color: #edf2f7;
text-decoration: none;
border-radius: var(--radius);
font-size: 0.9rem;
}
.nav-links a:hover,
.nav-links a:focus-visible {
background: rgba(255, 255, 255, 0.1);
outline: none;
}
.sidebar-note {
margin-top: 18px;
padding: 12px;
color: #dbe6f3;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: var(--radius);
font-size: 0.84rem;
}
main {
min-width: 0;
padding: 28px;
}
.hero {
padding: 24px;
margin-bottom: 18px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.eyebrow {
margin: 0 0 8px;
color: var(--blue);
font-size: 0.78rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
}
h1,
h2,
h3,
p {
overflow-wrap: anywhere;
}
h1 {
max-width: 920px;
margin: 0;
font-size: clamp(1.8rem, 3vw, 3rem);
line-height: 1.08;
letter-spacing: 0;
}
.lead {
max-width: 980px;
margin: 12px 0 0;
color: var(--muted);
font-size: 1rem;
}
.hero-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-top: 20px;
}
.metric {
min-height: 92px;
padding: 13px;
background: var(--soft);
border: 1px solid var(--line);
border-radius: var(--radius);
}
.metric strong {
display: block;
font-size: 1.52rem;
line-height: 1.1;
}
.metric span {
display: block;
margin-top: 6px;
color: var(--muted);
font-size: 0.85rem;
}
section {
margin-bottom: 18px;
padding: 20px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
section h2 {
margin: 0 0 8px;
font-size: 1.18rem;
letter-spacing: 0;
}
.section-note {
max-width: 980px;
margin: 0 0 16px;
color: var(--muted);
font-size: 0.93rem;
}
.workflow {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.flow-step {
position: relative;
min-height: 130px;
padding: 14px;
background: #f8fafc;
border: 1px solid var(--line);
border-top: 4px solid var(--blue);
border-radius: var(--radius);
}
.flow-step:nth-child(2) {
border-top-color: var(--green);
}
.flow-step:nth-child(3) {
border-top-color: var(--amber);
}
.flow-step:nth-child(4) {
border-top-color: var(--purple);
}
.flow-step .step-index {
color: var(--muted);
font-size: 0.72rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.flow-step h3 {
margin: 7px 0 7px;
font-size: 1rem;
letter-spacing: 0;
}
.flow-step p {
margin: 0;
color: var(--muted);
font-size: 0.88rem;
}
.track-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.track-card {
min-height: 210px;
padding: 14px;
background: #ffffff;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.track-card h3 {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin: 0 0 8px;
font-size: 1rem;
letter-spacing: 0;
}
.category {
display: inline-block;
padding: 3px 7px;
color: #ffffff;
background: var(--blue);
border-radius: 999px;
font-size: 0.7rem;
font-weight: 700;
white-space: nowrap;
}
.track-card:nth-child(2) .category {
background: var(--green);
}
.track-card:nth-child(3) .category {
background: var(--amber);
}
.track-card:nth-child(4) .category {
background: var(--purple);
}
.track-card dl {
display: grid;
gap: 7px;
margin: 0;
font-size: 0.86rem;
}
.track-card dt {
color: var(--muted);
font-weight: 700;
}
.track-card dd {
margin: 1px 0 0;
}
code {
padding: 1px 5px;
color: #27364f;
background: #edf2f7;
border: 1px solid #d9e1ec;
border-radius: 5px;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 0.86em;
}
.builder-grid {
display: grid;
grid-template-columns: minmax(260px, 0.9fr) minmax(0, 1.4fr);
gap: 14px;
align-items: start;
}
.panel {
background: #ffffff;
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 14px;
}
.panel h3 {
margin: 0 0 10px;
font-size: 1rem;
letter-spacing: 0;
}
.field {
display: grid;
gap: 6px;
margin-bottom: 12px;
}
.field label {
color: var(--muted);
font-size: 0.78rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.06em;
}
select,
input[type="search"] {
width: 100%;
min-height: 40px;
padding: 8px 10px;
color: var(--ink);
background: #ffffff;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.belt-buttons {
display: grid;
gap: 8px;
}
.belt-buttons button,
.chip-button {
min-height: 40px;
padding: 8px 10px;
color: var(--ink);
background: #ffffff;
border: 1px solid var(--line);
border-radius: var(--radius);
cursor: pointer;
text-align: left;
}
.belt-buttons button[aria-pressed="true"],
.chip-button[aria-pressed="true"] {
color: #ffffff;
background: var(--blue);
border-color: var(--blue);
}
.belt-stage {
display: grid;
grid-template-columns: repeat(5, minmax(110px, 1fr));
gap: 8px;
margin: 0 0 14px;
}
.stage-pill {
min-height: 76px;
padding: 10px;
background: #f8fafc;
border: 1px solid var(--line);
border-radius: var(--radius);
font-size: 0.82rem;
}
.stage-pill strong {
display: block;
margin-bottom: 4px;
font-size: 0.9rem;
}
.belt-detail {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.detail-box {
min-height: 104px;
padding: 12px;
background: #f8fafc;
border: 1px solid var(--line);
border-radius: var(--radius);
font-size: 0.87rem;
}
.detail-box strong {
display: block;
margin-bottom: 6px;
color: var(--ink);
}
.detail-box span {
color: var(--muted);
}
.demo-layout {
display: grid;
grid-template-columns: minmax(260px, 0.95fr) minmax(0, 1.35fr);
gap: 14px;
}
.knob-stack {
display: grid;
gap: 12px;
}
.control-row {
padding: 12px;
background: #f8fafc;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.control-row label {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
color: var(--ink);
font-weight: 700;
font-size: 0.9rem;
}
.control-row output {
color: var(--blue);
font-weight: 800;
white-space: nowrap;
}
input[type="range"] {
width: 100%;
accent-color: var(--blue);
}
.segmented {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
overflow: hidden;
background: #ffffff;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.segmented button {
min-height: 40px;
padding: 7px;
color: var(--muted);
background: #ffffff;
border: 0;
border-right: 1px solid var(--line);
cursor: pointer;
}
.segmented button:last-child {
border-right: 0;
}
.segmented button[aria-pressed="true"] {
color: #ffffff;
background: var(--teal);
}
.check-list {
display: grid;
gap: 8px;
}
.check-list label {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
color: var(--ink);
font-size: 0.88rem;
font-weight: 500;
}
input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: var(--blue);
}
.evidence-board {
display: grid;
gap: 12px;
}
.constraint-row {
display: grid;
gap: 8px;
}
.constraint {
display: grid;
grid-template-columns: 112px minmax(0, 1fr) 84px;
gap: 10px;
align-items: center;
font-size: 0.86rem;
}
.bar {
position: relative;
height: 14px;
overflow: hidden;
background: #e6ebf3;
border: 1px solid #d1d9e5;
border-radius: 999px;
}
.bar span {
display: block;
height: 100%;
width: 40%;
background: var(--green);
}
.constraint.warn .bar span {
background: var(--amber);
}
.constraint.fail .bar span {
background: var(--red);
}
.status {
font-weight: 800;
text-align: right;
}
.status.pass {
color: var(--green);
}
.status.warn {
color: var(--amber);
}
.status.fail {
color: var(--red);
}
.mini-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.mini-visual {
min-height: 176px;
padding: 12px;
background: #f8fafc;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.mini-visual h3 {
margin: 0 0 10px;
font-size: 0.94rem;
}
.pareto {
position: relative;
height: 122px;
background:
linear-gradient(to right, rgba(89, 101, 121, 0.16) 1px, transparent 1px),
linear-gradient(to bottom, rgba(89, 101, 121, 0.16) 1px, transparent 1px);
background-size: 34px 30px;
border-left: 2px solid #6b7280;
border-bottom: 2px solid #6b7280;
}
.point {
position: absolute;
width: 12px;
height: 12px;
margin: -6px 0 0 -6px;
background: var(--blue);
border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(23, 32, 51, 0.2);
}
.point.bad {
background: var(--red);
}
.point.selected {
width: 16px;
height: 16px;
margin: -8px 0 0 -8px;
background: var(--green);
box-shadow: 0 0 0 3px rgba(8, 127, 91, 0.2);
}
.heatmap {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
}
.cell {
aspect-ratio: 1;
border-radius: 4px;
background: #dce5ef;
border: 1px solid #cfdae7;
}
.cell.ok {
background: #b8e0d0;
}
.cell.warn {
background: #ffd8a8;
}
.cell.fail {
background: #f3a8b6;
}
.stack {
display: grid;
gap: 7px;
}
.stack-bar {
display: grid;
grid-template-columns: 76px minmax(0, 1fr) 46px;
gap: 8px;
align-items: center;
font-size: 0.82rem;
}
.stack-track {
height: 14px;
overflow: hidden;
background: #e6ebf3;
border-radius: 999px;
}
.stack-track span {
display: block;
height: 100%;
background: var(--purple);
}
.timeline {
display: grid;
gap: 7px;
margin-top: 4px;
}
.event {
display: grid;
grid-template-columns: 70px minmax(0, 1fr);
gap: 8px;
padding-bottom: 7px;
border-bottom: 1px solid var(--line);
font-size: 0.82rem;
}
.event:last-child {
border-bottom: 0;
}
.event strong {
color: var(--teal);
}
.topology {
min-height: 120px;
}
.topology svg {
width: 100%;
height: 120px;
display: block;
}
.toolbar {
position: sticky;
top: 0;
z-index: 2;
display: grid;
grid-template-columns: minmax(220px, 1fr) 170px 170px auto;
gap: 10px;
align-items: center;
padding: 12px;
margin-bottom: 14px;
background: rgba(255, 255, 255, 0.96);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
.chip-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.chip-button {
min-height: 34px;
text-align: center;
white-space: nowrap;
}
.catalog-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.modality {
display: grid;
gap: 10px;
min-height: 280px;
padding: 14px;
background: #ffffff;
border: 1px solid var(--line);
border-radius: var(--radius);
}
.modality[hidden] {
display: none;
}
.modality-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.modality h3 {
margin: 0;
font-size: 1rem;
letter-spacing: 0;
}
.kind {
display: inline-block;
flex: 0 0 auto;
padding: 3px 7px;
color: #ffffff;
background: var(--blue);
border-radius: 999px;
font-size: 0.68rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.kind.opening {
background: var(--blue);
}
.kind.prediction {
background: var(--teal);
}
.kind.control {
background: var(--green);
}
.kind.visual {
background: var(--amber);
}
.kind.evidence {
background: var(--red);
}
.kind.decision {
background: var(--purple);
}
.modality p {
margin: 0;
color: var(--muted);
font-size: 0.88rem;
}
.modality dl {
display: grid;
gap: 7px;
margin: 0;
font-size: 0.84rem;
}
.modality dt {
color: var(--muted);
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.modality dd {
margin: 1px 0 0;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-content: start;
}
.tag {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 3px 7px;
color: #27364f;
background: #edf2f7;
border: 1px solid #d9e1ec;
border-radius: 999px;
font-size: 0.76rem;
font-weight: 700;
}
.recipe-table {
width: 100%;
border-collapse: collapse;
overflow: hidden;
border: 1px solid var(--line);
border-radius: var(--radius);
font-size: 0.88rem;
}
.recipe-table th,
.recipe-table td {
padding: 10px;
text-align: left;
vertical-align: top;
border-bottom: 1px solid var(--line);
}
.recipe-table th {
color: var(--ink);
background: #edf2f7;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.recipe-table tr:last-child td {
border-bottom: 0;
}
.empty-state {
display: none;
padding: 18px;
color: var(--muted);
background: #f8fafc;
border: 1px dashed var(--line);
border-radius: var(--radius);
text-align: center;
}
.empty-state.visible {
display: block;
}
.source-list {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.source-list a,
.source-list div {
display: block;
min-height: 92px;
padding: 12px;
color: var(--ink);
background: #f8fafc;
border: 1px solid var(--line);
border-radius: var(--radius);
text-decoration: none;
}
.source-list strong {
display: block;
margin-bottom: 6px;
font-size: 0.94rem;
}
.source-list span {
display: block;
color: var(--muted);
font-size: 0.84rem;
}
@media (max-width: 1180px) {
.workflow,
.track-grid,
.hero-grid,
.catalog-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.toolbar {
grid-template-columns: minmax(220px, 1fr) repeat(2, 150px);
}
.toolbar .result-count {
grid-column: 1 / -1;
}
.belt-stage,
.belt-detail,
.source-list {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 840px) {
.shell {
display: block;
}
.sidebar {
position: static;
height: auto;
}
main {
padding: 16px;
}
.builder-grid,
.demo-layout,
.mini-grid {
grid-template-columns: 1fr;
}
.toolbar,
.workflow,
.track-grid,
.hero-grid,
.catalog-grid,
.belt-stage,
.belt-detail,
.source-list {
grid-template-columns: 1fr;
}
.constraint {
grid-template-columns: 84px minmax(0, 1fr) 74px;
}
}
</style>
</head>
<body>
<div class="shell">
<aside class="sidebar">
<div class="brand">
<img src="assets/images/logo.png" alt="MLSysBook logo">
<div>
<strong>Lab Belts And Knobs</strong>
<span>Track-aware content catalog</span>
</div>
</div>
<nav class="nav-group" aria-label="Page sections">
<p class="nav-title">Catalog</p>
<div class="nav-links">
<a href="#system">System</a>
<a href="#tracks">Tracks</a>
<a href="#belts">Belts</a>
<a href="#knobs">Knob Demo</a>
<a href="#modalities">Modalities</a>
<a href="#recipes">Recipes</a>
<a href="#sources">Sources</a>
</div>
</nav>
<div class="sidebar-note">
Modalities render content. They do not own hardware facts, solver equations, or scenario constants.
</div>
</aside>
<main>
<header class="hero">
<p class="eyebrow">MLSysBook Labs</p>
<h1>Reusable belts and knobs for track-aware lab content</h1>
<p class="lead">
A local reference for the components used to assemble labs: fixed pedagogical flow, track-specific realization, interactive controls, evidence views, source traces, reflections, decisions, and downloadable reports.
</p>
<div class="hero-grid" aria-label="Catalog summary">
<div class="metric">
<strong>4</strong>
<span>canonical tracks: iPhone, Oura Ring, RoboTaxi, Cloud Fleet</span>
</div>
<div class="metric">
<strong>5</strong>
<span>repeatable belts for opening, prediction, trade-off, synthesis, and report</span>
</div>
<div class="metric">
<strong>18</strong>
<span>modalities across controls, plots, evidence, and decisions</span>
</div>
<div class="metric">
<strong>1</strong>
<span>source-of-truth rule: facts and equations come from MLSysIM</span>
</div>
</div>
</header>
<section id="system">
<h2>System</h2>
<p class="section-note">
Every lab combines the same three layers. The layer boundary is what keeps track changes clean instead of scattering constants through notebooks.
</p>
<div class="workflow">
<div class="flow-step">
<span class="step-index">Layer 1</span>
<h3>Pedagogy Kernel</h3>
<p>Concept, prediction, evidence, reflection, decision, and takeaway remain stable across tracks.</p>
</div>
<div class="flow-step">
<span class="step-index">Layer 2</span>
<h3>Track Realization</h3>
<p>Track profile changes narrative, defaults, hardware refs, metrics, thresholds, and stakeholder pressure.</p>
</div>
<div class="flow-step">
<span class="step-index">Layer 3</span>
<h3>Modality Stack</h3>
<p>Selectors, sliders, plots, tables, callouts, ledgers, and report components make the idea visible.</p>
</div>
<div class="flow-step">
<span class="step-index">Boundary</span>
<h3>Single Source</h3>
<p>MLSysIM owns hardware, model, infrastructure facts, and solver equations. Labs own only typed scenario variants.</p>
</div>
</div>
</section>
<section id="tracks">
<h2>Canonical Tracks</h2>
<p class="section-note">
Students should pick one track once. Later labs read that track from the Design Ledger unless an instructor allows switching.
</p>
<div class="track-grid">
<article class="track-card">
<h3>iPhone <span class="category">Mobile</span></h3>
<dl>
<div>
<dt>Hardware source</dt>
<dd><code>Hardware.Mobile.iPhone15Pro</code></dd>
</div>
<div>
<dt>Primary metrics</dt>
<dd>Battery drain, thermal headroom, on-device latency, memory</dd>
</div>
<div>
<dt>Typical narrative</dt>
<dd>Ship a responsive local model without overheating or draining the phone.</dd>
</div>
</dl>
</article>
<article class="track-card">
<h3>Oura Ring <span class="category">TinyML</span></h3>
<dl>
<div>
<dt>Hardware source</dt>
<dd><code>Hardware.Tiny.OuraRing</code></dd>
</div>
<div>
<dt>Primary metrics</dt>
<dd>SRAM and flash fit, battery life, OTA payload, sampling cadence</dd>
</div>
<div>
<dt>Typical narrative</dt>
<dd>Fit sensing and inference into a tiny wearable budget.</dd>
</div>
</dl>
</article>
<article class="track-card">
<h3>RoboTaxi <span class="category">Edge</span></h3>
<dl>
<div>
<dt>Hardware source</dt>
<dd><code>Hardware.Edge.RoboTaxi</code></dd>
</div>
<div>
<dt>Primary metrics</dt>
<dd>p99 and p999 latency, rare-event recall, power, reliability</dd>
</div>
<div>
<dt>Typical narrative</dt>
<dd>Keep perception and planning inside tight latency and safety guardrails.</dd>
</div>
</dl>
</article>
<article class="track-card">
<h3>Cloud Fleet <span class="category">Fleet</span></h3>
<dl>
<div>
<dt>Hardware source</dt>
<dd>Cloud fleet profile backed initially by <code>Hardware.Cloud.H100</code></dd>
</div>
<div>
<dt>Primary metrics</dt>
<dd>Throughput, p99 latency, cost/request, utilization, carbon</dd>
</div>
<div>
<dt>Typical narrative</dt>
<dd>Run a service under SLA, cost, capacity, and sustainability constraints.</dd>
</div>
</dl>
</article>
</div>
</section>
<section id="belts">
<h2>Belts</h2>
<p class="section-note">
Belts are repeatable lab flows. Pick the flow, then plug in the track variant and modality stack.
</p>
<div class="builder-grid">
<div class="panel">
<h3>Flow selector</h3>
<div class="belt-buttons" role="group" aria-label="Select a lab belt">
<button type="button" data-belt="opening" aria-pressed="true">Opening Belt</button>
<button type="button" data-belt="nugget" aria-pressed="false">Part / Nugget Belt</button>
<button type="button" data-belt="tradeoff" aria-pressed="false">Trade-Off Belt</button>
<button type="button" data-belt="synthesis" aria-pressed="false">Synthesis Belt</button>
<button type="button" data-belt="report" aria-pressed="false">Report Belt</button>
</div>
</div>
<div class="panel">
<h3 id="belt-title">Opening Belt</h3>
<div class="belt-stage" id="belt-stages"></div>
<div class="belt-detail">
<div class="detail-box">
<strong>Pedagogical job</strong>
<span id="belt-job"></span>
</div>
<div class="detail-box">
<strong>Core modalities</strong>
<span id="belt-modalities"></span>
</div>
<div class="detail-box">
<strong>Output</strong>
<span id="belt-output"></span>
</div>
</div>
</div>
</div>
</section>
<section id="knobs">
<h2>Knob Demo</h2>
<p class="section-note">
The same component can render different narratives and constraints after the track changes. Values below are illustrative placeholders; implementation should pull real facts from MLSysIM.
</p>
<div class="demo-layout">
<div class="panel">
<h3>Inputs</h3>
<div class="field">
<label for="trackSelect">Track</label>
<select id="trackSelect">
<option value="iphone">iPhone</option>
<option value="oura">Oura Ring</option>
<option value="robotaxi">RoboTaxi</option>
<option value="cloud">Cloud Fleet</option>
</select>
</div>
<div class="knob-stack">
<div class="control-row">
<label for="bitWidth">Primary knob <output id="bitOut">6 bit</output></label>
<input id="bitWidth" type="range" min="2" max="16" value="6" step="1">
</div>
<div class="control-row">
<label>Strategy</label>
<div class="segmented" role="group" aria-label="Strategy selector">
<button type="button" data-strategy="quantize" aria-pressed="true">Quantize</button>
<button type="button" data-strategy="prune" aria-pressed="false">Prune</button>
<button type="button" data-strategy="distill" aria-pressed="false">Distill</button>
</div>
</div>
<div class="control-row">
<label>Stack builder</label>
<div class="check-list">
<label><input type="checkbox" data-stack="calibration" checked> Calibration pass</label>
<label><input type="checkbox" data-stack="guardrail" checked> Guardrail validation</label>
<label><input type="checkbox" data-stack="rollback"> Rollback plan</label>
</div>
</div>
</div>
</div>
<div class="panel evidence-board">
<h3 id="demo-title">iPhone realization</h3>
<div class="constraint-row" id="constraints"></div>
<div class="mini-grid">
<div class="mini-visual">
<h3>Pareto Frontier</h3>
<div class="pareto" aria-label="Illustrative Pareto frontier">
<span class="point" style="left: 16%; top: 76%;"></span>
<span class="point" style="left: 33%; top: 58%;"></span>
<span class="point selected" id="selectedPoint" style="left: 52%; top: 42%;"></span>
<span class="point" style="left: 70%; top: 31%;"></span>
<span class="point bad" style="left: 86%; top: 24%;"></span>
</div>
</div>
<div class="mini-visual">
<h3>Phase Diagram</h3>
<div class="heatmap" id="heatmap" aria-label="Illustrative feasibility heatmap"></div>
</div>
<div class="mini-visual">
<h3>Budget Stack</h3>
<div class="stack" id="budgetStack"></div>
</div>
<div class="mini-visual">
<h3>Timeline / Topology</h3>
<div class="timeline" id="timeline"></div>
<div class="topology" aria-label="Illustrative topology diagram">
<svg viewBox="0 0 360 120" role="img" aria-labelledby="topologyTitle topologyDesc">
<title id="topologyTitle">Sensor to compute topology</title>
<desc id="topologyDesc">A simple path from source to device, edge, and report.</desc>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="7" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#596579"></path>
</marker>
</defs>
<line x1="66" y1="62" x2="138" y2="62" stroke="#596579" stroke-width="2" marker-end="url(#arrow)"></line>
<line x1="206" y1="62" x2="278" y2="62" stroke="#596579" stroke-width="2" marker-end="url(#arrow)"></line>
<rect x="18" y="34" width="64" height="56" rx="7" fill="#e7f5ff" stroke="#8ecae6"></rect>
<rect x="146" y="34" width="64" height="56" rx="7" fill="#e6fcf5" stroke="#63c7a3"></rect>
<rect x="274" y="34" width="64" height="56" rx="7" fill="#fff4e6" stroke="#f0ad4e"></rect>
<text x="50" y="66" text-anchor="middle" font-size="12" fill="#172033">Input</text>
<text x="178" y="66" text-anchor="middle" font-size="12" fill="#172033">Solver</text>
<text x="306" y="66" text-anchor="middle" font-size="12" fill="#172033">Report</text>
</svg>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="modalities">
<h2>Modality Catalog</h2>
<p class="section-note">
Search or filter by role. Each entry names when to use it, what the student does, and the shape of the output contract.
</p>
<div class="toolbar">
<input id="search" type="search" placeholder="Search modalities, contracts, metrics, or tracks" aria-label="Search modalities">
<select id="kindFilter" aria-label="Filter by modality kind">
<option value="all">All kinds</option>
<option value="opening">Opening</option>
<option value="prediction">Prediction</option>
<option value="control">Control</option>
<option value="visual">Visual</option>
<option value="evidence">Evidence</option>
<option value="decision">Decision</option>
</select>
<select id="trackFilter" aria-label="Filter by track">
<option value="all">All tracks</option>
<option value="iphone">iPhone</option>
<option value="oura">Oura Ring</option>
<option value="robotaxi">RoboTaxi</option>
<option value="cloud">Cloud Fleet</option>
</select>
<strong class="result-count" id="resultCount">18 items</strong>
</div>
<div class="chip-row" role="group" aria-label="Teaching goal filters">
<button class="chip-button" type="button" data-goal="all" aria-pressed="true">All goals</button>
<button class="chip-button" type="button" data-goal="threshold" aria-pressed="false">Threshold</button>
<button class="chip-button" type="button" data-goal="tradeoff" aria-pressed="false">Trade-off</button>
<button class="chip-button" type="button" data-goal="scale" aria-pressed="false">Scale</button>
<button class="chip-button" type="button" data-goal="structure" aria-pressed="false">Structure</button>
<button class="chip-button" type="button" data-goal="judgment" aria-pressed="false">Judgment</button>
</div>
<div class="catalog-grid" id="catalogGrid"></div>
<div class="empty-state" id="emptyState">No modalities match the current filters.</div>
</section>
<section id="recipes">
<h2>Recipes</h2>
<p class="section-note">
These recipes show how belts and knobs combine into a lab part or an entire compression pilot.
</p>
<table class="recipe-table">
<thead>
<tr>
<th>Lab unit</th>
<th>Pedagogical move</th>
<th>Recommended stack</th>
<th>Report evidence</th>
</tr>
</thead>
<tbody>
<tr>
<td>Opening</td>
<td>Anchor the chapter and selected track.</td>
<td>Track selector, chapter recap, scenario strip, lab map.</td>
<td>Track ID, hardware ref, scenario ID, learning objectives.</td>
</tr>
<tr>
<td>Part A</td>
<td>Make a misconception visible before reveal.</td>
<td>Multiple-choice prediction, strategy selector, constraint budget, source trace.</td>
<td>Prediction, actual outcome, failed or binding constraint.</td>
</tr>
<tr>
<td>Part B</td>
<td>Show that the answer is a frontier, not a single knob.</td>
<td>Numeric prediction, slider, Pareto frontier, phase diagram, table fallback.</td>
<td>Selected point, dominated choices, guardrail violations, reflection.</td>
</tr>
<tr>
<td>Part C</td>
<td>Build a deployable recipe and name residual risk.</td>
<td>Stack builder, budget stack, before/after comparison, decision card.</td>
<td>Final configuration, primary metric, guardrail metric, residual risk.</td>
</tr>
<tr>
<td>Synthesis</td>
<td>Defend an engineering judgment.</td>
<td>Evidence summary, big takeaways, source trace, report export.</td>
<td>Markdown report plus optional JSON snapshot.</td>
</tr>
</tbody>
</table>
</section>
<section id="sources">
<h2>Source Documents</h2>
<p class="section-note">
This page is a visual companion to the planning docs. The docs remain the planning source.
</p>
<div class="source-list">
<a href="LAB_REALIZATION_MODALITY_CATALOG.md">
<strong>Modality Catalog</strong>
<span>Reusable components, controls, plots, evidence views, and report artifacts.</span>
</a>
<a href="LAB_STRUCTURE_AND_REPORT_CONTRACT.md">
<strong>Structure Contract</strong>
<span>Required lab headers, part headers, synthesis sections, and report schema.</span>
</a>
<a href="LAB_SINGLE_SOURCE_OF_TRUTH_POLICY.md">
<strong>Source-Of-Truth Policy</strong>
<span>Ownership boundary for MLSysIM facts, solver equations, and lab variants.</span>
</a>
</div>
</section>
</main>
</div>
<script>
const belts = {
opening: {
title: "Opening Belt",
stages: ["Learning Objectives", "Chapter Recap", "Your Track", "Scenario Brief", "Lab Map"],
job: "Turn a chapter into an assignable systems scenario before students touch controls.",
modalities: "Track selector, chapter recap, scenario strip, lab map.",
output: "Track ID, hardware ref, scenario ID, objectives, part completion plan."
},
nugget: {
title: "Part / Nugget Belt",
stages: ["What You Need To Know", "Scenario Slice", "Your Prediction", "Try It", "Checkpoint"],
job: "Teach one focused concept through prediction, manipulation, evidence, and saved reasoning.",
modalities: "Prediction lock, primary knob slider, evidence panel, source trace, reflection card.",
output: "Prediction, knob settings, result snapshot, binding constraint, reflection."
},
tradeoff: {
title: "Trade-Off Belt",
stages: ["Choose Strategy", "Sweep Knob", "Inspect Frontier", "Check Guardrail", "Defend Point"],
job: "Show that deployable ML systems rarely have a single best answer.",
modalities: "Strategy selector, sliders, Pareto frontier, phase diagram, constraint budget.",
output: "Selected candidate, dominated alternatives, guardrail violations, residual risk."
},
synthesis: {
title: "Synthesis Belt",
stages: ["Evidence Summary", "Final Decision", "Big Takeaways", "Residual Risk", "Next Consequence"],
job: "Convert part-level observations into one defensible engineering judgment.",
modalities: "Ledger summary, decision card, big takeaways, source trace.",
output: "Final decision, evidence summary, takeaways, invalidating assumption."
},
report: {
title: "Report Belt",
stages: ["Lab", "Track", "Predictions", "Evidence", "Source Trace"],
job: "Produce a local submission artifact even when fields are incomplete.",
modalities: "Design Ledger, report export, incomplete-fields notice, fallback text area.",
output: "Markdown report and optional JSON snapshot generated locally."
}
};
const tracks = {
iphone: {
label: "iPhone realization",
metrics: [
["Battery", 54, "pass"],
["Thermal", 68, "warn"],
["Latency", 41, "pass"],
["Memory", 57, "pass"]
],
budget: [["Weights", 42], ["Activations", 28], ["Runtime", 18], ["Headroom", 12]],
timeline: [["T0", "Track selected from local ledger"], ["T1", "On-device latency predicted"], ["T2", "Thermal guardrail checked"]]
},
oura: {
label: "Oura Ring realization",
metrics: [
["SRAM", 81, "warn"],
["Flash", 73, "warn"],
["Battery", 48, "pass"],
["OTA", 66, "warn"]
],
budget: [["Weights", 36], ["Buffers", 31], ["Radio", 20], ["Headroom", 13]],
timeline: [["T0", "Sampling cadence selected"], ["T1", "Memory fit checked"], ["T2", "OTA payload reviewed"]]
},
robotaxi: {
label: "RoboTaxi realization",
metrics: [
["p99", 76, "warn"],
["p999", 88, "fail"],
["Power", 63, "warn"],
["Recall", 46, "pass"]
],
budget: [["Perception", 43], ["Planning", 24], ["Transfer", 19], ["Headroom", 14]],
timeline: [["T0", "Sensor workload selected"], ["T1", "Tail latency inspected"], ["T2", "Rare-event recall protected"]]
},
cloud: {
label: "Cloud Fleet realization",
metrics: [
["SLA", 61, "pass"],
["Cost", 69, "warn"],
["Utilization", 74, "warn"],
["Carbon", 52, "pass"]
],
budget: [["Compute", 48], ["Network", 17], ["Storage", 13], ["Headroom", 22]],
timeline: [["T0", "Fleet scenario loaded"], ["T1", "Replica count swept"], ["T2", "SLA and cost reported"]]
}
};
const modalities = [
{
title: "Track Selector",
kind: "opening",
use: "Start Lab 00 and every track-aware lab from one system identity.",
action: "Choose iPhone, Oura Ring, RoboTaxi, or Cloud Fleet.",
contract: "track_id, category, hardware_ref, scenario_id, primary_constraints",
goals: ["structure", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["ledger", "track", "hardware"]
},
{
title: "Chapter Recap",
kind: "opening",
use: "Bridge the book chapter into the systems scenario.",
action: "Read the key idea, systems translation, and common trap.",
contract: "ChapterRecap",
goals: ["structure"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["recap", "concept", "trap"]
},
{
title: "Scenario Strip",
kind: "opening",
use: "Keep stakeholder, workload, model, metrics, and guardrails visible.",
action: "Scan the active scenario before interacting.",
contract: "track_id, stakeholder, hardware_ref, model_ref, workload_summary, objective",
goals: ["structure", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["scenario", "stakeholder", "metrics"]
},
{
title: "Multiple-Choice Prediction Lock",
kind: "prediction",
use: "Reveal a misconception before showing computed evidence.",
action: "Pick one outcome and lock it before reveal.",
contract: "prediction_id, selected_option, correct_option, reason",
goals: ["threshold", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["prediction", "misconception", "lock"]
},
{
title: "Numeric Prediction Lock",
kind: "prediction",
use: "Make students estimate a magnitude before computing it.",
action: "Enter a unit-bearing value and compare against actual.",
contract: "prediction_id, predicted_value, unit, actual_value, error_factor",
goals: ["threshold", "scale"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["magnitude", "estimate", "unit"]
},
{
title: "Primary Knob Slider",
kind: "control",
use: "Expose a threshold, cliff, or nonlinear transition.",
action: "Move one continuous variable and watch linked outputs.",
contract: "knob_id, mlsysim_param, value, unit, watched_outputs",
goals: ["threshold", "scale", "tradeoff"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["slider", "bit width", "batch", "cadence"]
},
{
title: "Strategy Selector",
kind: "control",
use: "Choose one engineering option from a small set.",
action: "Pick a strategy with a short trade-off label.",
contract: "strategy_id, selected_strategy, available_strategies, unsupported_warnings",
goals: ["tradeoff", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["strategy", "radio", "choice"]
},
{
title: "Stack Builder",
kind: "control",
use: "Compose interventions with cumulative cost and benefit.",
action: "Select compatible options and inspect interactions.",
contract: "stack_id, selected_items, cumulative_costs, cumulative_benefits, interaction_warnings",
goals: ["tradeoff", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["stack", "checkbox", "recipe"]
},
{
title: "Constraint Budget",
kind: "visual",
use: "Show which resource is closest to violation.",
action: "Compare value, limit, headroom, and binding constraint.",
contract: "ConstraintBudget, BottleneckReport",
goals: ["threshold", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["budget", "bottleneck", "guardrail"]
},
{
title: "Pareto Frontier",
kind: "visual",
use: "Show multi-objective trade-offs with no single best choice.",
action: "Pick a point and explain dominated choices or guardrail violations.",
contract: "ParetoFrontier, selected_candidate, dominated, violations",
goals: ["tradeoff", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["frontier", "scatter", "trade-off"]
},
{
title: "Phase Diagram / Heatmap",
kind: "visual",
use: "Show how two knobs jointly determine feasibility.",
action: "Move an operating point across feasible and infeasible regimes.",
contract: "SensitivityGrid, regime_labels, current_point",
goals: ["threshold", "scale", "tradeoff"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["heatmap", "grid", "regime"]
},
{
title: "Budget Stack",
kind: "visual",
use: "Decompose a total into meaningful parts.",
action: "Compare the contribution of weights, activations, network, radio, or operations.",
contract: "BudgetStack, components, total",
goals: ["tradeoff", "scale"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["stacked bar", "memory", "energy", "cost"]
},
{
title: "Before / After Comparison",
kind: "visual",
use: "Show the effect of one intervention.",
action: "Compare baseline, candidate, delta, and new bottleneck.",
contract: "baseline, candidate, deltas, new_bottleneck",
goals: ["tradeoff", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["delta", "baseline", "candidate"]
},
{
title: "Timeline",
kind: "visual",
use: "Show behavior that unfolds over time.",
action: "Inspect events, decision points, observed state, and true state.",
contract: "TimelineEvent, observed_metric, true_metric, decision_point",
goals: ["scale", "structure"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["drift", "rollout", "battery", "checkpoint"]
},
{
title: "Topology / Pipeline Diagram",
kind: "visual",
use: "Show structure when placement or dataflow matters.",
action: "Trace nodes, stages, links, capacities, and bottlenecks.",
contract: "nodes, edges, stage_capacities, active_bottleneck",
goals: ["structure", "scale"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["pipeline", "network", "placement"]
},
{
title: "Source Trace / Math Peek",
kind: "evidence",
use: "Connect numbers to MLSysIM facts, equations, and assumptions.",
action: "Open the trace to audit source, equation, and track-specific assumption.",
contract: "equation_id, mlsysim_api, hardware_ref, scenario_assumptions",
goals: ["structure", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["source", "equation", "provenance"]
},
{
title: "Failure Boundary Callout",
kind: "evidence",
use: "Name the first hard limit crossed by a selected configuration.",
action: "Read the failed value, limit, unit, and mitigation hint.",
contract: "failed_constraint, value, limit, mitigation_hint",
goals: ["threshold", "judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["failure", "limit", "mitigation"]
},
{
title: "Reflection / Decision / Report",
kind: "decision",
use: "Turn interaction into a saved engineering judgment and local artifact.",
action: "Save diagnosis, trade-off, residual risk, final decision, and report.",
contract: "decision_id, selected_configuration, primary_metric, guardrail_metric, Markdown report",
goals: ["judgment"],
tracks: ["iphone", "oura", "robotaxi", "cloud"],
tags: ["reflection", "ledger", "report"]
}
];
const beltButtons = Array.from(document.querySelectorAll("[data-belt]"));
const beltTitle = document.getElementById("belt-title");
const beltStages = document.getElementById("belt-stages");
const beltJob = document.getElementById("belt-job");
const beltModalities = document.getElementById("belt-modalities");
const beltOutput = document.getElementById("belt-output");
function renderBelt(key) {
const belt = belts[key];
beltTitle.textContent = belt.title;
beltStages.innerHTML = belt.stages.map((stage, index) => `
<div class="stage-pill">
<strong>${index + 1}. ${stage}</strong>
<span>${index === 0 ? "Start" : index === belt.stages.length - 1 ? "Commit" : "Build"}</span>
</div>
`).join("");
beltJob.textContent = belt.job;
beltModalities.textContent = belt.modalities;
beltOutput.textContent = belt.output;
beltButtons.forEach((button) => {
button.setAttribute("aria-pressed", String(button.dataset.belt === key));
});
}
beltButtons.forEach((button) => {
button.addEventListener("click", () => renderBelt(button.dataset.belt));
});
const trackSelect = document.getElementById("trackSelect");
const bitWidth = document.getElementById("bitWidth");
const bitOut = document.getElementById("bitOut");
const demoTitle = document.getElementById("demo-title");
const constraints = document.getElementById("constraints");
const budgetStack = document.getElementById("budgetStack");
const timeline = document.getElementById("timeline");
const heatmap = document.getElementById("heatmap");
const selectedPoint = document.getElementById("selectedPoint");
const strategyButtons = Array.from(document.querySelectorAll("[data-strategy]"));
function statusLabel(status) {
if (status === "fail") return "Fail";
if (status === "warn") return "Watch";
return "Pass";
}
function renderTrackDemo() {
const track = tracks[trackSelect.value];
const width = Number(bitWidth.value);
bitOut.textContent = `${width} bit`;
demoTitle.textContent = track.label;
const compressionEffect = Math.max(0, Math.min(18, 16 - width));
constraints.innerHTML = track.metrics.map(([label, base, status]) => {
const adjusted = Math.max(8, Math.min(98, base - compressionEffect));
const derivedStatus = adjusted > 84 ? "fail" : adjusted > 66 ? "warn" : status === "fail" ? "warn" : adjusted < 58 ? "pass" : status;
return `
<div class="constraint ${derivedStatus}">
<strong>${label}</strong>
<div class="bar"><span style="width: ${adjusted}%;"></span></div>
<span class="status ${derivedStatus}">${statusLabel(derivedStatus)}</span>
</div>
`;
}).join("");
budgetStack.innerHTML = track.budget.map(([label, value]) => {
const adjusted = label === "Headroom" ? value + Math.floor(compressionEffect / 2) : Math.max(8, value - Math.floor(compressionEffect / 4));
return `
<div class="stack-bar">
<strong>${label}</strong>
<div class="stack-track"><span style="width: ${adjusted}%;"></span></div>
<span>${adjusted}%</span>
</div>
`;
}).join("");
timeline.innerHTML = track.timeline.map(([time, event]) => `
<div class="event">
<strong>${time}</strong>
<span>${event}</span>
</div>
`).join("");
selectedPoint.style.left = `${Math.max(18, Math.min(82, 20 + width * 4))}%`;
selectedPoint.style.top = `${Math.max(22, Math.min(78, 78 - width * 3))}%`;
renderHeatmap(width);
}
function renderHeatmap(width) {
const cells = [];
for (let row = 0; row < 5; row += 1) {
for (let col = 0; col < 7; col += 1) {
const score = row * 1.2 + col * 0.8 + width / 6;
const klass = score > 8.2 ? "fail" : score > 6.2 ? "warn" : "ok";
cells.push(`<span class="cell ${klass}" aria-hidden="true"></span>`);
}
}
heatmap.innerHTML = cells.join("");
}
trackSelect.addEventListener("change", renderTrackDemo);
bitWidth.addEventListener("input", renderTrackDemo);
strategyButtons.forEach((button) => {
button.addEventListener("click", () => {
strategyButtons.forEach((candidate) => {
candidate.setAttribute("aria-pressed", String(candidate === button));
});
});
});
const catalogGrid = document.getElementById("catalogGrid");
const search = document.getElementById("search");
const kindFilter = document.getElementById("kindFilter");
const trackFilter = document.getElementById("trackFilter");
const resultCount = document.getElementById("resultCount");
const emptyState = document.getElementById("emptyState");
const goalButtons = Array.from(document.querySelectorAll("[data-goal]"));
let activeGoal = "all";
function modalityText(item) {
return [
item.title,
item.kind,
item.use,
item.action,
item.contract,
item.goals.join(" "),
item.tracks.join(" "),
item.tags.join(" ")
].join(" ").toLowerCase();
}
function renderCatalog() {
const query = search.value.trim().toLowerCase();
const kind = kindFilter.value;
const track = trackFilter.value;
let shown = 0;
catalogGrid.innerHTML = modalities.map((item) => {
const matchesQuery = !query || modalityText(item).includes(query);
const matchesKind = kind === "all" || item.kind === kind;
const matchesTrack = track === "all" || item.tracks.includes(track);
const matchesGoal = activeGoal === "all" || item.goals.includes(activeGoal);
const visible = matchesQuery && matchesKind && matchesTrack && matchesGoal;
if (visible) shown += 1;
return `
<article class="modality" ${visible ? "" : "hidden"}>
<div class="modality-head">
<h3>${item.title}</h3>
<span class="kind ${item.kind}">${item.kind}</span>
</div>
<p>${item.use}</p>
<dl>
<div>
<dt>Student action</dt>
<dd>${item.action}</dd>
</div>
<div>
<dt>Output contract</dt>
<dd>${item.contract}</dd>
</div>
</dl>
<div class="tag-list">
${item.tags.map((tag) => `<span class="tag">${tag}</span>`).join("")}
</div>
</article>
`;
}).join("");
resultCount.textContent = `${shown} ${shown === 1 ? "item" : "items"}`;
emptyState.classList.toggle("visible", shown === 0);
}
[search, kindFilter, trackFilter].forEach((control) => {
control.addEventListener("input", renderCatalog);
control.addEventListener("change", renderCatalog);
});
goalButtons.forEach((button) => {
button.addEventListener("click", () => {
activeGoal = button.dataset.goal;
goalButtons.forEach((candidate) => {
candidate.setAttribute("aria-pressed", String(candidate === button));
});
renderCatalog();
});
});
renderBelt("opening");
renderTrackDemo();
renderCatalog();
</script>
</body>
</html>