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;

    /* Page padding scales with the frame, so the chrome the card has to
       budget for is derived from it rather than guessed. --chrome is the
       vertical room the page chrome needs: two paddings plus the caption
       (8px margin + 14px line + 4px slack). The card is aspect-locked, so
       its width is capped by the height actually available or the 4:3 box
       grows taller than the viewport and the caption falls off. */
    --pad: clamp(5px, 1.6vw, 28px);
    --chrome: calc(2 * var(--pad) + 26px);
  }

  * { 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: var(--pad);
    -webkit-font-smoothing: antialiased;
  }

  /* 480px is the viewBox width, so the widest the figure ever draws is
     1 user unit = 1 CSS px. Every declared SVG font-size is therefore also
     its largest rendered size, and the smallest stage (a 343x257 card,
     which lands near 0.61) sets the floor. */
  .stage-wrap {
    width: min(100%, 480px, calc((100vh - var(--chrome)) * 4 / 3));
  }

  /* === 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;
  }
  /* SVG type is sized in user units; rendered px = declared x (card width
     / 480). The card never draws wider than 480 and never narrower than
     ~294 (the 343x257 stage), so the scale range is ~0.61 to 1.0 and
     18.5u is the smallest declaration that still clears 11px at 0.61. */
  .glyph {
    font-family: var(--mono);
    font-size: 19px;
    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: 18.5px; font-weight: 600;
    fill: var(--clay); dominant-baseline: central;
  }
  .fdepth {
    font-family: var(--mono); font-size: 18.5px;
    fill: var(--gray-500); text-anchor: end; dominant-baseline: central;
  }

  /* --- static chrome ------------------------------------------------ */
  .eyebrow {
    font-family: var(--mono); font-size: 18.5px; letter-spacing: 0.1em;
    fill: var(--gray-500); text-transform: uppercase;
  }
  /* "call stack" is 10 chars ~ 130u at this size; left of x=378 it would
     run past the 480u edge, so the label starts left of its slots. */
  .eyebrow-stack { transform: translate(-50px, 0); }
  .legend-label {
    font-family: var(--mono); font-size: 18.5px; letter-spacing: 0.04em;
    fill: var(--gray-500); dominant-baseline: central;
  }
  .legend-dot { r: 5; }
  /* Three chips side by side need ~500u at this size, so the legend is a
     single column at every width: dots at x=18, rows 24u apart, the top
     row clear of the bottom node band (which ends at y=282). */
  .lgi-1 { transform: translate(0, -38px); }
  .lgi-2 { transform: translate(-109px, -14px); }
  .lgi-3 { transform: translate(-218px, 10px); }

  @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, no rewind. The diagram holds
     the state the traversal leaves behind — every subtree resolved, the
     stack empty, the visitor gone home — and the visit ORDER is told in
     colour instead of movement: a slow depth-first pulse walks
     A → B → D → E → C → F → G, each node warming from olive back to clay
     for a beat before settling again, one step every 1.2s on an 8.4s
     loop. Nothing moves, nothing flashes, and because each node begins
     and ends the loop on its resolved olive the wrap is invisible. */
  @media (prefers-reduced-motion: reduce) {
    /* the blanket stop skips the node discs — they carry the reduced pulse */
    .cyc:not(.disc) { 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;
    }

    /* olive → clay → olive; ~2s of change inside an 8.4s loop */
    @keyframes visit-reduced {
      0%   { fill: #788C5D; stroke: #788C5D; opacity: 1; }
      6%   { fill: #D97757; stroke: #D97757; opacity: 0.6; }
      14%  { fill: #D97757; stroke: #D97757; opacity: 1; }
      26%  { fill: #788C5D; stroke: #788C5D; opacity: 1; }
      100% { fill: #788C5D; stroke: #788C5D; opacity: 1; }
    }

    .disc { animation: visit-reduced 8.4s linear infinite; }
    /* negative delays: every node is already mid-loop on arrival, and the
       offsets spell out the depth-first visit order */
    .n-A { animation-delay: -8.4s; }
    .n-B { animation-delay: -7.2s; }
    .n-D { animation-delay: -6s; }
    .n-E { animation-delay: -4.8s; }
    .n-C { animation-delay: -3.6s; }
    .n-F { animation-delay: -2.4s; }
    .n-G { animation-delay: -1.2s; }
  }

  @keyframes settle {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* === /FIGURE === */

  .caption {
    display: flex;
    gap: 10px;
    align-items: baseline;
    margin-top: 8px;
    font-family: var(--mono);
    font-size: 11px;
    line-height: 14px;
    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 eyebrow-stack" 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">
          <g class="lgi lgi-1">
            <circle class="legend-dot" cx="18" cy="336" fill="#D97757" />
            <text class="legend-label" x="29" y="336">on the stack</text>
          </g>
          <g class="lgi lgi-2">
            <circle class="legend-dot" cx="127" cy="336" fill="#B04A3F" />
            <text class="legend-label" x="138" y="336">backtracking</text>
          </g>
          <g class="lgi lgi-3">
            <circle class="legend-dot" cx="236" cy="336" fill="#788C5D" />
            <text class="legend-label" x="247" y="336">subtree resolved</text>
          </g>
        </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>