Back to the library

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.

conceptrecursiontreetraversalalgorithmsvg

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>