Depth First
A seven-node tree walked depth-first: a ring-shaped visitor descends edge by edge, painting the path clay as each node is pushed onto the call stack, then turns rust and climbs back — and as it climbs, the clay retracts while olive fills in behind it, so a resolved subtree is literally the trail the return trip leaves. The clay chain from root to visitor is the recursion stack, mirrored by frames pushing and popping in a panel on the right, and the descents use cubic-bezier(0.77,0,0.175,1) so each hop accelerates and settles like a real step, with leaves dwelling twice as long to read as work. Reach for it when you need recursion, backtracking, or scope/lifetime to be understood from motion rather than from a stack trace — algorithm explainers, compiler and parser docs, anything where "and then it comes back up" is the hard part to teach.
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Depth First — a visitor unwinding a tree</title>
<style>
/* ------------------------------------------------------------------
DEPTH FIRST — a recursive descent, made legible by motion.
WHAT TO KEEP when pasting into a real project:
• everything between the "=== FIGURE ===" and "=== /FIGURE ==="
markers below, plus the <svg> markup in the body.
• the page chrome around it (body / .stage-wrap / .caption) is only
here so the file stands alone — drop it.
The whole piece is one 12s timeline: 0-1s the tree draws in, 1-9.4s
the traversal runs, 9.4-10.9s the settled state holds, 10.9-11.6s it
dissolves, then it replays. Every animated element shares
--cycle, so the loop can be retimed from one variable.
------------------------------------------------------------------ */
:root {
--ivory: #FAF9F5;
--ink: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--rust: #B04A3F;
--gray-500: #87867F;
--gray-300: #D1CFC5;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);
--ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
--cycle: 12s;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--ivory);
color: var(--ink);
font-family: var(--sans);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 32px 20px;
-webkit-font-smoothing: antialiased;
}
.stage-wrap {
width: min(100%, 680px);
}
/* === FIGURE === */
.card {
position: relative;
width: 100%;
aspect-ratio: 4 / 3;
background: var(--ivory);
border: 1px solid var(--oat);
border-radius: 16px;
overflow: hidden;
}
.card svg { display: block; width: 100%; height: 100%; }
/* One clock. Per-keyframe timing functions do the shaping, so the
element-level function stays linear. */
.cyc {
animation-duration: var(--cycle);
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-fill-mode: both;
}
.stage { animation-name: cycle; }
/* --- edges ------------------------------------------------------- */
.base { fill: none; stroke: var(--gray-300); stroke-width: 1.5; }
.trace {
fill: none;
stroke-width: 2.5;
stroke-dasharray: 100;
stroke-linecap: butt;
}
/* clay grows parent -> child on the way down; on the way back up it
retracts from the child end while the olive path fills in behind it,
so the two strokes wipe past each other exactly under the visitor. */
.clay { stroke: var(--clay); }
.olive { stroke: var(--olive); }
/* --- nodes ------------------------------------------------------- */
.disc {
transform-box: fill-box;
transform-origin: center;
}
.glyph {
font-family: var(--mono);
font-size: 13px;
font-weight: 600;
letter-spacing: 0.02em;
text-anchor: middle;
dominant-baseline: central;
}
/* --- the visitor -------------------------------------------------- */
.walker { animation-name: walk; }
.hue { animation-name: walker-hue; }
/* The visitor is a ring, not a dot: it encircles whatever node it is
standing on instead of covering the label. */
.ring { fill: none; stroke: currentColor; stroke-width: 2; opacity: 0.9; }
.halo { fill: none; stroke: currentColor; stroke-width: 5; opacity: 0.13; }
/* --- call stack --------------------------------------------------- */
.slot {
fill: none;
stroke: var(--gray-300);
stroke-width: 1;
stroke-dasharray: 3 4;
opacity: 0.75;
}
.frame rect {
fill: rgba(217, 119, 87, 0.10);
stroke: var(--clay);
stroke-width: 1.25;
}
.fglyph {
font-family: var(--mono); font-size: 11px; font-weight: 600;
fill: var(--clay); dominant-baseline: central;
}
.fdepth {
font-family: var(--mono); font-size: 10px;
fill: var(--gray-500); text-anchor: end; dominant-baseline: central;
}
/* --- static chrome ------------------------------------------------ */
.eyebrow {
font-family: var(--mono); font-size: 10px; letter-spacing: 0.14em;
fill: var(--gray-500); text-transform: uppercase;
}
.legend-label {
font-family: var(--mono); font-size: 10px; letter-spacing: 0.04em;
fill: var(--gray-500); dominant-baseline: central;
}
.legend-dot { r: 3.5; }
@keyframes cycle {
0%, 90.8333% { opacity: 1; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
96.6667%, 100% { opacity: 0; }
}
@keyframes walk {
0%, 5.8333% { transform: translate(185px, 66px); opacity: 0; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
8.1667% { transform: translate(185px, 66px); opacity: 1; }
10% { transform: translate(185px, 66px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
13.5% { transform: translate(100px, 165px); animation-timing-function: linear; }
15.1667% { transform: translate(100px, 165px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
18.6667% { transform: translate(48px, 264px); animation-timing-function: linear; }
22% { transform: translate(48px, 264px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
25.5% { transform: translate(100px, 165px); animation-timing-function: linear; }
27.1667% { transform: translate(100px, 165px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
30.6667% { transform: translate(140px, 264px); animation-timing-function: linear; }
34% { transform: translate(140px, 264px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
37.5% { transform: translate(100px, 165px); animation-timing-function: linear; }
39.1667% { transform: translate(100px, 165px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
42.6667% { transform: translate(185px, 66px); animation-timing-function: linear; }
44.3333% { transform: translate(185px, 66px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
47.8333% { transform: translate(270px, 165px); animation-timing-function: linear; }
49.5% { transform: translate(270px, 165px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
53% { transform: translate(225px, 264px); animation-timing-function: linear; }
56.3333% { transform: translate(225px, 264px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
59.8333% { transform: translate(270px, 165px); animation-timing-function: linear; }
61.5% { transform: translate(270px, 165px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
65% { transform: translate(315px, 264px); animation-timing-function: linear; }
68.3333% { transform: translate(315px, 264px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
71.8333% { transform: translate(270px, 165px); animation-timing-function: linear; }
73.5% { transform: translate(270px, 165px); animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
77% { transform: translate(185px, 66px); animation-timing-function: linear; }
78.6667% { transform: translate(185px, 66px); opacity: 1; }
81.5%, 100% { transform: translate(185px, 66px); opacity: 0; }
}
@keyframes walker-hue {
0% { color: #D97757; }
9.4167% { color: #D97757; }
10.5833% { color: #D97757; }
14.5833% { color: #D97757; }
15.75% { color: #D97757; }
21.4167% { color: #D97757; }
22.5833% { color: #B04A3F; }
26.5833% { color: #B04A3F; }
27.75% { color: #D97757; }
33.4167% { color: #D97757; }
34.5833% { color: #B04A3F; }
38.5833% { color: #B04A3F; }
39.75% { color: #B04A3F; }
43.75% { color: #B04A3F; }
44.9167% { color: #D97757; }
48.9167% { color: #D97757; }
50.0833% { color: #D97757; }
55.75% { color: #D97757; }
56.9167% { color: #B04A3F; }
60.9167% { color: #B04A3F; }
62.0833% { color: #D97757; }
67.75% { color: #D97757; }
68.9167% { color: #B04A3F; }
72.9167% { color: #B04A3F; }
74.0833% { color: #B04A3F; }
100% { color: #B04A3F; }
}
@keyframes node-A {
0%, 0.8333% { opacity: 0; transform: scale(0.92); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
4.3333% { opacity: 1; transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; }
8.3333% { transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
9.5833% { transform: scale(1.08); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
11% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
78.6667% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
80.8333%, 100% { opacity: 1; transform: scale(1); fill: #788C5D; stroke: #788C5D; stroke-width: 2.5; }
}
@keyframes label-A {
0%, 0.8333% { opacity: 0; fill: #141413; }
4.3333% { opacity: 1; fill: #141413; }
78.6667% { fill: #141413; }
80.8333%, 100% { opacity: 1; fill: #FAF9F5; }
}
@keyframes node-B {
0%, 2% { opacity: 0; transform: scale(0.92); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
5.5% { opacity: 1; transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; }
13.5% { transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
14.75% { transform: scale(1.08); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
16.1667% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
39.1667% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
41.3333%, 100% { opacity: 1; transform: scale(1); fill: #788C5D; stroke: #788C5D; stroke-width: 2.5; }
}
@keyframes label-B {
0%, 2% { opacity: 0; fill: #141413; }
5.5% { opacity: 1; fill: #141413; }
39.1667% { fill: #141413; }
41.3333%, 100% { opacity: 1; fill: #FAF9F5; }
}
@keyframes node-C {
0%, 2% { opacity: 0; transform: scale(0.92); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
5.5% { opacity: 1; transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; }
47.8333% { transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
49.0833% { transform: scale(1.08); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
50.5% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
73.5% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
75.6667%, 100% { opacity: 1; transform: scale(1); fill: #788C5D; stroke: #788C5D; stroke-width: 2.5; }
}
@keyframes label-C {
0%, 2% { opacity: 0; fill: #141413; }
5.5% { opacity: 1; fill: #141413; }
73.5% { fill: #141413; }
75.6667%, 100% { opacity: 1; fill: #FAF9F5; }
}
@keyframes node-D {
0%, 3.1667% { opacity: 0; transform: scale(0.92); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
6.6667% { opacity: 1; transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; }
18.6667% { transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
19.9167% { transform: scale(1.08); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
21.3333% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
22% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
24.1667%, 100% { opacity: 1; transform: scale(1); fill: #788C5D; stroke: #788C5D; stroke-width: 2.5; }
}
@keyframes label-D {
0%, 3.1667% { opacity: 0; fill: #141413; }
6.6667% { opacity: 1; fill: #141413; }
22% { fill: #141413; }
24.1667%, 100% { opacity: 1; fill: #FAF9F5; }
}
@keyframes node-E {
0%, 3.1667% { opacity: 0; transform: scale(0.92); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
6.6667% { opacity: 1; transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; }
30.6667% { transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
31.9167% { transform: scale(1.08); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
33.3333% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
34% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
36.1667%, 100% { opacity: 1; transform: scale(1); fill: #788C5D; stroke: #788C5D; stroke-width: 2.5; }
}
@keyframes label-E {
0%, 3.1667% { opacity: 0; fill: #141413; }
6.6667% { opacity: 1; fill: #141413; }
34% { fill: #141413; }
36.1667%, 100% { opacity: 1; fill: #FAF9F5; }
}
@keyframes node-F {
0%, 3.1667% { opacity: 0; transform: scale(0.92); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
6.6667% { opacity: 1; transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; }
53% { transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
54.25% { transform: scale(1.08); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
55.6667% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
56.3333% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
58.5%, 100% { opacity: 1; transform: scale(1); fill: #788C5D; stroke: #788C5D; stroke-width: 2.5; }
}
@keyframes label-F {
0%, 3.1667% { opacity: 0; fill: #141413; }
6.6667% { opacity: 1; fill: #141413; }
56.3333% { fill: #141413; }
58.5%, 100% { opacity: 1; fill: #FAF9F5; }
}
@keyframes node-G {
0%, 3.1667% { opacity: 0; transform: scale(0.92); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
6.6667% { opacity: 1; transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; }
65% { transform: scale(1); fill: #FAF9F5; stroke: #D1CFC5; stroke-width: 1.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
66.25% { transform: scale(1.08); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
67.6667% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; }
68.3333% { transform: scale(1); fill: #FAF9F5; stroke: #D97757; stroke-width: 2.5; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
70.5%, 100% { opacity: 1; transform: scale(1); fill: #788C5D; stroke: #788C5D; stroke-width: 2.5; }
}
@keyframes label-G {
0%, 3.1667% { opacity: 0; fill: #141413; }
6.6667% { opacity: 1; fill: #141413; }
68.3333% { fill: #141413; }
70.5%, 100% { opacity: 1; fill: #FAF9F5; }
}
@keyframes base-AB {
0%, 0.5% { opacity: 0; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
4.6667%, 100% { opacity: 1; }
}
@keyframes clay-AB {
0%, 10% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
13.5% { stroke-dashoffset: 0; }
39.1667% { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
42.6667%, 100% { stroke-dashoffset: 100; }
}
@keyframes olive-AB {
0%, 39.1667% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
42.6667%, 100% { stroke-dashoffset: 0; }
}
@keyframes base-AC {
0%, 0.5% { opacity: 0; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
4.6667%, 100% { opacity: 1; }
}
@keyframes clay-AC {
0%, 44.3333% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
47.8333% { stroke-dashoffset: 0; }
73.5% { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
77%, 100% { stroke-dashoffset: 100; }
}
@keyframes olive-AC {
0%, 73.5% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
77%, 100% { stroke-dashoffset: 0; }
}
@keyframes base-BD {
0%, 1.6667% { opacity: 0; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
5.8333%, 100% { opacity: 1; }
}
@keyframes clay-BD {
0%, 15.1667% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
18.6667% { stroke-dashoffset: 0; }
22% { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
25.5%, 100% { stroke-dashoffset: 100; }
}
@keyframes olive-BD {
0%, 22% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
25.5%, 100% { stroke-dashoffset: 0; }
}
@keyframes base-BE {
0%, 1.6667% { opacity: 0; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
5.8333%, 100% { opacity: 1; }
}
@keyframes clay-BE {
0%, 27.1667% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
30.6667% { stroke-dashoffset: 0; }
34% { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
37.5%, 100% { stroke-dashoffset: 100; }
}
@keyframes olive-BE {
0%, 34% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
37.5%, 100% { stroke-dashoffset: 0; }
}
@keyframes base-CF {
0%, 1.6667% { opacity: 0; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
5.8333%, 100% { opacity: 1; }
}
@keyframes clay-CF {
0%, 49.5% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
53% { stroke-dashoffset: 0; }
56.3333% { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
59.8333%, 100% { stroke-dashoffset: 100; }
}
@keyframes olive-CF {
0%, 56.3333% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
59.8333%, 100% { stroke-dashoffset: 0; }
}
@keyframes base-CG {
0%, 1.6667% { opacity: 0; animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
5.8333%, 100% { opacity: 1; }
}
@keyframes clay-CG {
0%, 61.5% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
65% { stroke-dashoffset: 0; }
68.3333% { stroke-dashoffset: 0; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
71.8333%, 100% { stroke-dashoffset: 100; }
}
@keyframes olive-CG {
0%, 68.3333% { stroke-dashoffset: 100; animation-timing-function: cubic-bezier(0.77, 0, 0.175, 1); }
71.8333%, 100% { stroke-dashoffset: 0; }
}
@keyframes bar-A {
0%, 8.3333% { opacity: 0; transform: translateX(-12px); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
10.5% { opacity: 1; transform: translateX(0); }
78.6667% { opacity: 1; transform: translateX(0); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
81.1667%, 100% { opacity: 0; transform: translateX(16px); }
}
@keyframes bar-B {
0%, 13.5% { opacity: 0; transform: translateX(-12px); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
15.6667% { opacity: 1; transform: translateX(0); }
39.1667% { opacity: 1; transform: translateX(0); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
41.6667%, 100% { opacity: 0; transform: translateX(16px); }
}
@keyframes bar-C {
0%, 47.8333% { opacity: 0; transform: translateX(-12px); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
50% { opacity: 1; transform: translateX(0); }
73.5% { opacity: 1; transform: translateX(0); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
76%, 100% { opacity: 0; transform: translateX(16px); }
}
@keyframes bar-D {
0%, 18.6667% { opacity: 0; transform: translateX(-12px); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
20.8333% { opacity: 1; transform: translateX(0); }
22% { opacity: 1; transform: translateX(0); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
24.5%, 100% { opacity: 0; transform: translateX(16px); }
}
@keyframes bar-E {
0%, 30.6667% { opacity: 0; transform: translateX(-12px); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
32.8333% { opacity: 1; transform: translateX(0); }
34% { opacity: 1; transform: translateX(0); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
36.5%, 100% { opacity: 0; transform: translateX(16px); }
}
@keyframes bar-F {
0%, 53% { opacity: 0; transform: translateX(-12px); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
55.1667% { opacity: 1; transform: translateX(0); }
56.3333% { opacity: 1; transform: translateX(0); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
58.8333%, 100% { opacity: 0; transform: translateX(16px); }
}
@keyframes bar-G {
0%, 65% { opacity: 0; transform: translateX(-12px); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
67.1667% { opacity: 1; transform: translateX(0); }
68.3333% { opacity: 1; transform: translateX(0); animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); }
70.8333%, 100% { opacity: 0; transform: translateX(16px); }
}
.b-AB{animation-name:base-AB}.c-AB{animation-name:clay-AB}.o-AB{animation-name:olive-AB}
.b-AC{animation-name:base-AC}.c-AC{animation-name:clay-AC}.o-AC{animation-name:olive-AC}
.b-BD{animation-name:base-BD}.c-BD{animation-name:clay-BD}.o-BD{animation-name:olive-BD}
.b-BE{animation-name:base-BE}.c-BE{animation-name:clay-BE}.o-BE{animation-name:olive-BE}
.b-CF{animation-name:base-CF}.c-CF{animation-name:clay-CF}.o-CF{animation-name:olive-CF}
.b-CG{animation-name:base-CG}.c-CG{animation-name:clay-CG}.o-CG{animation-name:olive-CG}
.n-A{animation-name:node-A}.l-A{animation-name:label-A}.s-A{animation-name:bar-A}
.n-B{animation-name:node-B}.l-B{animation-name:label-B}.s-B{animation-name:bar-B}
.n-C{animation-name:node-C}.l-C{animation-name:label-C}.s-C{animation-name:bar-C}
.n-D{animation-name:node-D}.l-D{animation-name:label-D}.s-D{animation-name:bar-D}
.n-E{animation-name:node-E}.l-E{animation-name:label-E}.s-E{animation-name:bar-E}
.n-F{animation-name:node-F}.l-F{animation-name:label-F}.s-F{animation-name:bar-F}
.n-G{animation-name:node-G}.l-G{animation-name:label-G}.s-G{animation-name:bar-G}
/* Reduced motion: no travel, no redraw. The diagram simply arrives at
the state the traversal would have left behind — every subtree
resolved, the stack empty, the visitor gone home. */
@media (prefers-reduced-motion: reduce) {
.cyc { animation: none !important; }
.base { opacity: 1; }
.clay { stroke-dashoffset: 100; }
.olive { stroke-dashoffset: 0; }
.disc { opacity: 1; fill: var(--olive); stroke: var(--olive); stroke-width: 2.5; transform: none; }
.glyph { opacity: 1; fill: var(--ivory); }
.frame { opacity: 0; }
.walker { opacity: 0; }
.stage {
opacity: 1;
animation: settle 500ms var(--ease-out) both !important;
}
}
@keyframes settle {
from { opacity: 0; }
to { opacity: 1; }
}
/* === /FIGURE === */
.caption {
display: flex;
gap: 10px;
align-items: baseline;
margin-top: 14px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.06em;
color: var(--gray-500);
text-transform: uppercase;
}
.caption b { color: var(--ink); font-weight: 500; }
</style>
</head>
<body>
<div class="stage-wrap">
<div class="card">
<svg viewBox="0 0 480 360" xmlns="http://www.w3.org/2000/svg" role="img"
aria-label="A seven-node tree traversed depth-first. A visitor descends from the root, marking each node as it is placed on the call stack, and turns each subtree resolved as it backtracks. A call-stack panel pushes and pops alongside.">
<!-- static chrome: never animates, so the dissolve leaves a frame behind -->
<text class="eyebrow" x="16" y="26">depth-first</text>
<text class="eyebrow" x="378" y="204">call stack</text>
<rect class="slot" x="378" y="280" width="84" height="24" rx="5" />
<rect class="slot" x="378" y="250" width="84" height="24" rx="5" />
<rect class="slot" x="378" y="220" width="84" height="24" rx="5" />
<g class="legend">
<circle class="legend-dot" cx="18" cy="336" fill="#D97757" />
<text class="legend-label" x="29" y="336">on the stack</text>
<circle class="legend-dot" cx="127" cy="336" fill="#B04A3F" />
<text class="legend-label" x="138" y="336">backtracking</text>
<circle class="legend-dot" cx="236" cy="336" fill="#788C5D" />
<text class="legend-label" x="247" y="336">subtree resolved</text>
</g>
<!-- everything below shares the 12s clock and dissolves together -->
<g class="cyc stage">
<path class="cyc base b-AB" d="M185 66 L100 165" />
<path class="cyc trace clay c-AB" pathLength="100" d="M185 66 L100 165" />
<path class="cyc trace olive o-AB" pathLength="100" d="M100 165 L185 66" />
<path class="cyc base b-AC" d="M185 66 L270 165" />
<path class="cyc trace clay c-AC" pathLength="100" d="M185 66 L270 165" />
<path class="cyc trace olive o-AC" pathLength="100" d="M270 165 L185 66" />
<path class="cyc base b-BD" d="M100 165 L48 264" />
<path class="cyc trace clay c-BD" pathLength="100" d="M100 165 L48 264" />
<path class="cyc trace olive o-BD" pathLength="100" d="M48 264 L100 165" />
<path class="cyc base b-BE" d="M100 165 L140 264" />
<path class="cyc trace clay c-BE" pathLength="100" d="M100 165 L140 264" />
<path class="cyc trace olive o-BE" pathLength="100" d="M140 264 L100 165" />
<path class="cyc base b-CF" d="M270 165 L225 264" />
<path class="cyc trace clay c-CF" pathLength="100" d="M270 165 L225 264" />
<path class="cyc trace olive o-CF" pathLength="100" d="M225 264 L270 165" />
<path class="cyc base b-CG" d="M270 165 L315 264" />
<path class="cyc trace clay c-CG" pathLength="100" d="M270 165 L315 264" />
<path class="cyc trace olive o-CG" pathLength="100" d="M315 264 L270 165" />
<circle class="cyc disc n-A" cx="185" cy="66" r="17" />
<text class="cyc glyph l-A" x="185" y="66">A</text>
<circle class="cyc disc n-B" cx="100" cy="165" r="17" />
<text class="cyc glyph l-B" x="100" y="165">B</text>
<circle class="cyc disc n-C" cx="270" cy="165" r="17" />
<text class="cyc glyph l-C" x="270" y="165">C</text>
<circle class="cyc disc n-D" cx="48" cy="264" r="17" />
<text class="cyc glyph l-D" x="48" y="264">D</text>
<circle class="cyc disc n-E" cx="140" cy="264" r="17" />
<text class="cyc glyph l-E" x="140" y="264">E</text>
<circle class="cyc disc n-F" cx="225" cy="264" r="17" />
<text class="cyc glyph l-F" x="225" y="264">F</text>
<circle class="cyc disc n-G" cx="315" cy="264" r="17" />
<text class="cyc glyph l-G" x="315" y="264">G</text>
<g class="cyc walker">
<g class="cyc hue">
<circle class="halo" r="27" />
<circle class="ring" r="24" />
</g>
</g>
<g class="cyc frame s-A">
<rect x="378" y="280" width="84" height="24" rx="5" />
<text class="fglyph" x="390" y="292">A</text>
<text class="fdepth" x="450" y="292">0</text>
</g>
<g class="cyc frame s-B">
<rect x="378" y="250" width="84" height="24" rx="5" />
<text class="fglyph" x="390" y="262">B</text>
<text class="fdepth" x="450" y="262">1</text>
</g>
<g class="cyc frame s-C">
<rect x="378" y="250" width="84" height="24" rx="5" />
<text class="fglyph" x="390" y="262">C</text>
<text class="fdepth" x="450" y="262">1</text>
</g>
<g class="cyc frame s-D">
<rect x="378" y="220" width="84" height="24" rx="5" />
<text class="fglyph" x="390" y="232">D</text>
<text class="fdepth" x="450" y="232">2</text>
</g>
<g class="cyc frame s-E">
<rect x="378" y="220" width="84" height="24" rx="5" />
<text class="fglyph" x="390" y="232">E</text>
<text class="fdepth" x="450" y="232">2</text>
</g>
<g class="cyc frame s-F">
<rect x="378" y="220" width="84" height="24" rx="5" />
<text class="fglyph" x="390" y="232">F</text>
<text class="fdepth" x="450" y="232">2</text>
</g>
<g class="cyc frame s-G">
<rect x="378" y="220" width="84" height="24" rx="5" />
<text class="fglyph" x="390" y="232">G</text>
<text class="fdepth" x="450" y="232">2</text>
</g>
</g>
</svg>
</div>
<div class="caption"><b>visit</b> · descend · <b>return</b> · unwind</div>
</div>
</body>
</html>