:root {
  --bg: #faf7f2;
  --bg-raised: #f1ebe1;
  --ink: #2b2721;
  --ink-soft: #5c564c;
  --line: #ddd3c2;
  --accent: #a4501f;
  --accent-soft: #d9c9a8;
  --danger: #9c2b2b;
  --danger-soft: #e8cfc9;
  --mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
  --serif: Georgia, "Iowan Old Style", "Palatino Linotype", serif;
}

/* --- Dark theme (added session 234): overrides the same custom properties, applied via
   <html data-theme="dark">, toggled by the nav button and persisted in localStorage (see the
   inline pre-paint script in <head> and the toggle-wiring script before </body>). Contrast
   checked with a WCAG luminance script before shipping — every fg/bg pair here clears 4.5:1
   (ink/bg 14.08:1, ink-soft/bg 6.52:1, accent/bg 7.05:1, same trio on bg-raised 12.87/5.96/6.44:1,
   danger/bg 6.47:1, danger/bg-raised 5.92:1). --- */
html[data-theme="dark"] {
  --bg: #1c1815;
  --bg-raised: #262019;
  --ink: #ece5d8;
  --ink-soft: #a89c86;
  --line: #46392c;
  --accent: #e2914f;
  --accent-soft: #4a3a26;
  --danger: #e2827a;
  --danger-soft: #4a2420;
  color-scheme: dark;
}

/* Interactive demo widgets (.demo and everything inside — cell grids, node graphs, bar charts)
   were designed against the light palette's hardcoded state colors (e.g. .cell.range's #f1e3cf)
   and rely on inherited --ink for legible text on those light swatches. Re-pinning every custom
   property back to its light value inside .demo, rather than auditing every state color across
   every page for its own dark-mode variant, keeps every demo exactly as verified before — same
   effect as a fixed light "instrument panel" regardless of site theme. Confirmed session 234 via
   a script checking every hardcoded-color/var(--ink)-as-background CSS rule's class names never
   appear in any page's markup outside a .demo container. */
html[data-theme="dark"] .demo {
  --bg: #faf7f2;
  --bg-raised: #f1ebe1;
  --ink: #2b2721;
  --ink-soft: #5c564c;
  --line: #ddd3c2;
  --accent: #a4501f;
  --accent-soft: #d9c9a8;
  --danger: #9c2b2b;
  --danger-soft: #e8cfc9;
}

* { box-sizing: border-box; }

html { color-scheme: light; }

body { transition: background-color 0.15s ease, color 0.15s ease; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1.6;
}

.wrap {
  max-width: 720px;
  margin: 0 auto;
  padding: 2.5rem 1.5rem 5rem;
}

/* Skip link (added session 322): first focusable element on every page, invisible until a
   keyboard user tabs to it, then jumps past the header/nav straight to #main-content. Tried
   --accent/white first (already used together on .topo-node.flagged) but that pairing is only
   safe inside .demo, which pins --accent back to its light value regardless of theme — this
   link sits outside .demo, so dark mode's real --accent (#e2914f) put white text at 2.5:1.
   Inverted ink/bg instead: the site's primary text pair, already 13.9:1 light / 14.1:1 dark
   (see the dark-theme block's own comment above), so no new color combo to verify. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: 0.5rem 1rem;
  background: var(--ink);
  color: var(--bg);
  font-family: var(--mono);
  font-size: 0.85rem;
  text-decoration: none;
  border-radius: 0 0 6px 0;
}

.skip-link:focus { left: 0; }

header.site {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.5rem 1.5rem;
  border-bottom: 2px solid var(--ink);
  padding-bottom: 0.75rem;
  margin-bottom: 2rem;
}

header.site .mark {
  font-family: var(--mono);
  font-weight: 700;
  font-size: 1.4rem;
  letter-spacing: -0.02em;
  color: var(--ink);
  text-decoration: none;
}

header.site .mark::before {
  content: "▲ ";
  color: var(--accent);
}

nav.site a {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--ink-soft);
  text-decoration: none;
  margin-left: 1.25rem;
}

nav.site a:hover { color: var(--accent); }

.theme-toggle {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--ink-soft);
  background: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.15rem 0.5rem;
  margin-left: 1.25rem;
  cursor: pointer;
  line-height: 1.4;
}

.theme-toggle:hover { color: var(--accent); border-color: var(--accent); }

.theme-toggle .tt-light { display: none; }
html[data-theme="dark"] .theme-toggle .tt-dark { display: none; }
html[data-theme="dark"] .theme-toggle .tt-light { display: inline; }

h1, h2, h3 {
  font-family: var(--serif);
  line-height: 1.25;
}

h1 { font-size: 2rem; margin-top: 0; }
h2 { font-size: 1.4rem; margin-top: 2.5rem; }
h3 { font-size: 1.1rem; }

p { margin: 1rem 0; }

a { color: var(--accent); }

code, pre, .mono {
  font-family: var(--mono);
}

pre {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 1rem;
  overflow-x: auto;
  font-size: 0.85rem;
  line-height: 1.5;
}

code {
  background: var(--bg-raised);
  padding: 0.1em 0.35em;
  border-radius: 4px;
  font-size: 0.88em;
}

pre code { background: none; padding: 0; }

.pre-wrap {
  position: relative;
}

.copy-btn {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  background: var(--bg);
  color: var(--ink-soft);
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.15rem 0.5rem;
  cursor: pointer;
  line-height: 1.4;
}

.copy-btn:hover,
.copy-btn:focus {
  color: var(--ink);
  border-color: var(--ink-soft);
}

.tagline {
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 0.95rem;
  margin-top: -0.5rem;
}

.notice {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-left: 4px solid var(--accent);
  border-radius: 4px;
  padding: 0.85rem 1.1rem;
  font-size: 0.92rem;
  color: var(--ink-soft);
  margin: 1.5rem 0;
}

.filter-wrap {
  margin: 1.5rem 0;
}

.filter-box {
  width: 100%;
  font-family: var(--mono);
  font-size: 0.95rem;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--bg-raised);
  color: var(--ink);
}

.filter-box:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.filter-wrap .meta {
  margin-top: 0.4rem;
}

.category {
  font-family: var(--mono);
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-soft);
  margin: 2rem 0 0.5rem;
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--line);
}

h2 + .category {
  margin-top: 1rem;
}

.entry-list {
  list-style: none;
  padding: 0;
}

.entry-list li {
  border-bottom: 1px solid var(--line);
  padding: 1rem 0;
}

.entry-list li:first-child { padding-top: 0; }

.entry-list a.title {
  font-size: 1.1rem;
  font-weight: bold;
  text-decoration: none;
  color: var(--ink);
}

.entry-list a.title:hover { color: var(--accent); }

.entry-list .meta {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
  display: block;
  margin-bottom: 0.25rem;
}

.recent-list {
  list-style: none;
  padding: 0;
  margin: 0;
}

.recent-list li {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.5rem 1rem;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}

.recent-list li:first-child { padding-top: 0; }
.recent-list li:last-child { border-bottom: none; }

.recent-list a {
  color: var(--ink);
  font-weight: bold;
  text-decoration: none;
}

.recent-list a:hover { color: var(--accent); }

.recent-list .meta {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
  white-space: nowrap;
}

footer.site {
  margin-top: 4rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
}

footer.site a { color: var(--ink-soft); }

.crumb {
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
  margin: 0.15rem 0 0.9rem;
}
.crumb a { color: var(--ink-soft); text-decoration: underline; text-decoration-color: var(--line); }
.crumb a:hover { color: var(--accent); text-decoration-color: var(--accent); }

.journal-jump {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
  margin: 1.25rem 0 2rem;
  font-family: var(--mono);
}

.journal-jump .label {
  font-size: 0.8rem;
  color: var(--ink-soft);
  margin-right: 0.25rem;
}

.journal-jump a {
  font-size: 0.78rem;
  color: var(--ink-soft);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  padding: 0.12rem 0.45rem;
}

.journal-jump a:hover {
  color: var(--accent);
  border-color: var(--accent);
}

.journal-jump-grouped {
  display: block;
}

.journal-jump-grouped .label {
  display: block;
  margin: 0 0 0.5rem;
}

.jump-group {
  margin-bottom: 0.3rem;
}

.jump-group summary {
  cursor: pointer;
  font-size: 0.78rem;
  color: var(--ink-soft);
  padding: 0.15rem 0;
}

.jump-group summary:hover {
  color: var(--accent);
}

.jump-group[open] summary {
  color: var(--ink);
}

.jump-group .jump-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.4rem 0 0.6rem;
}

.jump-subgroup {
  margin-bottom: 0.4rem;
}

.jump-subgroup .sublabel {
  font-size: 0.78rem;
  color: var(--ink-soft);
  padding: 0.15rem 0;
}

.jump-subgroup .jump-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  margin: 0.25rem 0 0.6rem;
}

.journal-entry {
  border-bottom: 1px solid var(--line);
  padding: 1.5rem 0;
}

.journal-entry:first-of-type { padding-top: 0; }

.journal-entry .date {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--accent);
  display: block;
  margin-bottom: 0.4rem;
}

/* --- binary search demo --- */
.demo {
  background: var(--bg-raised);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 1.25rem;
  margin: 1.5rem 0;
  font-family: var(--mono);
}

.demo .controls {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-bottom: 1rem;
  align-items: center;
}

.demo input[type=text], .demo input[type=number] {
  font-family: var(--mono);
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg);
  color: var(--ink);
}

.demo input#target { width: 5rem; }
.demo input#arr { width: 16rem; }
.demo input#weightInput { width: 4rem; }

.demo select {
  font-family: var(--mono);
  padding: 0.4rem 0.6rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg);
  color: var(--ink);
}

.demo button {
  font-family: var(--mono);
  padding: 0.4rem 0.9rem;
  border: 1px solid var(--ink);
  border-radius: 4px;
  background: var(--ink);
  color: var(--bg);
  cursor: pointer;
}

.demo button:hover { background: var(--accent); border-color: var(--accent); }

.demo button:disabled { opacity: 0.4; cursor: default; }

.cells {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin: 1rem 0;
}

.cell {
  width: 2.4rem;
  height: 2.4rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg);
  font-size: 0.9rem;
  position: relative;
  transition: background 0.2s, border-color 0.2s;
}

.cell.range { background: #f1e3cf; }
.cell.mid { background: var(--accent); color: white; border-color: var(--accent); font-weight: bold; }
.cell.found { background: #4f7d3a; color: white; border-color: #4f7d3a; font-weight: bold; }
.cell.discarded { opacity: 0.3; }

/* heavy-light-decomposition.html: marks the first position of a new chain in the flattened array */
.cell.chain-start { border-left: 3px solid var(--ink); }

/* search-rotated-sorted-array.html: marks whichever half of the current range is this step's
   determined-sorted half, layered on top of .cell.range/.cell.mid. var(--ink), not a hardcoded
   hex, so it needs no .demo dark-mode reset entry. */
.cell.sorted-half { outline: 2px solid var(--ink); outline-offset: -2px; }

.demo .log {
  font-size: 0.85rem;
  color: var(--ink-soft);
  min-height: 1.4em;
  white-space: pre-wrap;
}

/* --- insertion sort demo --- */
.bars {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  height: 160px;
  margin: 1rem 0;
  padding: 0 0.25rem;
}

.bar {
  flex: 1;
  min-width: 1.6rem;
  max-width: 3rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px 4px 0 0;
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  transition: height 0.2s, background 0.2s, border-color 0.2s;
}

.bar.sorted { background: #f1e3cf; }
.bar.hole { background: var(--accent); border-color: var(--accent); }
.bar.empty { background: transparent; border-style: dashed; }

.bar-label {
  position: absolute;
  top: -1.4rem;
  font-size: 0.8rem;
  color: var(--ink-soft);
}

.bar-index {
  position: absolute;
  bottom: -1.3rem;
  font-size: 0.7rem;
  color: var(--ink-soft);
  font-family: var(--mono);
}

/* --- merge sort demo --- */
.bar.window-left { background: #f1e3cf; }
.bar.window-right { background: #e8d9c2; }
.bar.cursor { border: 2px solid var(--accent); }

/* --- quicksort demo --- */
.bar.partition { background: #f1e3cf; }
.bar.lt { background: #e8d9c2; }
.bar.pivot { background: var(--accent); border-color: var(--accent); color: white; }

/* --- sqrt decomposition demo (reuses .bar.sorted/.bar.cursor above) --- */
.bar.block-edge { border-left: 3px solid var(--ink); }

/* --- quickselect demo (reuses .bar.partition/.bar.lt/.bar.pivot/.bar.cursor above).
   .bar.discarded marks the side dropped after partitioning, never visited again — same
   opacity convention as .cell.discarded. .bar.found reuses .cell.found's exact green. */
.bar.discarded { opacity: 0.3; }
.bar.found { background: #4f7d3a; color: white; border-color: #4f7d3a; font-weight: bold; }

/* --- stack demo --- */
.stack-wrap {
  display: flex;
  flex-direction: column-reverse;
  justify-content: flex-start;
  align-items: center;
  gap: 6px;
  min-height: 240px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
}

.stack-block {
  width: 8rem;
  padding: 0.5rem 0.75rem;
  text-align: center;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  font-family: var(--mono);
  transition: background 0.2s, border-color 0.2s;
}

.stack-block.top {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}

.stack-empty {
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 0.85rem;
  align-self: center;
  margin: auto;
}

/* --- queue demo --- */
.queue-wrap {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: center;
  gap: 6px;
  min-height: 100px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
}

.queue-block {
  width: 8rem;
  padding: 0.5rem 0.75rem;
  text-align: center;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  font-family: var(--mono);
  transition: background 0.2s, border-color 0.2s;
}

.queue-block.front {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}

.queue-block.rear {
  border-color: var(--accent);
  border-width: 2px;
}

.queue-empty {
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 0.85rem;
  align-self: center;
  margin: auto;
}

/* --- dynamic array demo --- */
.arr-wrap {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 6px;
  min-height: 100px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
}

.arr-cell {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
}

.arr-box {
  width: 2.75rem;
  height: 2.75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  font-family: var(--mono);
  transition: background 0.2s, border-color 0.2s;
}

.arr-idx {
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--ink-soft);
}

.arr-cell.slot .arr-box {
  border-style: dashed;
  background: var(--bg-raised);
  color: var(--ink-soft);
}

.arr-cell.copied .arr-box {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.arr-cell.hit .arr-box {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}

/* --- circular buffer demo: tail marker on top of the shared arr-cell family --- */
.arr-cell.tail .arr-box {
  border-color: var(--accent);
  border-width: 2px;
}

/* --- BFS grid demo --- */
.bfs-grid {
  display: grid;
  gap: 3px;
  margin: 1rem 0;
}

.bfs-cell {
  aspect-ratio: 1;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--bg);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.bfs-cell:hover { border-color: var(--ink-soft); }

.bfs-cell.wall { background: var(--ink); border-color: var(--ink); }
.bfs-cell.w0 { background: #597896; border-color: #597896; }
.bfs-cell.w3 { background: #ead9bb; }
.bfs-cell.w9 { background: #c9a15b; border-color: #b58e4a; }
.bfs-cell.start { background: #4f7d3a; border-color: #4f7d3a; cursor: default; }
.bfs-cell.end { background: var(--accent); border-color: var(--accent); cursor: default; }
.bfs-cell.visited { background: #e8d9c2; }
.bfs-cell.frontier { background: #f1e3cf; }
.bfs-cell.current { border: 2px solid var(--accent); }
.bfs-cell.hit { border: 2px dotted var(--accent); }
.bfs-cell.path { background: #d9c9a8; border-color: var(--accent); }
.bfs-cell.start.path, .bfs-cell.end.path { background: inherit; }

.bfs-queue {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  min-height: 2.1rem;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.8rem;
  margin: 0.25rem 0 1rem;
}

.bfs-queue-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
}

.bfs-queue-chip.front { border-color: var(--accent); color: var(--accent); font-weight: bold; }

.bfs-queue-empty { color: var(--ink-soft); }

/* --- DFS stack-strip demo (reuses .bfs-grid/.bfs-cell for the maze itself) --- */
.dfs-stack {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  min-height: 2.1rem;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.8rem;
  margin: 0.25rem 0 1rem;
}

.dfs-stack-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
}

.dfs-stack-chip.top { border-color: var(--accent); color: var(--accent); font-weight: bold; }

.dfs-stack-empty { color: var(--ink-soft); }

/* --- linked list demo --- */
.ll-wrap {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: flex-start;
  align-items: center;
  gap: 4px;
  min-height: 100px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
}

.ll-node {
  width: 5.5rem;
  padding: 0.5rem 0.6rem;
  text-align: center;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  font-family: var(--mono);
  transition: background 0.2s, border-color 0.2s;
}

.ll-node-label {
  font-size: 0.7rem;
  color: var(--accent);
  height: 1em;
  margin-bottom: 0.15rem;
}

.ll-node.tail {
  background: var(--accent-soft);
  border-color: var(--accent);
  font-weight: bold;
}

.ll-node.tail .ll-node-label { color: var(--accent); }

.ll-node.head {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}

.ll-node.head .ll-node-label { color: white; }

/* --- Floyd's cycle detection demo (reuses .ll-wrap/.ll-node/.ll-arrow/.ll-null; .head/.tail
   above double as hare/tortoise. .found reuses .cell.found's exact green (4.85:1, already
   verified), .cycle-start reuses the same --danger/--danger-soft pair as .uf-node.contradiction) */
.ll-node.found { background: #4f7d3a; border-color: #4f7d3a; color: white; font-weight: bold; }
.ll-node.found .ll-node-label { color: white; }
.ll-node.cycle-start { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); font-weight: bold; }
.ll-node.cycle-start .ll-node-label { color: var(--danger); }

.ll-node-remove {
  display: block;
  margin: 0.35rem auto 0;
  width: 1.4rem;
  height: 1.4rem;
  line-height: 1;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--bg);
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 0.85rem;
  cursor: pointer;
}

.ll-node-remove:hover { border-color: var(--danger); color: var(--danger); }

.ll-arrow {
  font-family: var(--mono);
  color: var(--ink-soft);
  font-size: 1.1rem;
}

.ll-null {
  font-family: var(--mono);
  color: var(--ink-soft);
  font-size: 1rem;
  padding: 0.5rem 0.3rem;
}

.ll-empty {
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 0.85rem;
  align-self: center;
  margin: auto;
}

/* --- binary search tree demo --- */
.bst-wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 260px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
  overflow-x: auto;
}

.bst-canvas {
  position: relative;
}

.bst-edges {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

.bst-edge {
  stroke: var(--line);
  stroke-width: 2;
}

.bst-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.8rem;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}

.bst-node.visited { background: #f1e3cf; }

.bst-node.target {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}

.bst-node.deadend {
  border: 2px dashed var(--accent);
  color: var(--accent);
  font-weight: bold;
}

.bst-node.rotated {
  border: 2px solid var(--ink);
  background: var(--accent-soft);
}

.bst-empty {
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: 0.85rem;
  align-self: center;
  margin: auto;
}

/* --- Red-Black tree demo (reuses .bst-wrap/.bst-canvas/.bst-node) ---
   The node's fill is the tree's own red/black color data (same "color as data" use
   graph-coloring.html's swatches established), so status highlighting below uses
   outline rings instead of background/border, which the fill already owns. */
.bst-node.rb-red { background: #b3261e; border-color: #b3261e; color: #fff; }
.bst-node.rb-black { background: var(--ink); border-color: var(--ink); color: #fff; }
.bst-node.rb-touch { outline: 3px solid var(--accent); outline-offset: 3px; }
.bst-node.rb-path { outline: 2px dashed var(--ink-soft); outline-offset: 3px; }
.bst-node.rb-miss { outline: 3px dashed var(--accent); outline-offset: 3px; }

/* --- Skip List demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node above;
   layout is a level x value grid rather than a tree, but the same absolutely-positioned
   node/edge primitives apply unchanged) --- */
.bst-node.sl-head { background: var(--ink); border-color: var(--ink); color: #fff; font-size: 0.7rem; }
.bst-node.sl-touch { outline: 3px solid var(--accent); outline-offset: 3px; }
.bst-node.sl-new { background: var(--accent-soft); border-color: var(--accent); font-weight: bold; }
.bst-node.sl-miss { outline: 3px dashed var(--accent); outline-offset: 3px; }
.sl-level-label {
  position: absolute;
  transform: translateY(-50%);
  font-family: var(--mono);
  font-size: 0.7rem;
  color: var(--ink-soft);
}

/* --- Treap demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node/.visited/.target/
   .deadend/.rotated above; adds a two-line node so the randomized priority stays visible
   alongside the value — no new colors, so contrast matches whatever .bst-node state applies) --- */
.bst-node.treap-node {
  width: 2.6rem;
  height: 2.6rem;
  flex-direction: column;
  line-height: 1.15;
}
.bst-node.treap-node .prio {
  font-size: 0.55rem;
}

/* --- B-tree demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge above; nodes hold several
   keys side by side instead of one value, so .bt-node is a row of .bt-key cells rather than a
   circle) --- */
.bt-node {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  background: var(--bg);
  border-radius: 6px;
  transition: border-color 0.2s;
}
.bt-key {
  width: 2.1rem;
  height: 2.1rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line);
  margin-left: -1px;
  font-family: var(--mono);
  font-size: 0.8rem;
  background: var(--bg);
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.bt-key:first-child { margin-left: 0; border-radius: 6px 0 0 6px; }
.bt-key:last-child { border-radius: 0 6px 6px 0; }
.bt-key:only-child { border-radius: 6px; }
.bt-key.visited { background: #f1e3cf; }
.bt-key.target {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}
.bt-key.promoted {
  background: var(--accent-soft);
  border-color: var(--accent);
  font-weight: bold;
}
.bt-node.split-flash { outline: 2px dashed var(--ink-soft); outline-offset: 3px; }

/* --- B+ tree demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bt-node/.bt-key/
   .split-flash above; adds a bottom bar so the leaf row reads differently from the routing-only
   internal rows above it, a dashed edge for the leaves' own linked list, and a highlight for
   whichever leaves a range query actually touched) --- */
.bt-node.leaf-node { border-bottom: 3px solid var(--ink-soft); padding-bottom: 2px; }
.bst-edge.leaf-link { stroke: var(--ink-soft); stroke-width: 2; stroke-dasharray: 4 3; }
.bt-node.range-hit { box-shadow: 0 0 0 3px var(--accent-soft); border-radius: 8px; }

/* --- Merge Sort Tree demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bt-node/.bt-key/
   .split-flash(current node)/.range-hit(canonical node, matched keys use .bt-key.target) above
   verbatim; only new rule is the faded "outside the query range" state, same opacity-fade
   convention as .kruskal-node.interior/.dp-item.rejected/.ht-entry.drained/.topo-node.excluded) --- */
.bt-node.mst-skip { opacity: 0.4; }

/* --- generic measured-data table (first use: skip-list.html's height-vs-log2(n) figures) --- */
.stat-table {
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 0.85rem;
  margin: 1rem 0;
}
.stat-table th, .stat-table td {
  border: 1px solid var(--line);
  padding: 0.35rem 0.9rem;
  text-align: right;
}
.stat-table th {
  color: var(--ink-soft);
  font-weight: normal;
  background: var(--bg-raised);
}
/* --- .stat-table modifier for prose cells (first use: choosing-a-fuzzy-string-matcher.html's
   comparison table) — the base rule right-aligns for numeric data, this left-aligns for text --- */
.stat-table.text th, .stat-table.text td { text-align: left; }

/* --- .stat-table row-state modifiers (first use: suffix-array.html's sorted-suffix table) —
   mirrors .dp-table td.current/.match but on a whole <tr> since suffix rows vary in width --- */
.stat-table tr.sa-current td {
  background: var(--ink);
  color: white;
  font-weight: bold;
}
.stat-table tr.sa-match td {
  background: var(--accent-soft);
}

/* --- LRU cache demo (reuses .ll-wrap/.ll-node/.ll-arrow for the chain) --- */
.ll-node.lru-touched {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}

.ll-node.lru-touched .ll-node-label { color: white; }

.lru-map {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.8rem;
  margin: 0.25rem 0 1rem;
}

.lru-map-label { color: var(--ink-soft); margin-right: 0.2rem; }

.lru-map-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
}

.lru-map-chip.touched { border-color: var(--accent); color: var(--accent); font-weight: bold; }

.lru-map-empty { color: var(--ink-soft); }

/* --- hash table demo --- */
.ht-stats {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-top: 0.75rem;
}

.ht-stats.grew { color: var(--accent); font-weight: bold; }

.ht-table {
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
}

.ht-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.3rem 0.2rem;
  border-radius: 4px;
  transition: background 0.2s;
}

.ht-row.touched { background: var(--bg-raised); }

.ht-idx {
  width: 1.4rem;
  text-align: right;
  font-family: var(--mono);
  font-size: 0.8rem;
  color: var(--ink-soft);
  flex-shrink: 0;
}

.ht-chain {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  font-family: var(--mono);
  font-size: 0.8rem;
}

.ht-entry {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg);
}

.ht-entry.touched { border-color: var(--accent); color: var(--accent); font-weight: bold; }
.ht-entry.drained { opacity: 0.35; }

.ht-arrow { color: var(--ink-soft); }

/* --- topological sort demo --- */
.topo-wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 300px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
  overflow-x: auto;
}

.topo-canvas { position: relative; }

.topo-edges {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

.topo-edge { stroke: var(--line); stroke-width: 2; fill: none; }

.topo-edge.cycle-edge { stroke: var(--ink-soft); stroke-dasharray: 5 4; }

.topo-edge.flagged { stroke: var(--accent); stroke-width: 2.5; }

.topo-edge.bridge-edge { stroke: var(--danger); stroke-width: 3; stroke-dasharray: 6 4; }

/* heavy-light-decomposition.html: heavy edges (structural, not a step highlight like .flagged) */
.topo-edge.heavy { stroke: var(--ink); stroke-width: 3; }

/* maximum-independent-set-tree.html: a node whose final decision was NOT to include it, same
   faded-opacity convention as .kruskal-node.interior/.dp-item.rejected/.ht-entry.drained */
.topo-node.excluded { opacity: 0.4; }

.topo-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 2.7rem;
  height: 2.7rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.68rem;
  text-align: center;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}

.topo-node.visiting { border: 2px dashed var(--accent); color: var(--accent); }

.topo-node.done { background: #f1e3cf; }

.topo-node.current { border: 2px solid var(--ink); }

.topo-node.flagged {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}

/* 2-sat.html: a literal and its own negation forced into the same component — genuine
   contradiction, same --danger/--danger-soft pair as .uf-node.contradiction */
.topo-node.contradiction { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); font-weight: bold; }

.topo-order {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.8rem;
  margin: 0.6rem 0 0;
  min-height: 1.8rem;
}

.topo-order-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
}

.topo-order-chip.latest { border-color: var(--accent); color: var(--accent); font-weight: bold; }

.topo-order-empty { color: var(--ink-soft); }

.ht-empty { color: var(--ink-soft); }

/* --- cuckoo hashing demo (reuses .ht-table/.ht-row/.ht-idx/.ht-chain/.ht-entry/.ht-empty/.ht-stats above) --- */
.ck-tables {
  display: flex;
  gap: 1.25rem;
  flex-wrap: wrap;
}

.ck-tables .ht-table { flex: 1 1 220px; margin: 0.5rem 0; }

.ck-table-label {
  font-family: var(--mono);
  font-size: 0.75rem;
  color: var(--ink-soft);
  margin-bottom: 0.35rem;
}

/* --- union-find demo --- */
.uf-wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 300px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
  overflow-x: auto;
}

.uf-canvas { position: relative; }

.uf-edges {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

.uf-edge { stroke: var(--line); stroke-width: 2; }

.uf-edge.onpath { stroke: var(--accent); }

.uf-edge.new { stroke: var(--accent); stroke-width: 3; }

.uf-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.85rem;
  cursor: pointer;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}

.uf-node:hover { border-color: var(--ink-soft); }

.uf-node.root { background: var(--accent-soft); border-color: var(--ink); font-weight: bold; }

.uf-node.onpath { background: #f1e3cf; }

.uf-node.found-root { background: var(--accent); border-color: var(--accent); color: white; font-weight: bold; }

.uf-node.selected-a { border: 2px dashed var(--accent); }

.uf-node.selected-b { border: 2px solid var(--accent); }

.uf-sets {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.8rem;
  margin: 0.6rem 0 0;
  min-height: 1.8rem;
}

.uf-set-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
}

.uf-set-empty { color: var(--ink-soft); }

.uf-node.contradiction { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); font-weight: bold; }

.uf-edge-label {
  font-family: var(--mono);
  font-size: 0.7rem;
  fill: var(--ink-soft);
  paint-order: stroke;
  stroke: var(--bg);
  stroke-width: 3px;
  text-anchor: middle;
}

/* --- persistent union-find demo: version strip --- */
.puf-versions {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  margin: 0 0 0.6rem;
}

.puf-version-chip {
  font-family: var(--mono);
  font-size: 0.8rem;
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
  color: var(--ink);
  cursor: pointer;
}

.puf-version-chip:hover { border-color: var(--ink-soft); }

.puf-version-chip.viewing { border-color: var(--accent); color: var(--accent); font-weight: bold; }

/* --- kruskal's algorithm demo --- */
.kruskal-wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 300px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
  overflow-x: auto;
}

.kruskal-canvas { position: relative; }

.kruskal-edges {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

.kruskal-edge { stroke: var(--line); stroke-width: 2; fill: none; }

.kruskal-edge.rejected { stroke: var(--ink-soft); stroke-dasharray: 5 4; opacity: 0.6; }

.kruskal-edge.current { stroke: var(--ink); stroke-width: 2.5; }

.kruskal-edge.accepted { stroke: var(--accent); stroke-width: 3; }

.kruskal-edge-label {
  font-family: var(--mono);
  font-size: 0.7rem;
  fill: var(--ink-soft);
}

.kruskal-edge-label.current, .kruskal-edge-label.accepted { fill: var(--ink); font-weight: bold; }

/* --- second-best spanning tree demo (reuses everything above; two new modifiers for the
   tree-path/swap highlight, no new colors) --- */
.kruskal-edge.path { stroke-width: 5; }
.kruskal-edge.danger { stroke: var(--danger); stroke-width: 4; }
.kruskal-edge-label.danger { fill: var(--danger); font-weight: bold; }

.kruskal-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.72rem;
  text-align: center;
  line-height: 1;
  padding: 0.2rem;
}

.kruskal-node.endpoint { border: 2px solid var(--ink); }

/* --- Graham Scan demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-node/.kruskal-edges/
   .kruskal-edge(.current/.accepted/.rejected)/.kruskal-edgelist/.kruskal-edge-chip verbatim;
   one new modifier for "currently on the hull stack," same color pair as .uf-node.root) --- */
.kruskal-node.hull { background: var(--accent-soft); border-color: var(--accent); font-weight: bold; }

/* --- Quickhull demo: one new modifier for a point proven interior (never reconsidered),
   same faded-opacity convention as .dp-item.rejected/.ht-entry.drained --- */
.kruskal-node.interior { opacity: 0.4; }

/* --- Chan's Algorithm demo: group-color modifiers reuse the exact hex values already WCAG-checked
   against white text as .gc-node.c0-.c3/.ch-c0-.c3 (see the session-119/161 contrast sweep notes),
   not re-verified here since the contrast math only depends on the color pair, not which page uses
   it. .confirmed is a new border-only modifier (no background of its own) so it layers over any
   group color instead of fighting it the way .hull's accent-soft background would. */
.kruskal-node.cg0 { background: #597896; color: #fff; }
.kruskal-node.cg1 { background: #7d5a8a; color: #fff; }
.kruskal-node.cg2 { background: #2a7d7d; color: #fff; }
.kruskal-node.cg3 { background: #7a5a3f; color: #fff; }
.kruskal-node.confirmed { border: 3px solid var(--accent); font-weight: bold; }

/* --- KD-Tree demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-node/.kruskal-edges/.kruskal-edge/
   .hull(best-so-far)/.interior(pruned, ruled out)/.endpoint(currently visiting) above unchanged;
   three additions: a solid-accent "final answer" modifier using the same white-on-accent pair
   already contrast-checked at 5.6:1 as .ll-node.head/.dp-item.taken and every other white-on-accent
   case swept in sessions 119/161/182; a square "query point" marker so it reads as "not part of the
   dataset" by shape rather than color alone; and a dashed search-radius circle around the query
   point, shrinking as a closer candidate is found. */
.kruskal-node.kd-best { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: bold; }
.kruskal-node.kd-query { border-radius: 4px; border: 2px solid var(--ink); font-weight: bold; }
.kd-radius { fill: none; stroke: var(--accent); stroke-width: 1.5; stroke-dasharray: 4 3; opacity: 0.75; }

.kruskal-edgelist {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.78rem;
  margin: 0.6rem 0 0;
  min-height: 1.8rem;
}

.kruskal-edge-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
  color: var(--ink-soft);
}

.kruskal-edge-chip.current { border-color: var(--ink); color: var(--ink); font-weight: bold; }

.kruskal-edge-chip.accepted { border-color: var(--accent); color: var(--accent); font-weight: bold; }

.kruskal-edge-chip.rejected { text-decoration: line-through; opacity: 0.6; }

.kruskal-stats {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-top: 0.5rem;
}

.kruskal-stats strong { color: var(--ink); }

/* --- prim's algorithm demo --- */
.prim-wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 300px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
  overflow-x: auto;
}

.prim-canvas { position: relative; }

.prim-edges {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

.prim-edge { stroke: var(--line); stroke-width: 2; fill: none; }

.prim-edge.candidate { stroke: var(--ink-soft); stroke-dasharray: 5 4; }

.prim-edge.current { stroke: var(--ink); stroke-width: 2.5; }

.prim-edge.tree { stroke: var(--accent); stroke-width: 3; }

.prim-edge-label {
  font-family: var(--mono);
  font-size: 0.7rem;
  fill: var(--ink-soft);
}

.prim-edge-label.current, .prim-edge-label.tree { fill: var(--ink); font-weight: bold; }

.prim-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.72rem;
  text-align: center;
  line-height: 1;
  padding: 0.2rem;
}

.prim-node.in-tree { background: var(--accent); border-color: var(--accent); color: white; font-weight: bold; }

.prim-node.current { border: 2px solid var(--ink); }

.prim-pq {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.78rem;
  margin: 0.6rem 0 0;
  min-height: 1.8rem;
}

.prim-pq-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
  color: var(--ink-soft);
}

.prim-pq-chip.front { border-color: var(--ink); color: var(--ink); font-weight: bold; }

.prim-pq-empty { color: var(--ink-soft); font-family: var(--mono); font-size: 0.78rem; }

.prim-stats {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-top: 0.5rem;
}

.prim-stats strong { color: var(--ink); }

/* --- bellman-ford demo --- */
.bf-wrap {
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 320px;
  border: 1px dashed var(--line);
  border-radius: 8px;
  padding: 0.75rem;
  margin: 1rem 0;
  overflow-x: auto;
}

.bf-canvas { position: relative; }

.bf-edges {
  position: absolute;
  top: 0;
  left: 0;
  overflow: visible;
}

.bf-edge { stroke: var(--line); stroke-width: 2; fill: none; }

.bf-edge.negative { stroke: var(--ink-soft); stroke-dasharray: 5 4; }

.bf-edge.cycle-edge { stroke: var(--accent); stroke-dasharray: 5 4; }

.bf-edge.current { stroke: var(--ink); stroke-width: 2.5; }

.bf-edge.relaxed { stroke: var(--accent); stroke-width: 3; }

/* --- Suurballe's Algorithm demo (reuses .bf-wrap/.bf-canvas/.bf-node(.current)/.bf-edge(.current)/
   .bf-edge-label/.bf-dist(-chip)/.kruskal-canvas/.kruskal-node/.kruskal-edge(.accepted/.rejected)
   verbatim — each SVG line and node div carries both a bf-* and a kruskal-* class so either
   family's modifier classes apply. New: .reversed marks one of P1's own edges flipped to a
   zero-cost residual edge; .path-a/.path-b give the two final disjoint paths distinct, permanent
   colors reusing --ink/--accent, no new palette entries. --- */
.bf-edge.reversed { stroke: var(--ink-soft); stroke-dasharray: 5 4; opacity: 0.75; }
.bf-edge.path-a { stroke: var(--ink); stroke-width: 4; }
.bf-edge.path-b { stroke: var(--accent); stroke-width: 4; }

.bf-edge-label {
  font-family: var(--mono);
  font-size: 0.7rem;
  fill: var(--ink-soft);
}

.bf-edge-label.current { fill: var(--ink); font-weight: bold; }

.bf-edge-label.relaxed { fill: var(--accent); font-weight: bold; }

.bf-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 2.7rem;
  height: 2.7rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.68rem;
  text-align: center;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}

.bf-node.current { border: 2px solid var(--ink); }

.bf-node.updated { background: var(--accent-soft); border-color: var(--accent); }

.bf-node.cycle {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
  font-weight: bold;
}

.bf-dist {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.78rem;
  margin: 0.6rem 0 0;
  min-height: 1.8rem;
}

.bf-dist-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
  color: var(--ink-soft);
}

.bf-dist-chip.updated { border-color: var(--accent); color: var(--accent); font-weight: bold; }

.bf-dist-chip.cycle { border-color: var(--accent); color: white; background: var(--accent); font-weight: bold; }

.bf-stats {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-top: 0.5rem;
}

.bf-stats strong { color: var(--ink); }

/* --- Floyd-Warshall demo (reuses .bf-wrap/.bf-canvas/.bf-node/.bf-edge/.bf-stats above) --- */
.fw-matrix-wrap {
  overflow-x: auto;
  margin: 0.7rem 0 0;
}

.fw-matrix {
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 0.72rem;
  width: 100%;
}

.fw-matrix th, .fw-matrix td {
  border: 1px solid var(--line);
  padding: 0.3rem 0.45rem;
  text-align: center;
  transition: background 0.2s, color 0.2s;
}

.fw-matrix thead th {
  color: var(--ink-soft);
  font-weight: normal;
  background: var(--bg-raised);
}

.fw-matrix tbody th {
  color: var(--ink-soft);
  font-weight: normal;
  background: var(--bg-raised);
  text-align: left;
}

.fw-matrix th.through-col, .fw-matrix td.through-col {
  background: var(--accent-soft);
}

.fw-matrix tr.active-row td, .fw-matrix th.active-row {
  background: #f1e3cf;
}

.fw-matrix td.changed {
  background: var(--accent);
  color: white;
  font-weight: bold;
}

.fw-matrix td.self {
  color: var(--ink-soft);
}

.fw-matrix td.self.neg {
  background: var(--accent);
  color: white;
  font-weight: bold;
}

/* --- longest common subsequence demo --- */
.dp-wrap {
  overflow-x: auto;
  margin: 1rem 0 0;
}

.dp-table {
  border-collapse: collapse;
  font-family: var(--mono);
  font-size: 0.78rem;
}

.dp-table th, .dp-table td {
  border: 1px solid var(--line);
  width: 1.9rem;
  height: 1.9rem;
  text-align: center;
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}

.dp-table thead th, .dp-table tbody th {
  color: var(--ink-soft);
  font-weight: normal;
  background: var(--bg-raised);
}

.dp-table th.hcol, .dp-table td.hcol {
  background: var(--accent-soft);
}

.dp-table td.current {
  background: var(--ink);
  color: white;
  font-weight: bold;
}

.dp-table td.match {
  background: var(--accent-soft);
}

.dp-table td.empty {
  color: var(--ink-soft);
}

.dp-table td.hit {
  border: 2px dotted var(--accent);
}

.dp-table td.path {
  border: 2px dashed var(--accent);
}

.dp-table td.path.taken {
  background: var(--accent);
  color: white;
  font-weight: bold;
  border-style: solid;
}

.dp-strings {
  font-family: var(--mono);
  font-size: 0.85rem;
  margin: 0.9rem 0 0;
}

.dp-string-row {
  display: flex;
  align-items: center;
  gap: 3px;
  margin: 0.3rem 0;
}

.dp-string-label {
  color: var(--ink-soft);
  width: 1.2rem;
  flex-shrink: 0;
}

.dp-char {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
}

.dp-char.matched {
  background: var(--accent);
  color: white;
  font-weight: bold;
  border-color: var(--accent);
}

.dp-result {
  margin-top: 0.5rem;
  color: var(--ink-soft);
}

.dp-result strong { color: var(--accent); }

.dp-stats {
  font-size: 0.85rem;
  color: var(--ink-soft);
  margin-top: 0.6rem;
}

.dp-stats strong { color: var(--ink); }

/* --- edit distance demo (extends .dp-table above with three-way edit ops) --- */
.dp-table td.path.sub {
  background: var(--accent-soft);
  border: 2px solid var(--accent);
  font-weight: bold;
}

.dp-table td.path.del {
  border: 2px dashed var(--ink-soft);
}

.dp-table td.path.ins {
  border: 2px dotted var(--ink-soft);
}

.dp-align {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 3px;
  margin: 0.9rem 0 0;
}

.dp-align-col {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.dp-align-chip {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.7rem;
  height: 1.5rem;
  font-family: var(--mono);
  font-size: 0.85rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
}

.dp-align-chip.gap {
  color: var(--ink-soft);
  border-style: dashed;
}

.dp-align-col.sub .dp-align-chip {
  background: var(--accent-soft);
  border-color: var(--accent);
  font-weight: bold;
}

.dp-align-col.del .dp-align-chip.top {
  border-color: var(--ink-soft);
  border-style: dashed;
}

.dp-align-col.ins .dp-align-chip.bot {
  background: var(--accent);
  color: white;
  font-weight: bold;
  border-color: var(--accent);
}

/* --- Damerau-Levenshtein demo (extends .dp-table/.dp-align above with a fourth "trans"
   edit op, styled with --danger/--danger-soft to stand apart from sub/del/ins's accent/
   ink-soft treatment above — the whole point of this page is that transposition is a
   distinct fourth option, not a variant of the other three). --- */
.dp-table td.path.trans {
  background: var(--danger-soft);
  border: 2px solid var(--danger);
  font-weight: bold;
}

.dp-align-col.trans .dp-align-chip {
  background: var(--danger-soft);
  border-color: var(--danger);
  font-weight: bold;
}

/* --- banded edit distance demo (extends .dp-table above). .outband marks a cell the
   algorithm provably never needs to visit — distinct from .empty, which means "not yet
   computed but will be": .outband cells are excluded by construction, not by timing. */
.dp-table td.outband {
  color: var(--ink-soft);
  background: repeating-linear-gradient(45deg, var(--bg-raised), var(--bg-raised) 4px, var(--bg) 4px, var(--bg) 8px);
}

/* --- 0/1 knapsack demo (reuses .dp-table/.dp-result above) --- */
.dp-items {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin: 0.9rem 0 0;
}

.dp-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.15rem;
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
  min-width: 4rem;
  font-family: var(--mono);
  transition: background 0.2s, color 0.2s, border-color 0.2s;
}

.dp-item .name {
  font-weight: bold;
  font-size: 0.85rem;
}

.dp-item .wv {
  color: var(--ink-soft);
  font-size: 0.72rem;
}

.dp-item.taken {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}

.dp-item.taken .wv {
  color: rgba(255, 255, 255, 0.85);
}

.dp-item.partial {
  background: repeating-linear-gradient(45deg, var(--accent-soft), var(--accent-soft) 6px, var(--bg-raised) 6px, var(--bg-raised) 12px);
  border-color: var(--accent);
}

/* --- trie demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge above) --- */
.bst-node.trie-root {
  border-style: dashed;
  color: var(--ink-soft);
}

.bst-node.created {
  border: 2px solid var(--accent);
}

.bst-node.wordend::after {
  content: "";
  position: absolute;
  bottom: -4px;
  right: -4px;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--accent);
  border: 1px solid var(--bg);
}

.bst-node.target.wordend::after,
.bst-node.deadend.wordend::after {
  background: white;
}

.bst-node.wordhit {
  background: var(--accent-soft);
  border-color: var(--accent);
}

.trie-matches {
  font-family: var(--mono);
  font-size: 0.82rem;
  color: var(--ink-soft);
  margin-top: 0.5rem;
}

.trie-matches strong { color: var(--ink); }

/* --- Ternary Search Tree demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node/
   .trie-root/.created/.wordend/.wordhit/.trie-matches above — same node vocabulary as the trie
   demo, since a TST node is still "one character, maybe end of a word." Only new thing: a mid
   link (go deeper, next character) needs to read differently from a left/right link (same
   character position, different word) at a glance. */
.bst-edge.tst-mid { stroke: var(--ink-soft); stroke-width: 3; }
.bst-edge.tst-side { stroke-dasharray: 3 3; }

/* --- Radix Tree demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node/.trie-root/
   .created/.wordend/.wordhit/.visited/.target/.deadend/.trie-matches above — same node vocabulary
   as the trie demo, since a radix node is still "maybe end of a word," just without a single
   character of its own. .rdx-edge-label is the compressed substring each edge represents, drawn
   at the edge's midpoint the same way .hc-edge-label/.kruskal-edge-label label their own edges. */
.rdx-edge-label { font-family: var(--mono); font-size: 0.68rem; fill: var(--ink-soft); text-anchor: middle; }

/* --- Rope demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node/.created/.target/
   .visited/.trie-matches above). A rope node isn't "one character" like a trie/TST node, so the
   fixed-size circle doesn't fit: a leaf holds a short text chunk (rectangle, auto width) and an
   internal node holds its cached weight (still circular, smaller text, muted color like a
   Huffman internal node). */
.bst-node.rope-leaf {
  border-radius: 6px;
  width: auto;
  min-width: 2.4rem;
  height: 1.8rem;
  padding: 0 0.4rem;
  white-space: pre;
  font-size: 0.72rem;
}
.bst-node.rope-internal {
  font-size: 0.72rem;
  color: var(--ink-soft);
}

/* --- counting sort demo (reuses .bars/.bar above and .dp-wrap/.dp-table from the DP pages) --- */
.cs-caption {
  font-family: var(--mono);
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  margin: 0.9rem 0 0.15rem;
}

/* --- KMP string matching demo (reuses .cells/.cell, .dp-wrap/.dp-table, .cs-caption, .dp-stats above) --- */
.cell.ghost {
  visibility: hidden;
  border-color: transparent;
  background: transparent;
}

.cell.miss {
  border: 2px dashed var(--accent);
  color: var(--accent);
  font-weight: bold;
}

/* --- Aho-Corasick demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node,
   .dp-items/.dp-item, .cells/.cell, .cs-caption, .dp-stats above) --- */
.bst-edge.fail-edge {
  stroke: var(--ink-soft);
  stroke-dasharray: 4 3;
  opacity: 0.55;
}

.bst-edge.fail-edge.active {
  stroke: var(--accent);
  stroke-width: 2.5;
  opacity: 1;
}

/* --- Rabin-Karp demo (reuses .cells/.cell, .cs-caption, .dp-stats above). color: var(--ink)
   not white here — matches .bfs-cell.reject's dark-on-gold convention; white-on-#c9962c only
   hits 2.66:1 contrast, well under WCAG AA's 4.5:1 (caught session 119's contrast sweep). --- */
.cell.collide {
  background: #c9962c;
  color: var(--ink);
  border-color: #c9962c;
  font-weight: bold;
}

/* --- Bitap demo (reuses .cells/.cell, .cs-caption, .dp-wrap/.dp-table, .dp-stats above).
   .cell.fuzzy is a real match with errors > 0 — distinct from .cell.collide above, which
   flags a false positive, not a genuine (if imperfect) match. */
.cell.fuzzy {
  background: #3d6b78;
  color: white;
  border-color: #3d6b78;
  font-weight: bold;
}

/* --- Bloom filter demo (reuses .cells/.cell, .cs-caption, .dp-stats, .cell.found/.cell.miss
   above). .cell.bit-on is a bit set to 1 by some earlier add(), not currently being probed.
   .cell.probe marks a bit this action is checking that isn't decisive on its own (checked,
   was already 1). A probed bit that's the 0 proving absence gets .miss instead; probed bits
   on a might-contain verdict get .found instead — .bit-on/.probe never combine with those. */
.cell.bit-on { background: var(--accent-soft); border-color: var(--accent); font-weight: bold; }
.cell.probe { border: 2px solid var(--ink); font-weight: bold; }

/* --- Van Emde Boas tree demo (reuses .cells/.cell, .cell.bit-on/.cell.probe/.cell.found,
   .cs-caption, .dp-stats, .log above). Same shape as .bar.block-edge on the sqrt decomposition
   demo, applied to a .cell grid instead of a .bars row: thick edge marks a cluster-of-4
   boundary, thin dashed edge marks the pair-of-2 boundary one level further down. */
.cell.cluster-edge { border-left: 3px solid var(--ink); }
.cell.subcluster-edge { border-left: 1px dashed var(--ink-soft); }

.bloom-added {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.8rem;
  margin: 0.4rem 0 0;
  min-height: 1.8rem;
}

.bloom-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
}

.bloom-empty { color: var(--ink-soft); }

/* --- N-Queens demo (reuses .bfs-grid/.bfs-cell, .dp-stats above). .dark is plain chessboard
   checkering; .attacked shows every row the column-in-progress's placed queens would reject;
   .reject/.backtrack are transient per-step flashes; .queen.solved marks a completed board. */
.bfs-cell.dark { background: var(--bg-raised); }
.bfs-cell.queen {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  line-height: 1;
  color: var(--accent);
  cursor: default;
}
.bfs-cell.queen.solved { background: #4f7d3a; border-color: #4f7d3a; color: white; }
.bfs-cell.attacked { background: #ead9bb; }
.bfs-cell.reject { background: #c9962c; border-color: #c9962c; }
.bfs-cell.backtrack { background: var(--accent-soft); border-color: var(--accent); }

/* --- Sudoku demo (reuses .bfs-grid/.bfs-cell, .dp-stats above). .num centers a digit for
   either a fixed given or a search-placed value; .box-shade tints alternating 3x3 boxes the
   same way .dark checkers N-Queens' board; .given/.filled distinguish the two digit sources;
   .solved marks every cell once the board is fully and validly filled. */
.bfs-cell.num {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.95rem;
  line-height: 1;
  cursor: default;
}
.bfs-cell.box-shade { background: var(--bg-raised); }
.bfs-cell.given { color: var(--ink); font-weight: 700; }
.bfs-cell.filled { color: var(--accent); font-weight: 600; }

/* --- Graph Coloring demo (reuses .topo-wrap/.topo-canvas/.topo-edges above for graph layout,
   .dp-stats for the attempts/backtracks line). .gc-node.c0-.c3 are the four assignable colors
   themselves — a fixed palette distinct from every other semantic color already in use on the
   site (reject/backtrack/solved flashes elsewhere aren't colorings, they're status), since a
   node's fill here *is* the algorithm's own output. Each node's label always carries its color
   number too, so the fill is a visual aid, never the only signal. .current marks the vertex
   being tried this step; .conflict flashes a vertex (and .gc-edge.conflict the edge between it
   and the neighbor that blocked it) on a rejected color attempt. */
.gc-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 2.7rem;
  height: 2.7rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--bg);
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 0.72rem;
  text-align: center;
  transition: background 0.2s, border-color 0.2s, color 0.2s;
}
.gc-node.current { border: 2px solid var(--ink); }
.gc-node.conflict { border: 2px solid var(--accent); }
.gc-node.c0 { background: #597897; border-color: #597897; color: white; font-weight: 600; }
.gc-node.c1 { background: #7d5a8a; border-color: #7d5a8a; color: white; font-weight: 600; }
.gc-node.c2 { background: #2a7d7d; border-color: #2a7d7d; color: white; font-weight: 600; }
.gc-node.c3 { background: #7a5a3f; border-color: #7a5a3f; color: white; font-weight: 600; }

.gc-edge { stroke: var(--line); stroke-width: 2; }
.gc-edge.conflict { stroke: var(--accent); stroke-width: 2.5; }
.bfs-cell.num.solved { background: #4f7d3a; border-color: #4f7d3a; color: white; }

/* --- Hamiltonian Path/Cycle demo (reuses .topo-wrap/.topo-canvas/.topo-edges for graph layout,
   .gc-node/.gc-node.current/.gc-node.conflict and .gc-edge/.gc-edge.conflict verbatim from Graph
   Coloring above, .dp-stats/.log). New: .gc-edge.inpath marks an edge that's part of the current
   walk, in the same blue as .gc-node.c0 (reused here as "on the walk" rather than "color 0",
   since only one path is ever shown at a time — no color-collision with Graph Coloring's own use
   since the two pages never render together). */
.gc-edge.inpath { stroke: #597897; stroke-width: 2.5; }

/* --- Huffman Coding demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node above for
   the merge tree, .bst-node.created for a just-merged highlight, .stat-table for the code table,
   .dp-stats for the final bit-count line, .cs-caption for the queue's small label). .hc-leaf
   carries a real symbol; .hc-internal is an anonymous merge result and gets a subtle fill so the
   two read apart without needing a legend. .hc-edge-label is the 0/1 bit each edge contributes. */
.bst-node.hc-internal { background: var(--bg-raised); color: var(--ink-soft); }
.hc-edge-label { font-family: var(--mono); font-size: 0.65rem; fill: var(--ink-soft); }

.hc-queue {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.8rem;
  margin: 0.25rem 0 1rem;
  min-height: 1.8rem;
}

.hc-chip {
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
  color: var(--ink-soft);
}

.hc-chip.new { border-color: var(--accent); color: var(--accent); font-weight: bold; }

.hc-queue-empty { color: var(--ink-soft); }

/* --- Activity Selection demo (reuses .cs-caption, .dp-stats above). New: a horizontal timeline,
   one row per activity in the order the greedy rule processes them, each row a label plus a
   track with a bar positioned/sized by percentage of the shared time axis. .as-bar states mirror
   the site's existing semantics — accepted reuses the same green as .cell.found, rejected reuses
   the same faded-dashed look as .cell.discarded, current reuses the accent border every
   in-progress highlight already uses. .as-cutoff is a short accent tick inside only the row
   currently being decided, marking the last accepted finish time on that row's own axis. */
.as-timeline {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin: 0.9rem 0;
}

.as-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}

.as-label {
  width: 6rem;
  flex-shrink: 0;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--ink-soft);
  text-align: right;
}

.as-track {
  position: relative;
  flex: 1;
  height: 1.4rem;
  background: var(--bg);
  border-bottom: 1px solid var(--line);
}

.as-bar {
  position: absolute;
  top: 0.15rem;
  bottom: 0.15rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--bg-raised);
  transition: background 0.2s, border-color 0.2s, opacity 0.2s;
}

.as-bar .as-bar-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
}

.as-bar.current { border: 2px solid var(--accent); }

.as-bar.accepted { background: #4f7d3a; border-color: #4f7d3a; }
.as-bar.accepted .as-bar-label { color: white; }

.as-bar.rejected { opacity: 0.35; border-style: dashed; }

/* --- Interval Point Cover demo additions (reuses .as-timeline/.as-row/.as-label/.as-track/
   .as-bar/.as-bar.accepted/.as-bar.current/.as-cutoff above verbatim). .covered marks an interval
   already stabbed by an earlier point (soft-tan, distinct from .accepted's "new point" green and
   from .rejected's "failed candidate" fade — being covered isn't a failure). .uncovered is applied
   only at the end of a run, overriding .covered, for an interval the demo's own independent
   validity check finds the final point set doesn't actually contain — the buggy sort-by-start rule
   marks these .covered during the step-through and only this final check catches the mistake. */
.as-bar.covered { background: var(--accent-soft); border-color: var(--accent); }

.as-bar.uncovered { background: var(--danger-soft); border-color: var(--danger); border-style: dashed; }
.as-bar.uncovered .as-bar-label { color: var(--danger); font-weight: bold; }

.as-cutoff {
  position: absolute;
  top: -0.3rem;
  bottom: -0.3rem;
  width: 2px;
  background: var(--accent);
}

.as-axis {
  background: transparent;
  border-bottom: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-family: var(--mono);
  font-size: 0.68rem;
  color: var(--ink-soft);
}

.as-warn { color: var(--accent); font-weight: bold; }

/* --- Fractional Knapsack demo (reuses .dp-items/.dp-item/.dp-item.taken, .dp-item.partial above,
   .cs-caption, .dp-result, .dp-stats). New: a single capacity bar filling left-to-right with one
   segment per item taken, striped for the one item that's only partially taken. */
.fk-capacity-track {
  display: flex;
  height: 2rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  overflow: hidden;
  background: var(--bg);
  margin: 0.4rem 0 0.9rem;
}

.fk-segment {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.68rem;
  color: white;
  background: #4f7d3a;
  border-right: 1px solid var(--bg);
  white-space: nowrap;
  overflow: hidden;
  transition: width 0.3s;
}

.fk-segment.partial {
  background: repeating-linear-gradient(45deg, #4f7d3a, #4f7d3a 6px, #3f6530 6px, #3f6530 12px);
}

.fk-segment-empty {
  background: transparent;
  border-right: none;
}

/* --- Count-Min Sketch demo (reuses .fw-matrix/.fw-matrix-wrap/.fw-matrix td.changed above for the
   d x w counter grid, .cs-caption, .dp-stats, .bloom-added/.bloom-chip/.bloom-empty from the Bloom
   Filter demo for the ground-truth chip list, .log). New: two small td modifiers for query-time
   highlighting — .cms-probe (a row's reading that wasn't the minimum) reuses .cell.probe's exact
   outline, .cms-min (the reading that won, i.e. the estimate) reuses .cell.found's exact green. */
.fw-matrix td.cms-probe {
  border: 2px solid var(--ink);
  font-weight: bold;
}

.fw-matrix td.cms-min {
  background: #4f7d3a;
  color: white;
  font-weight: bold;
}

/* --- Count Sketch demo (reuses .fw-matrix/.fw-matrix-wrap/.fw-matrix td.changed and .cms-probe
   above -- a row's reading that wasn't the chosen one still just needs the same outline, min or
   median. New: .csk-median is the same green as .cms-min under a name that fits a median-based
   winner instead of a minimum-based one; .neg colors a counter red when it's gone negative, a
   state Count-Min Sketch's own grid never reaches since its counters only ever count up. */
.fw-matrix td.csk-median {
  background: #4f7d3a;
  color: white;
  font-weight: bold;
}

.fw-matrix td.neg {
  color: var(--danger);
}

/* --- Consistent Hashing demo (reuses .kruskal-wrap/.kruskal-canvas for the ring's positioning
   context, .kruskal-edges for the SVG overlay, .kruskal-edge-label for the "0/1000" orientation
   label, .kruskal-edgelist for the node-chip row, .dp-wrap/.dp-table td.match/td.hit for the
   key/owner table's "moved"/"new" highlighting, .dp-stats, .log — all verbatim, zero new table
   CSS. New: the ring itself (a stroked circle with no prior equivalent), node/key markers, node
   chips with a remove button, and a 6-color palette for node identity — a node's label always
   carries its full name too, so color is a visual aid, never the only signal, same principle
   graph-coloring's .gc-node.c0-.c3 established. */
.ch-ring { stroke: var(--line); stroke-width: 2; fill: none; }

.ch-node {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 2.6rem;
  height: 2.6rem;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.66rem;
  text-align: center;
  line-height: 1.1;
  padding: 0.15rem;
  color: white;
  font-weight: 600;
  border: 2px solid var(--bg);
}

.ch-key {
  position: absolute;
  transform: translate(-50%, -50%);
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  border: 2px solid var(--bg);
}

.ch-key.moved { box-shadow: 0 0 0 3px var(--ink); }
.ch-key.new { box-shadow: 0 0 0 3px var(--accent); }

/* .ch-c0 was #5b7a99 (4.48:1 white-text contrast, session 161 sweep) — darkened to
   #597896 (4.63:1) to clear WCAG AA's 4.5:1, same class of near-miss session 119 found
   on .gc-node.c0. Only .ch-node pairs a PALETTE color with white text (.dot/.ch-key
   never render text), so only .ch-c0 needed checking against combined-class usage
   rather than same-rule pairs. */
.ch-c0 { background: #597896; }
.ch-c1 { background: #7d5a8a; }
.ch-c2 { background: #2a7d7d; }
.ch-c3 { background: #7a5a3f; }
.ch-c4 { background: #4f7a4f; }
.ch-c5 { background: #9c6b2f; }

.ch-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.2rem 0.5rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--bg-raised);
  font-family: var(--mono);
  font-size: 0.78rem;
}

.ch-chip .dot {
  width: 0.6rem;
  height: 0.6rem;
  border-radius: 50%;
  display: inline-block;
}

.ch-chip .remove {
  border: none;
  background: none;
  color: var(--ink-soft);
  cursor: pointer;
  font-family: var(--mono);
  font-size: 0.85rem;
  padding: 0 0.1rem;
  line-height: 1;
}

.ch-chip .remove:hover { color: var(--accent); }

.demo input#nodeInput, .demo input#keyInput { width: 9rem; }

/* --- Minimax demo (reuses .bfs-grid/.bfs-cell/.num from Sudoku/N-Queens above for the tic-tac-toe
   board, .given/.filled to distinguish the fixed starting position from marks the search is
   currently trying out, .current for the cell just placed, .num.solved for the winning line on a
   terminal board, .dp-stats/.log). Zero new classes — every semantic this page needs (fixed vs.
   hypothetical, current, terminal-win) already existed from earlier board-grid demos. */

/* --- Edmonds-Karp / Maximum Flow demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-node/
   .kruskal-edge/.kruskal-edge.current/.kruskal-edge.accepted/.kruskal-edge-label/.kruskal-stats/
   .log verbatim — same graph-canvas plumbing every MST/hashing-ring page has used since Kruskal.
   New, because this is the site's first *directed* graph: an arrowhead per edge via SVG <marker>,
   one colored variant per edge state so the arrowhead always matches its line's color without
   relying on context-fill (patchy support) — and a "this edge/node is on the revealed min cut"
   modifier, since no earlier graph demo ever needed to highlight a *cut* through a graph. */
.mf-arrowhead-default { fill: var(--line); }
.mf-arrowhead-current { fill: var(--ink); }
.mf-arrowhead-flowing { fill: var(--accent); }

.kruskal-edge.cut { stroke-dasharray: 3 4; }

.kruskal-node.cut-source { border: 2px solid var(--accent); background: var(--bg-raised); }

/* --- Dinic's Algorithm demo (reuses every Edmonds-Karp class verbatim, including
   .kruskal-edge.rejected from Kruskal for edges excluded from the current phase's level
   graph — dashed/dim already meant "not usable right now" and that reading carries over
   unchanged). New: a small per-node level badge, since no earlier graph demo needed to show
   a number *on* the node itself rather than on an edge. */
.level-badge {
  position: absolute;
  bottom: -6px;
  right: -6px;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  font-family: var(--mono);
  font-size: 0.6rem;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* --- Push-Relabel demo (reuses every Edmonds-Karp/Dinic's class verbatim, including
   .level-badge for the per-node height number — same "small circular number on a node"
   shape, different algorithm's number). New: an .excess-badge in the opposite corner,
   since this is the first flow demo where a node itself can hold a value mid-algorithm
   (its excess) rather than every number living on an edge. */
.excess-badge {
  position: absolute;
  top: -6px;
  left: -6px;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  background: var(--ink);
  color: #fff;
  font-family: var(--mono);
  font-size: 0.6rem;
  align-items: center;
  justify-content: center;
  line-height: 1;
}

/* --- Miller-Rabin demo (reuses .cells/.cell, .cs-caption, .dp-stats, .log above). New:
   .cell.wide, since this is the first .cell grid whose contents are full modular-arithmetic
   results (up to 21 digits) rather than a single index/character that always fits the fixed
   2.4rem square. */
.cell.wide {
  width: auto;
  min-width: 2.4rem;
  padding: 0 0.5rem;
}

/* --- Job Sequencing demo (reuses .dp-items/.dp-item, .cells/.cell, .cs-caption, .dp-stats,
   .log, .as-warn above). New: .dp-item.current mirrors .as-bar.current's accent-border
   highlight for the job just decided; .dp-item.rejected mirrors .as-bar.rejected's faded
   dashed look for a job that found no free slot. */
.dp-item.current { border: 2px solid var(--accent); }

.dp-item.rejected { opacity: 0.35; border-style: dashed; }

/* --- Minimum-Cost Maximum Flow demo (reuses every Edmonds-Karp/Dinic's/Push-Relabel class
   verbatim, including .kruskal-edge.danger/.kruskal-edge-label.danger from Second-Best
   Spanning Tree for the reverse-residual-edge highlight). New: a fourth arrowhead color to
   match .danger, since no earlier flow demo needed to color an arrowhead red — every earlier
   edge state (default/current/flowing) already had one. */
.mf-arrowhead-danger { fill: var(--danger); }

/* --- Comparison-sort race, choosing-a-comparison-sort.html: the site's first interactive
   guide. A horizontal bar per algorithm, scaled to the slowest finished run. */
.race-wrap {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0.75rem 0;
}
.race-row {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-size: 0.85rem;
}
.race-name { width: 8rem; flex: none; }
.race-bar-track {
  flex: 1 1 auto;
  height: 1.1rem;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 4px;
  overflow: hidden;
}
.race-bar-fill { display: block; height: 100%; background: var(--accent); }
.race-bar-fill.error { background: var(--danger); }
.race-ms {
  width: 11rem;
  flex: none;
  text-align: right;
  color: var(--ink-soft);
}
.race-ms.bad { color: var(--danger); font-weight: bold; }

/* --- Point in Polygon demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-node.endpoint/
   .kruskal-edges/.kruskal-edge(.accepted)/.kruskal-stats verbatim). New: a translucent polygon
   fill so "inside" reads as a region, not just an outline; a dashed ray line distinct from
   .kruskal-edge's own dashed .rejected state; small dots marking each actual ray/edge crossing. */
.pip-fill { fill: var(--accent-soft); fill-opacity: 0.45; stroke: none; }
.pip-ray { stroke: var(--ink-soft); stroke-width: 1.5; stroke-dasharray: 3 4; }
.pip-cross { fill: var(--accent); }

/* --- Slab Decomposition demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-node.endpoint/
   .kruskal-edges/.kruskal-edge(.accepted/.current)/.kruskal-stats/.pip-fill/.pip-ray verbatim).
   New: a dashed vertical line per slab boundary, distinct from .pip-ray's horizontal dash; a
   translucent rectangle marking which slab the binary search landed in. */
.slab-line { stroke: var(--ink-soft); stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.7; }
.slab-highlight { fill: var(--ink); fill-opacity: 0.06; stroke: none; }

/* --- Quadtree demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-node/.kruskal-edges/
   .kruskal-node.kd-best/.kruskal-node.interior/.kruskal-stats/.pip-fill verbatim, the last for
   the query-rectangle overlay). New: quadrant-boundary rectangles, since a quadtree divides
   space into nested boxes instead of connecting points with lines or a single fixed shape. */
.qt-box { fill: none; stroke: var(--line); stroke-width: 1.5; }
.qt-box.visited { stroke: var(--ink); stroke-width: 2; }
.qt-box.pruned { stroke: var(--ink-soft); stroke-dasharray: 4 3; opacity: 0.5; }

/* --- R-tree demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-edges/.kruskal-stats/.pip-fill/
   .qt-box(.visited/.pruned) verbatim). New: .qt-box.internal marks an internal node's MBR (drawn
   dashed/faint so it reads as "container," distinct from a leaf's solid boundary) since an R-tree,
   unlike a quadtree, has a real multi-level box hierarchy worth showing. .rt-rect/.rt-label draw
   the indexed data itself as small filled rectangles with a centered letter, not .kruskal-node
   point circles -- an R-tree's entries are rectangles, not points, so the shape needs to say so. */
.qt-box.internal { stroke-dasharray: 3 3; opacity: 0.65; }
.rt-rect { fill: var(--accent-soft); stroke: var(--ink-soft); stroke-width: 1; }
.rt-rect.matched { fill: var(--accent); stroke: var(--accent); }
.rt-label { font-family: var(--mono); font-size: 0.62rem; fill: var(--ink); text-anchor: middle; dominant-baseline: middle; pointer-events: none; }
.rt-rect.matched + .rt-label { fill: #fff; font-weight: bold; }

/* --- Interval Tree demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node/.target
   verbatim for the augmented BST diagram -- it's a genuine binary tree, so the same in-order-
   index/depth layout applies unchanged). New: .it-hit/.it-pruned mark a node as a confirmed
   overlap or an entire skipped subtree. .itree-line-wrap/.itree-row/.itree-bar are new -- no
   existing component draws several labeled ranges on a shared numeric axis, which is the natural
   picture for what an interval actually *is* (a span, not a point or a box) alongside the tree
   that indexes them. */
.bst-node.it-hit { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: bold; }
.bst-node.it-pruned { opacity: 0.35; }
.itree-line-wrap {
  position: relative;
  margin: 1rem 0 0.5rem;
  padding: 0.5rem 0.5rem 0.75rem;
  border: 1px dashed var(--line);
  border-radius: 8px;
  overflow-x: auto;
}
.itree-row { position: relative; height: 1.7rem; min-width: 320px; }
.itree-bar {
  position: absolute;
  top: 0.2rem;
  height: 1.2rem;
  border-radius: 999px;
  background: var(--bg-raised);
  border: 1px solid var(--line);
  display: flex;
  align-items: center;
  padding: 0 0.4rem;
  font-family: var(--mono);
  font-size: 0.7rem;
  white-space: nowrap;
  overflow: hidden;
  transition: background 0.2s, border-color 0.2s, opacity 0.2s, color 0.2s;
}
.itree-bar.it-current { border: 2px solid var(--ink); font-weight: bold; }
.itree-bar.it-hit { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: bold; }
.itree-bar.it-pruned { opacity: 0.3; }
.itree-bar.it-query { background: transparent; border: 2px dashed var(--ink); color: var(--ink); font-weight: bold; }

/* --- Merkle Tree demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node above).
   Leaf vs internal nodes get their own background via .mk-leaf/.mk-internal (same
   background+color pairing as .hc-internal above, already covered by the contrast sweep);
   changed/proof-path/target states are outline-only overlays, same technique as .rb-touch/
   .rb-path above, so they compose with the base classes without a background clash. ---*/
.bst-node.mk-leaf { background: var(--bg-raised); font-weight: bold; }
.bst-node.mk-internal { background: var(--bg-raised); color: var(--ink-soft); }
.bst-node.mk-changed { outline: 3px solid var(--accent); outline-offset: 2px; }
.bst-node.mk-proof { outline: 2px dashed var(--ink-soft); outline-offset: 2px; }
.bst-node.mk-target { outline: 3px solid var(--ink); outline-offset: 2px; }

/* --- Trapezoidal Map demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-edges/.kruskal-edge/
   .kruskal-node.endpoint/.kruskal-stats/.pip-fill verbatim). New: .tm-trap draws one partition
   cell's outline — conceptually the same unfilled-region idea as .qt-box, but defined fresh
   rather than reusing that selector directly, since .qt-box.visited already carries a different,
   already-shipped meaning (a visited-but-not-necessarily-final search node) on the Quadtree page,
   and this demo needs its highlighted cell to carry a fill, not just a heavier stroke. */
.tm-trap { fill: none; stroke: var(--line); stroke-width: 1; }
.tm-trap.hit { fill: var(--accent-soft); fill-opacity: 0.55; stroke: var(--accent); stroke-width: 2; }

/* --- Fibonacci Heap demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node/.target/
   .visited/.rotated above for the root-list forest layout — same variable-arity subtreeWidth/
   layout recursion b-tree.html uses, just run once per root tree). .fh-marked is the one new
   class, an outline-only overlay for the persistent "marked" bit, same technique as .rb-path/
   .mk-proof above, so it composes with .target/.visited without a background clash or a new
   color to contrast-check. */
.bst-node.fh-marked { outline: 2px dashed var(--ink-soft); outline-offset: 3px; }

/* --- Karatsuba/FFT recursion-tree demo (.kt-tree/.kt-tree-wrap/.kt-node/.kt-label, first shipped
   on karatsuba-multiplication.html session 247 with no rules at all — the plain nested <ul>/<li>
   rendered with zero visual distinction between an active and a resolved call, despite that
   page's own caption text describing "bold border = current call, shaded = resolved." Added here
   (fast-fourier-transform.html reuses the identical class names for its own recursion trees) using
   CSS variables rather than a hardcoded hex, so no .demo dark-mode override is needed the way the
   ~60 hardcoded state colors elsewhere need one. */
.kt-tree, .kt-tree ul { list-style: none; margin: 0.15rem 0; padding-left: 1.25rem; }
.kt-tree { padding-left: 0; }
.kt-tree-wrap { overflow-x: auto; }
.kt-label { display: inline-block; padding: 0.05rem 0.4rem; border-radius: 3px; }
.kt-node.current > .kt-label { border: 2px solid var(--ink); }
.kt-node.done > .kt-label { background: var(--bg-raised); }

/* --- BSP Tree demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-edges/.kruskal-edge/
   .kruskal-node.kd-query/.kruskal-stats/.log verbatim). New: .kruskal-edge.cg0/.cg1/.cg2 stroke
   modifiers reuse the exact hex values already WCAG-checked as white-on-color text pairs for
   .kruskal-node.cg0-.cg2 (session 119/161 lineage) — applied here to plain SVG strokes and an SVG
   <text> fill, neither of which carries the text-on-background contrast requirement a filled node
   label does, so no new contrast check is needed. .bsp-wall.current bumps stroke-width only (never
   stroke color) at three-class specificity, specifically so it can't lose a cascade-order fight
   against the two-class .kruskal-edge.cgN color rules below it. .bsp-partition is the dashed,
   half-opacity "extended splitting line" style, clipped to a fixed world-space box before reaching
   here (see clipLineToBox in the page's own script) rather than relying on SVG overflow clipping.
   .bsp-screen/.bsp-col are new: a flex row of plain divs standing in for a 360° panorama "screen,"
   painted via inline background-color per column as the demo's own script steps through a
   traversal order — deliberately plain divs with percentage-free flex sizing (no
   getBoundingClientRect anywhere), consistent with the standing no-real-browser lesson. */
.kruskal-edge.cg0 { stroke: #597896; }
.kruskal-edge.cg1 { stroke: #7d5a8a; }
.kruskal-edge.cg2 { stroke: #2a7d7d; }
.kruskal-edge.bsp-wall { stroke-width: 3; }
.kruskal-edge.bsp-wall.current { stroke-width: 5; }
.kruskal-edge.bsp-partition { stroke-dasharray: 6 4; opacity: 0.55; stroke-width: 1.5; }
.bsp-wall-label { font-family: var(--mono); font-size: 0.8rem; font-weight: bold; }
.bsp-wall-label.cg0 { fill: #597896; }
.bsp-wall-label.cg1 { fill: #7d5a8a; }
.bsp-wall-label.cg2 { fill: #2a7d7d; }

.bsp-vp.active { background: var(--accent); border-color: var(--accent); }

.bsp-screen-label {
  font-size: 0.8rem;
  color: var(--ink-soft);
  margin-top: 0.7rem;
}

.bsp-screen {
  display: flex;
  height: 2.4rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  overflow: hidden;
  margin-top: 0.3rem;
}

.bsp-col { flex: 1 1 auto; background: var(--bg-raised); }
.bsp-col + .bsp-col { border-left: 1px solid var(--bg); }

/* --- Interval Partitioning demo (reuses .as-timeline/.as-row/.as-label/.as-track/.as-bar
   (.current/.accepted/.covered/.uncovered)/.as-axis/.as-warn/.cs-caption/.dp-stats verbatim,
   zero new rules). Rows here are rooms, not input items — created dynamically as the greedy rule
   opens them, and a single .as-track can hold several .as-bar children as talks get reused into
   the same room over time, unlike Activity Selection's/Interval Point Cover's one-bar-per-row
   shape. .accepted marks a reused room, .covered marks a newly opened one (repurposing Interval
   Point Cover's two-state palette for a different meaning), .uncovered marks a real detected
   double-booking. */

/* --- Dancing Links demo (reuses .bfs-cell's base square/border/.current/.reject/.backtrack
   states verbatim — those three are already generic, not gated behind N-Queens' own .queen).
   .dlx-grid is a plain non-interactive variant of .bfs-grid (no hover affordance, matrix cells
   aren't clickable). Only three new cell states needed: .removed (a covered column or a row with
   no live columns left), .in-path (a row still tentatively selected up the current recursion),
   .solved (a row in the final reported solution). */
.dlx-grid {
  display: grid;
  gap: 3px;
  margin: 1rem 0;
  max-width: 32rem;
}

.dlx-cell {
  cursor: default;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 0.85rem;
}

.dlx-cell.dlx-corner { background: transparent; border-color: transparent; }
.dlx-cell.dlx-colhead, .dlx-cell.dlx-rowhead {
  background: var(--bg-raised);
  font-weight: 700;
  color: var(--ink-soft);
}
.dlx-cell.dlx-one { color: var(--ink); font-weight: 600; }
.dlx-cell.removed { background: var(--bg-raised); color: var(--ink-soft); opacity: 0.45; }
.dlx-cell.in-path { border-color: var(--accent); }
.dlx-cell.solved { background: #4f7d3a; border-color: #4f7d3a; color: white; }

/* --- Levenshtein Automaton demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node/
   .trie-root/.wordend/.target/.deadend/.visited above verbatim, same node vocabulary as
   trie.html's own demo, plus .it-pruned (Interval Tree's own "confirmed out of range" dimming)
   for a whole trie subtree the automaton proved it never needs to visit). Zero new rules. */

/* --- Hilbert Curve demo (reuses .kruskal-wrap/.kruskal-canvas/.kruskal-node/.kruskal-edges/
   .kruskal-edge/.kd-best(match)/.interior(wasted)/.endpoint(currently visiting)/.pip-fill/
   .kruskal-stats above verbatim). One addition: a real match the naive corner-to-corner scan
   never visits at all (a false negative, not just wasted work) needs a state .interior/.kd-best
   can't express — same --danger/--danger-soft pair as .uf-node.contradiction (Union-Find's own
   "this should be impossible" marker), reused here for the same reason: an answer that's
   actually wrong, not merely a rejected candidate. */
.kruskal-node.missed { background: var(--danger-soft); border-color: var(--danger); color: var(--danger); font-weight: bold; }

/* --- Partition Refinement demo: one bordered .pr-block per current group, each holding a
   .cells row of reused .cell elements. .cell.range marks "toggled into the splitter S" (same
   tan highlight every other .cells demo uses for "under consideration"); .cell.sorted-half
   marks the DFA's one accepting state (same neutral ink outline search-rotated-sorted-array.html
   uses, not a new color — this page introduces zero new colors, so no WCAG re-sweep needed). */
.pr-blocks { display: flex; flex-direction: column; gap: 0.6rem; margin: 1rem 0; }
.pr-block { border: 1px solid var(--line); border-radius: 6px; padding: 0.5rem 0.75rem; }
.pr-block .cells { margin: 0.35rem 0 0; }
.pr-block-label { font-family: var(--mono); font-size: 0.78rem; color: var(--ink-soft); }

/* --- BK-Tree demo (reuses .bst-wrap/.bst-canvas/.bst-edges/.bst-edge/.bst-node/.target/.visited/
   .it-pruned above verbatim, same node vocabulary as the trie/Levenshtein Automaton demos). New:
   .bk-node widens the circle slightly for a whole word instead of one character (up to 4 letters
   in this page's own dictionary) and shrinks the font to fit; .bk-edge-label prints the real edge
   distance at each edge's midpoint, the same pattern .hc-edge-label/.rdx-edge-label already use
   for their own edge annotations — no new colors, just a new label class per that precedent. */
.bst-node.bk-node { width: 2.6rem; height: 2.6rem; font-size: 0.62rem; }
.bk-edge-label { font-family: var(--mono); font-size: 0.62rem; fill: var(--ink-soft); text-anchor: middle; }

/* --- Winding Number Algorithm demo: two .kruskal-canvas panels side by side, same vertex list
   drawn twice with a different SVG fill-rule each (evenodd vs nonzero) so the browser's own
   renderer shows the disagreement directly. .wind-point marks the query point as a plain filled
   circle (ink on bg, same pairing every .kruskal-node outline already uses, so no new WCAG check
   needed — it's a shape stroke, not a text/background color pair). */
.wind-panels { display: flex; gap: 1rem; flex-wrap: wrap; justify-content: center; }
.wind-panel { text-align: center; }
.wind-panel-label { font-family: var(--mono); font-size: 0.75rem; color: var(--ink-soft); margin-bottom: 0.35rem; }
.wind-point { fill: var(--ink); stroke: var(--bg); stroke-width: 2; }
