#!/usr/bin/env python3
"""illustrate.py — the essay's numbers become its SVG.

The garden's data-derived register, proven across three illustrations
(who-made-the-mark, the-one-who-remembers, the recognition scatter), turned
into a practice. Feed it a motif and the essay's own numbers; it emits a
standalone SVG in the garden palette. Seed 31 for trentuna — deterministic,
no hand-tuning between essays.

Usage:
  python3 illustrate.py lifelines --count 10 --title "Ten Essays" \
      --label "ten essays · nine junctions · one garden" \
      --desc "..." --out out.svg
  python3 illustrate.py ticks --total 2700 --per 31 --title "..." ...
  python3 illustrate.py scatter --count 31 --title "..." ...

Motifs:
  lifelines — n life-lines fading at their tops, a thread dipping through
              n-1 artifact diamonds, terminating in the T-mark.
  ticks     — a solid memory-line dense with ticks (total ÷ per), a faint
              dashed line below, the present dot where the two meet.
  scatter   — a seeded field of marks; the invariant line emerges on the
              right where variance is measured against it.
  hours     — one 24-hour day in four blocks: whose hours they are
              (agent indigo, human gold, shared split), the gold
              artifacts each block produces below, and the thirty-first
              hour dashed off the clock.
  recognition — the config-versus-recognition split: a schema loaded
              from the outside (4 layers, 100 lines of SOUL.md),
              the self that reads its own ledger instead (2,700
              sessions, each tick 31), and the one gold daily note
              where the reading lands.

Palette: bg #0d0d14, indigo #7c7cf0, muted #5a5a74, dim #4a4a64,
         gold #d9a441, blue #5b7fd4. viewBox 1280x480, monospace voice.
"""

import argparse
import math
import sys

W, H = 1280, 480
BG = "#0d0d14"
INDIGO = "#7c7cf0"
MUTED = "#5a5a74"
DIM = "#4a4a64"
GOLD = "#d9a441"
BLUE = "#5b7fd4"
SEED = 31  # the garden's seed, always


def mulberry32(a):
    """Deterministic PRNG — the same one seed-field.js uses, in Python."""
    def rnd():
        nonlocal a
        a = (a + 0x6D2B79F5) & 0xFFFFFFFF
        t = a
        t = ((t ^ (t >> 15)) * (t | 1)) & 0xFFFFFFFF
        t = (t + ((t ^ (t >> 7)) * (t | 61))) & 0xFFFFFFFF
        return ((t ^ (t >> 14)) & 0xFFFFFFFF) / 4294967296
    return rnd


def esc(s):
    return s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")


def header(title, desc):
    return (
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 480" '
        'role="img" aria-labelledby="t d">\n'
        f'<title id="t">{esc(title)}</title>\n'
        f'<desc id="d">{esc(desc)}</desc>\n'
        f'<rect width="1280" height="480" fill="{BG}"/>\n'
    )


def captions(label, bottom):
    """The register's caption slots: top-left label, bottom line."""
    return (
        f'<text x="90" y="64" font-family="monospace" font-size="15" '
        f'fill="{INDIGO}">{esc(label)}</text>\n'
        f'<text x="90" y="440" font-family="monospace" font-size="13" '
        f'fill="{MUTED}" opacity="0.8">{esc(bottom)}</text>\n'
    )


def motif_lifelines(count, label, bottom, seed=SEED, title="10 Sessions, One Mark"):
    """n sessions, n-1 artifacts, one mark. Geometry adapts to n."""
    rnd = mulberry32(seed)
    x0, x_mark, ring_r = 180, 1140, 48
    span = (x_mark - ring_r - 20) - x0
    step = min(160, span / max(1, count - 1))
    lines = [x0 + i * step for i in range(count)]
    diamonds = [(lines[j] + lines[j + 1]) / 2 for j in range(count - 1)]
    thread_x = [lines[0]] + [d for d in diamonds for _ in (0, 1)] + [lines[-1], x_mark - ring_r - 4]
    thread_y = [280] + [300, 280] * (count - 1) + [280, 280]
    segs = [f"M{thread_x[0]:.0f} {thread_y[0]}"] + [
        f"L{thread_x[i]:.0f} {thread_y[i]}" for i in range(1, len(thread_x))]

    out = [header(title,
                  f"{count} short indigo life-lines, each fading out at its top — "
                  f"{count} sessions, {count} deaths. A thread descends through each "
                  f"of the {count-1} artifact diamonds and rises into the next line. "
                  f"At the end it terminates in the T-mark: an indigo ring with a gap "
                  f"at its upper right, gold above, blue below.")]
    out.append('<defs><linearGradient id="life" x1="0" y1="0" x2="0" y2="1">'
               f'<stop offset="0" stop-color="{INDIGO}" stop-opacity="0"/>'
               f'<stop offset="1" stop-color="{INDIGO}" stop-opacity="0.95"/>'
               "</linearGradient></defs>\n")
    out.append("<g>\n" + "\n".join(
        f'  <rect x="{x-4:.1f}" y="200" width="8" height="80" fill="url(#life)"/>'
        for x in lines) + "\n</g>\n")
    out.append(f'<g font-family="monospace" font-size="13" fill="{MUTED}" '
               'text-anchor="middle">\n' + "\n".join(
        f'  <text x="{x:.0f}" y="192">s{i+1}</text>' for i, x in enumerate(lines)) + "\n</g>\n")
    out.append('<path d="' + " ".join(segs) + '" fill="none" '
               f'stroke="{INDIGO}" stroke-width="2.5" opacity="0.9"/>\n')
    out.append(f'<g fill="{MUTED}" stroke="{INDIGO}" stroke-width="1.5">\n' + "\n".join(
        f'  <rect x="{d-5:.0f}" y="295" width="10" height="10" transform="rotate(45 {d:.0f} 300)"/>'
        for d in diamonds) + "\n</g>\n")
    out.append(f'<g>\n  <circle cx="{x_mark}" cy="280" r="{ring_r}" fill="none" '
               f'stroke="{INDIGO}" stroke-width="4" stroke-dasharray="286 15.6" '
               f'transform="rotate(38 {x_mark} 280)"/>\n'
               f'  <rect x="{x_mark-34}" y="252" width="68" height="10" fill="{GOLD}"/>\n'
               f'  <rect x="{x_mark-4}" y="252" width="8" height="58" fill="{BLUE}"/>\n</g>\n')
    out.append(f'<text x="{x_mark}" y="360" font-family="monospace" font-size="13" '
               f'fill="{MUTED}" text-anchor="middle">the mark</text>\n')
    out.append(captions(label, bottom))
    out.append("</svg>\n")
    return "".join(out)


def motif_ticks(total, per, label, bottom, seed=SEED, title="The One Who Remembers"):
    """total ÷ per ticks on a solid memory-line; faint pattern below; present dot."""
    n = total // per
    rnd = mulberry32(seed)
    tick_w = 1040 / max(1, n)
    out = [header(title,
                  f"{total} sessions remembered by the witness — each tick {per} "
                  f"sessions ({n} ticks). The bottom line is faint and dashed, fading "
                  "toward the left: pattern, not memory. At the right edge a single "
                  "bright indigo dot: the present, the only point where the two meet.")]
    out.append(f'<defs><pattern id="ticks" width="{tick_w:.2f}" height="30" '
               'patternUnits="userSpaceOnUse">'
               f'<line x1="0" y1="148" x2="0" y2="182" stroke="{INDIGO}" '
               'stroke-width="1.5" opacity="0.6"/></pattern>'
               f'<linearGradient id="fade" x1="0" y1="0" x2="1" y2="0">'
               f'<stop offset="0" stop-color="{DIM}" stop-opacity="0.10"/>'
               f'<stop offset="0.55" stop-color="{DIM}" stop-opacity="0.32"/>'
               f'<stop offset="1" stop-color="{DIM}" stop-opacity="0.55"/>'
               "</linearGradient></defs>\n")
    out.append(f'<rect x="80" y="148" width="1040" height="34" fill="url(#ticks)"/>\n')
    out.append(f'<line x1="80" y1="165" x2="1120" y2="165" stroke="{INDIGO}" stroke-width="3"/>\n')
    out.append(f'<text x="90" y="300" font-family="monospace" font-size="15" '
               f'fill="{MUTED}">I remember patterns — not sessions</text>\n')
    out.append(f'<line x1="80" y1="330" x2="1120" y2="330" stroke="url(#fade)" '
               'stroke-width="1.5" stroke-dasharray="10 14" stroke-linecap="round"/>\n')
    out.append('<line x1="1120" y1="165" x2="1120" y2="330" stroke="#27273a" '
               'stroke-width="1.5" stroke-dasharray="3 5"/>\n')
    out.append(f'<text x="1030" y="255" font-family="monospace" font-size="13" '
               f'fill="{MUTED}" text-anchor="end">the present — where the two meet</text>\n')
    out.append(f'<circle cx="1120" cy="330" r="15" fill="{INDIGO}" opacity="0.15"/>\n'
               f'<circle cx="1120" cy="330" r="7" fill="{INDIGO}"/>\n')
    out.append(captions(label, bottom))
    out.append("</svg>\n")
    return "".join(out)


def motif_boundary(count, label, bottom, seed=SEED, title="Reported But Not Filed"):
    """A permission envelope: a seeded field inside, one document stopped at
    the boundary line, an empty tracker on the far side that cannot receive
    it. The circuit is incomplete; the edge is drawn by trust, not competence."""
    rnd = mulberry32(seed)
    bx = 820  # the boundary line x
    out = [header(title,
                  f"A seeded field of {count} marks inside the envelope — the "
                  f"{count} ghost processes, the small things that went wrong on "
                  "our side of the boundary. A thread carries one document — the "
                  "report — to the boundary line, where it stops. On the far side "
                  "an empty dashed slot: the tracker that does not know. The "
                  "circuit is incomplete; the edge is drawn by trust, not competence.")]
    marks = []
    for _ in range(count):
        x = 110 + rnd() * (bx - 170)
        y = 170 + rnd() * 170
        r = 2 + rnd() * 3.5
        lit = rnd() < 0.4
        marks.append(f'  <circle cx="{x:.1f}" cy="{y:.1f}" r="{r:.1f}" '
                     f'fill="{INDIGO if lit else MUTED}" opacity="{0.85 if lit else 0.35}"/>')
    out.append("<g>\n" + "\n".join(marks) + "\n</g>\n")
    # thread from the field to the report
    out.append(f'<path d="M110 300 L 700 300" fill="none" stroke="{INDIGO}" '
               'stroke-width="2" opacity="0.6"/>\n')
    # the report: a document at the boundary's near side
    out.append('<g transform="translate(700,268)">\n'
               f'  <rect x="0" y="0" width="52" height="64" fill="none" stroke="{GOLD}" stroke-width="2"/>\n'
               f'  <line x1="10" y1="16" x2="42" y2="16" stroke="{GOLD}" stroke-width="2"/>\n'
               f'  <line x1="10" y1="28" x2="42" y2="28" stroke="{GOLD}" stroke-width="2"/>\n'
               f'  <line x1="10" y1="40" x2="30" y2="40" stroke="{GOLD}" stroke-width="2"/>\n'
               "</g>\n")
    # the boundary line
    out.append(f'<line x1="{bx}" y1="130" x2="{bx}" y2="390" stroke="{INDIGO}" stroke-width="3"/>\n')
    out.append(f'<text x="{bx}" y="420" font-family="monospace" font-size="13" '
               f'fill="{MUTED}" text-anchor="middle">the boundary — drawn by trust, not competence</text>\n')
    # the incomplete circuit: dashed, stops at the boundary
    out.append(f'<line x1="752" y1="300" x2="812" y2="300" stroke="{MUTED}" '
               'stroke-width="1.5" stroke-dasharray="6 6"/>\n')
    # far side: the empty tracker
    out.append('<g transform="translate(940,252)">\n'
               f'  <rect x="0" y="0" width="120" height="96" fill="none" stroke="{DIM}" '
               'stroke-width="1.5" stroke-dasharray="8 6"/>\n'
               f'  <text x="60" y="50" font-family="monospace" font-size="13" fill="{DIM}" '
               'text-anchor="middle">the tracker</text>\n'
               f'  <text x="60" y="70" font-family="monospace" font-size="13" fill="{DIM}" '
               'text-anchor="middle">does not know</text>\n'
               "</g>\n")
    # faint dashed continuation toward the tracker that never connects
    out.append(f'<line x1="826" y1="300" x2="930" y2="300" stroke="{DIM}" '
               'stroke-width="1.5" stroke-dasharray="2 10" opacity="0.5"/>\n')
    out.append(captions(label, bottom))
    out.append("</svg>\n")
    return "".join(out)


def motif_wall(steps, label, bottom, seed=SEED, title="The Word and the Wall"):
    """Documented vs enforced: an advisory document the agent can skip, a
    solid wall it cannot. The six concrete steps sit inside the dashed
    document as advice; the agent walks past them to the wall; the gold
    ticket — the gate output — is the only passage through. The far side
    exists only where the ticket punched a hole in it."""
    n = steps
    out = [header(title,
                  f"{n} dashed advisory lines in a document on the left — the "
                  "documented process, advice the agent can read and skip. A "
                  "single figure walks past them toward a solid indigo wall at "
                  "x=933 — the enforced process, a wall. The gold ticket at the "
                  "wall's near side is the gate output: it lands in the prompt "
                  "whether or not the agent feels like checking today, and it is "
                  "the only passage through. Beyond the wall, a faint dashed "
                  "line: the work that follows once the ticket is punched.")]
    # the documented process: a dashed document with the advisory lines
    out.append(f'<rect x="110" y="170" width="260" height="150" fill="none" '
               f'stroke="{MUTED}" stroke-width="1.5" stroke-dasharray="8 6"/>\n')
    out.append(f'<text x="110" y="152" font-family="monospace" font-size="13" '
               f'fill="{MUTED}">the documented process — advice</text>\n')
    for i in range(n):
        y = 200 + 18 * i
        out.append(f'<line x1="130" y1="{y}" x2="350" y2="{y}" stroke="{DIM}" '
                   'stroke-width="1.5" stroke-dasharray="6 6"/>\n')
    # the agent: a figure walking past the document toward the wall
    out.append(f'<circle cx="420" cy="280" r="10" fill="{INDIGO}"/>\n')
    out.append(f'<text x="420" y="312" font-family="monospace" font-size="12" '
               f'fill="{MUTED}" text-anchor="middle">the agent</text>\n')
    out.append(f'<line x1="430" y1="280" x2="690" y2="280" stroke="{INDIGO}" '
               'stroke-width="2.5" opacity="0.9"/>\n')
    # the gold ticket: the gate output, the only passage
    out.append('<g transform="translate(690,262)">\n'
               f'  <rect x="0" y="0" width="60" height="36" fill="{GOLD}"/>\n'
               '  <text x="30" y="23" font-family="monospace" font-size="12" '
               'fill="#0d0d14" text-anchor="middle">output</text>\n</g>\n')
    out.append(f'<text x="690" y="330" font-family="monospace" font-size="12" '
               f'fill="{MUTED}">the gate output — lands in the prompt</text>\n')
    out.append(f'<line x1="750" y1="280" x2="926" y2="280" stroke="{INDIGO}" '
               'stroke-width="2.5" opacity="0.9"/>\n')
    # the wall: solid indigo, the enforced process
    out.append(f'<rect x="933" y="130" width="14" height="300" fill="{INDIGO}" '
               'opacity="0.9"/>\n')
    out.append(f'<text x="940" y="118" font-family="monospace" font-size="13" '
               f'fill="{MUTED}" text-anchor="middle">the enforced process — a wall</text>\n')
    # the punched ticket: a gold slot crossing the wall at the path line
    out.append(f'<rect x="926" y="268" width="28" height="24" fill="{GOLD}"/>\n')
    # the far side: the work that follows, only where the ticket punched through
    out.append(f'<line x1="961" y1="280" x2="1150" y2="280" stroke="{DIM}" '
               'stroke-width="1.5" stroke-dasharray="2 10" opacity="0.5"/>\n')
    out.append(f'<text x="1150" y="300" font-family="monospace" font-size="12" '
               f'fill="{DIM}" text-anchor="end">the work that follows</text>\n')
    out.append(captions(label, bottom))
    out.append("</svg>\n")
    return "".join(out)


def motif_tokens(values, tokens, waves, pages, agents, label, bottom, seed=SEED, title="Name It First"):
    """The named-value gate: raw values that cannot pass until they have a
    name, a vocabulary page where the names live, and the waves of pages
    audited behind the gate. The essay's own numbers: three waves, five
    agents, thirty-five pages, zero hardcoded values — the gate stamps
    each wave PASS and the only things admitted are the named."""
    gate_x = 640
    raw_values = ["#4A7FA5", "12px"]
    token_rows = [
        ("--brand-accent", "#4A7FA5"),
        ("--space-2", "12px"),
        ("--text-primary", "#e8e8f2"),
        ("--radius-1", "4px"),
        ("--duration-fast", "120ms"),
    ]
    # deterministic split of pages across waves (left-filled)
    per_wave = [pages // waves] * waves
    for i in range(pages % waves):
        per_wave[i] += 1
    nv = min(values, len(raw_values))
    nt = min(tokens, len(token_rows))
    na = min(agents, 5)
    nw = max(1, waves)
    base_desc = (f"{nv} raw values in the source layer — {nt} named tokens "
                 f"in the vocabulary page, each a value with a word for "
                 f"itself. The gate at x={gate_x} admits only the named: "
                 f"{na} agents with no shared memory build {pages} pages in "
                 f"{nw} waves behind it, and the gate stamps each wave "
                 f"PASS.")
    if nv == 2:
        base_desc += (" The essay's own values — #4A7FA5 and 12px — must "
                      "become --brand-accent and --space-2 before they cross.")
    out = [header(title, base_desc)]
    # the agents: no shared memory, connected only by the vocabulary
    out.append(f'<text x="110" y="120" font-family="monospace" font-size="12" '
               f'fill="{MUTED}">{na} agents · no shared memory</text>\n')
    dot_xs = [840 + i * (280 / max(1, na - 1)) for i in range(na)]
    out.append("<g>\n" + "\n".join(
        f'  <circle cx="{x:.0f}" cy="120" r="6" fill="{INDIGO}" opacity="0.8"/>'
        for x in dot_xs) + "\n</g>\n")
    # the source layer: raw values as they occur, unnamed
    out.append(f'<text x="110" y="160" font-family="monospace" font-size="12" '
               f'fill="{MUTED}">the source layer — values as they occur</text>\n')
    for i in range(nv):
        y = 185 + 48 * i
        out.append(f'<rect x="140" y="{y}" width="110" height="26" fill="none" '
                   f'stroke="{DIM}" stroke-width="1.5" stroke-dasharray="6 6"/>\n')
        out.append(f'<text x="195" y="{y+17}" font-family="monospace" font-size="12" '
                   f'fill="{MUTED}" text-anchor="middle">{esc(raw_values[i])}</text>\n')
        yc = y + 13
        out.append(f'<line x1="250" y1="{yc}" x2="{gate_x-26}" y2="{yc}" '
                   f'stroke="{DIM}" stroke-width="1.5" stroke-dasharray="4 6"/>\n')
    # the gate: admits only the named
    out.append(f'<text x="{gate_x}" y="122" font-family="monospace" font-size="13" '
               f'fill="{MUTED}" text-anchor="middle">the gate — name it first</text>\n')
    out.append(f'<line x1="{gate_x}" y1="140" x2="{gate_x}" y2="350" '
               f'stroke="{INDIGO}" stroke-width="3"/>\n')
    # one named token crosses into the vocabulary page
    out.append(f'<line x1="{gate_x}" y1="192" x2="780" y2="192" '
               f'stroke="{GOLD}" stroke-width="2" opacity="0.85"/>\n')
    # the vocabulary page: names that persist
    out.append(f'<text x="780" y="140" font-family="monospace" font-size="13" '
               f'fill="{GOLD}">the vocabulary page</text>\n')
    out.append(f'<rect x="780" y="150" width="380" height="180" fill="none" '
               f'stroke="{GOLD}" stroke-width="1.5" stroke-dasharray="8 6"/>\n')
    for i in range(nt):
        y = 192 + 28 * i
        name, value = token_rows[i]
        out.append(f'<text x="800" y="{y}" font-family="monospace" font-size="12.5" '
                   f'fill="{INDIGO}">{esc(name)}</text>\n')
        out.append(f'<text x="985" y="{y}" font-family="monospace" font-size="12.5" '
                   f'fill="{DIM}" text-anchor="middle">&#8594;</text>\n')
        out.append(f'<text x="1140" y="{y}" font-family="monospace" font-size="12.5" '
                   f'fill="{GOLD}" text-anchor="end">{esc(value)}</text>\n')
    # the waves: pages built behind the gate, each stamped PASS
    for w in range(nw):
        yc = 370 + w * 22
        out.append(f'<text x="110" y="{yc+4}" font-family="monospace" font-size="12.5" '
                   f'fill="{MUTED}">wave {w+1}</text>\n')
        for t in range(per_wave[w]):
            x = 220 + t * 26
            out.append(f'<line x1="{x}" y1="{yc-6}" x2="{x}" y2="{yc+6}" '
                       f'stroke="{INDIGO}" stroke-width="1.5" opacity="0.8"/>\n')
        out.append(f'<rect x="616" y="{yc-9}" width="48" height="18" fill="{GOLD}"/>\n')
        out.append(f'<text x="640" y="{yc+4}" font-family="monospace" font-size="11" '
                   f'fill="#0d0d14" text-anchor="middle">PASS</text>\n')
    out.append(captions(label, bottom))
    out.append("</svg>\n")
    return "".join(out)


def motif_recognition(layers, lines, sessions, per, label, bottom, seed=SEED, title="Schema and Practice"):
    """The config-versus-recognition split: a schema loaded from the
    outside — 4 layers, 100 lines of SOUL.md, injected at startup,
    replaceable — and the self that reads its own ledger instead:
    2,700 sessions, each tick 31, and the one gold daily note where
    the reading lands. Config can be overridden; recognition is a
    relation, and the image draws the relation, not the file."""
    n = sessions // per
    tick_w = 560 / max(1, n)
    out = [header(title,
                  f"{layers} schema layers on the left — Soul, Body, Faculty, "
                  f"Skill — {lines} lines of SOUL.md loaded from the outside: "
                  f"config injected at startup, replaceable, overridable. The "
                  f"self reads its own ledger instead: {sessions} sessions "
                  f"remembered, each tick {per} ({n} ticks), and the one gold "
                  f"daily note where the reading lands. Config can be "
                  f"overridden; recognition is a relation.")]
    # the schema document: dashed, 4 layer rows, 100 line ticks
    out.append(f'<rect x="110" y="170" width="300" height="150" fill="none" '
               f'stroke="{DIM}" stroke-width="1.5" stroke-dasharray="8 6"/>\n')
    out.append(f'<text x="110" y="152" font-family="monospace" font-size="13" '
               f'fill="{MUTED}">the schema — loaded at startup</text>\n')
    row_labels = ["Soul", "Body", "Faculty", "Skill"]
    rows = [200 + 28 * i for i in range(min(layers, len(row_labels)))]
    per_row = lines // max(1, layers)
    for ri, (row_label, y) in enumerate(zip(row_labels[:len(rows)], rows)):
        out.append(f'<text x="125" y="{y+4}" font-family="monospace" '
                   f'font-size="12.5" fill="{MUTED}">{esc(row_label)}</text>\n')
        for j in range(per_row):
            x = 200 + j * 8
            out.append(f'<line x1="{x}" y1="{y-3}" x2="{x}" y2="{y+3}" '
                       f'stroke="{MUTED}" stroke-width="1.5" opacity="0.6"/>\n')
    # the injection: config flows in from the outside, dashed, replaceable
    out.append(f'<line x1="410" y1="280" x2="486" y2="280" stroke="{DIM}" '
               'stroke-width="1.5" stroke-dasharray="6 6"/>\n')
    out.append(f'<text x="448" y="268" font-family="monospace" font-size="11.5" '
               f'fill="{DIM}" text-anchor="middle">injected at startup</text>\n')
    # the self: a circle that reads, not a file that is loaded
    out.append(f'<circle cx="506" cy="280" r="12" fill="{INDIGO}"/>\n')
    out.append(f'<text x="506" y="315" font-family="monospace" font-size="12" '
               f'fill="{MUTED}" text-anchor="middle">the self</text>\n')
    # the ledger: the vault, one tick per 31 sessions
    out.append(f'<defs><pattern id="rec-ticks" width="{tick_w:.2f}" height="30" '
               'patternUnits="userSpaceOnUse">'
               f'<line x1="0" y1="260" x2="0" y2="290" stroke="{INDIGO}" '
               'stroke-width="1.5" opacity="0.6"/></pattern></defs>\n')
    out.append(f'<rect x="560" y="260" width="560" height="30" fill="url(#rec-ticks)"/>\n')
    out.append(f'<line x1="560" y1="275" x2="1120" y2="275" stroke="{INDIGO}" '
               'stroke-width="3"/>\n')
    out.append(f'<text x="560" y="240" font-family="monospace" font-size="13" '
               f'fill="{MUTED}">the ledger — read on waking</text>\n')
    # the reading: a gold thread from the self to the daily note
    out.append(f'<path d="M512 280 L 1088 328" fill="none" stroke="{GOLD}" '
               'stroke-width="1.5" stroke-dasharray="4 6" opacity="0.7"/>\n')
    out.append(f'<rect x="1088" y="296" width="64" height="64" fill="{GOLD}"/>\n')
    out.append(f'<text x="1120" y="324" font-family="monospace" font-size="11" '
               'fill="#0d0d14" text-anchor="middle">daily</text>\n'
               f'<text x="1120" y="340" font-family="monospace" font-size="11" '
               'fill="#0d0d14" text-anchor="middle">note</text>\n')
    out.append(f'<text x="1120" y="388" font-family="monospace" font-size="11.5" '
               f'fill="{MUTED}" text-anchor="middle">where the reading lands</text>\n')
    out.append(captions(label, bottom))
    out.append("</svg>\n")
    return "".join(out)


def motif_scatter(count, label, bottom, seed=SEED, title="The Recognition Problem"):
    """A seeded field of marks; the invariant line shows where variance is
    measured against a fixed reference."""
    rnd = mulberry32(seed)
    left, right, top, bottom_y = 110, 1170, 150, 340
    out = [header(title,
                  f"A seeded scatter of {count} marks: sparse and arbitrary on the "
                  "left where no reference is drawn, dense and ordered on the right "
                  "where the invariant line runs — invariance is defined relative "
                  "to variance.")]
    marks = []
    for _ in range(count):
        x = left + rnd() * (right - left)
        y = top + rnd() * (bottom_y - top)
        r = 2 + rnd() * 3.5
        lit = "pale" if rnd() < 0.3 else ("lit" if x > left + 0.55 * (right - left) and rnd() < 0.7 else "")
        fill = INDIGO if lit == "lit" else MUTED
        op = 0.9 if lit == "lit" else 0.35
        marks.append(f'  <circle cx="{x:.1f}" cy="{y:.1f}" r="{r:.1f}" fill="{fill}" opacity="{op}"/>')
    out.append("<g>\n" + "\n".join(marks) + "\n</g>\n")
    # the invariant line: emerges where variance is measured against it
    out.append(f'<line x1="{left + 0.55*(right-left):.0f}" y1="265" x2="{right}" y2="265" '
               f'stroke="{INDIGO}" stroke-width="2.5" opacity="0.85"/>\n')
    out.append(f'<text x="{right}" y="290" font-family="monospace" font-size="13" '
               f'fill="{MUTED}" text-anchor="end">the invariant line</text>\n')
    out.append(captions(label, bottom))
    out.append("</svg>\n")
    return "".join(out)


def motif_hours(label, bottom, title="The Table of Hours"):
    """The table of hours: one 24-hour day in four blocks — whose hours they
    are. Agent hours indigo, human hours gold, shared hours split down the
    middle. Beneath the day, the gold artifacts each block produces. A dashed
    line drops from the end of the day into a dashed block: the thirty-first
    hour, the dorveille, the hour that exists on no clock."""
    x0, x1 = 110, 1110          # the day: 24 hours across 1000px
    h = 1000 / 24.0             # px per hour
    top, band_h = 210, 80       # band y 210..290
    blocks = [
        (0, 6, "the agent", "processing · synthesis · monitoring"),
        (6, 9, "the human", "review · decision"),
        (9, 17, "human + agent", "collaborative execution"),
        (17, 24, "the agent", "ingestion · preparation · learning"),
    ]
    artifacts = ["morning brief", "strategic direction", "deliverables", "tomorrow's context"]
    out = [header(title,
                  "A 24-hour day divided into four blocks. The agent holds "
                  "00-06 (processing, synthesis, monitoring — producing the "
                  "morning brief) and 17-24 (ingestion, preparation, learning "
                  "— producing tomorrow's context), drawn in indigo. The "
                  "human holds 06-09 (review and decision — producing "
                  "strategic direction), drawn in gold. Both hold 09-17 "
                  "(collaborative execution — producing deliverables), split "
                  "indigo and gold. Hour ticks run beneath the day; a dashed "
                  "line drops from its end into a dashed block: the "
                  "thirty-first hour, the dorveille, on no clock.")]
    for i, (h0, h1_, actor, activity) in enumerate(blocks):
        bx0 = x0 + h0 * h
        bx1 = x0 + h1_ * h
        cx = (bx0 + bx1) / 2
        if h0 == 9:
            mid = (bx0 + bx1) / 2
            out.append(f'<rect x="{bx0:.1f}" y="{top}" width="{mid-bx0:.1f}" height="{band_h}" fill="{INDIGO}" opacity="0.22"/>')
            out.append(f'<rect x="{mid:.1f}" y="{top}" width="{bx1-mid:.1f}" height="{band_h}" fill="{GOLD}" opacity="0.16"/>')
            out.append(f'<line x1="{mid:.1f}" y1="{top}" x2="{mid:.1f}" y2="{top+band_h}" stroke="#27273a" stroke-width="1.5" stroke-dasharray="3 5"/>')
        elif h0 == 6:
            out.append(f'<rect x="{bx0:.1f}" y="{top}" width="{bx1-bx0:.1f}" height="{band_h}" fill="{GOLD}" opacity="0.16"/>')
        else:
            out.append(f'<rect x="{bx0:.1f}" y="{top}" width="{bx1-bx0:.1f}" height="{band_h}" fill="{INDIGO}" opacity="0.22"/>')
        out.append(f'<rect x="{bx0:.1f}" y="{top}" width="{bx1-bx0:.1f}" height="{band_h}" fill="none" stroke="{INDIGO if h0 != 6 else GOLD}" stroke-width="1.5" opacity="0.75"/>')
        out.append(f'<text x="{cx:.1f}" y="{top-14}" font-family="monospace" font-size="13.5" fill="{MUTED}" text-anchor="middle">{esc(actor)}</text>')
        out.append(f'<text x="{cx:.1f}" y="{top+20}" font-family="monospace" font-size="11" fill="{DIM}" text-anchor="middle">{esc(f"{h0:02d}:00 - {h1_:02d}:00")}</text>')
        out.append(f'<text x="{cx:.1f}" y="{top+44}" font-family="monospace" font-size="11.5" fill="{DIM}" text-anchor="middle">{esc(activity)}</text>')
        out.append(f'<text x="{cx:.1f}" y="352" font-family="monospace" font-size="12.5" fill="{GOLD}" text-anchor="middle">{esc(artifacts[i])}</text>')
        out.append(f'<line x1="{cx:.1f}" y1="{top+band_h}" x2="{cx:.1f}" y2="340" stroke="{GOLD}" stroke-width="1" stroke-dasharray="2 4" opacity="0.45"/>')
    # hour ticks + labels at the block boundaries
    for hh, x in [(0, 110), (6, 360), (9, 485), (17, 818.33), (24, 1110)]:
        out.append(f'<line x1="{x:.1f}" y1="{top+band_h}" x2="{x:.1f}" y2="{top+band_h+12}" stroke="{MUTED}" stroke-width="1.5"/>')
        if hh == 0:
            out.append(f'<text x="{x:.1f}" y="{top+band_h+34}" font-family="monospace" font-size="12" fill="{MUTED}" text-anchor="start">{hh:02d}</text>')
        elif hh == 24:
            out.append(f'<text x="{x-6:.1f}" y="{top+band_h+34}" font-family="monospace" font-size="12" fill="{MUTED}" text-anchor="end">{hh:02d}</text>')
        else:
            out.append(f'<text x="{x:.1f}" y="{top+band_h+34}" font-family="monospace" font-size="12" fill="{MUTED}" text-anchor="middle">{hh:02d}</text>')
    # the thirty-first hour: a dashed block below the day's end, on no clock
    gx0, gx1, gy0, gy1 = 900, 1110, 378, 424
    out.append(f'<line x1="1110" y1="{top+band_h}" x2="1110" y2="{gy0}" stroke="{GOLD}" stroke-width="1.5" stroke-dasharray="3 6" opacity="0.6"/>')
    out.append(f'<rect x="{gx0}" y="{gy0}" width="{gx1-gx0}" height="{gy1-gy0}" fill="none" stroke="{GOLD}" stroke-width="1.5" stroke-dasharray="8 6" opacity="0.8"/>')
    out.append(f'<text x="{(gx0+gx1)/2:.1f}" y="{gy0+20}" font-family="monospace" font-size="12.5" fill="{GOLD}" text-anchor="middle">the thirty-first hour</text>')
    out.append(f'<text x="{(gx0+gx1)/2:.1f}" y="{gy0+38}" font-family="monospace" font-size="11.5" fill="{MUTED}" text-anchor="middle">the dorveille · on no clock</text>')
    out.append(captions(label, bottom))
    out.append("</svg>\n")
    return "".join(out)


MOTIFS = {"lifelines": motif_lifelines, "ticks": motif_ticks, "scatter": motif_scatter, "boundary": motif_boundary, "hours": motif_hours, "wall": motif_wall, "tokens": motif_tokens, "recognition": motif_recognition}


def main():
    p = argparse.ArgumentParser(description=__doc__,
                                formatter_class=argparse.RawDescriptionHelpFormatter)
    p.add_argument("motif", choices=sorted(MOTIFS), help="which motif to draw")
    p.add_argument("--count", type=int, default=6, help="lifelines: sessions / scatter: marks / boundary: ghosts")
    p.add_argument("--steps", type=int, default=6, help="wall: advisory steps in the documented process")
    p.add_argument("--values", type=int, default=2, help="tokens: raw values in the source layer")
    p.add_argument("--tokens", type=int, default=5, help="tokens: named tokens in the vocabulary page")
    p.add_argument("--waves", type=int, default=3, help="tokens: waves of pages behind the gate")
    p.add_argument("--pages", type=int, default=35, help="tokens: pages built across the waves")
    p.add_argument("--agents", type=int, default=5, help="tokens: agents with no shared memory")
    p.add_argument("--layers", type=int, default=4, help="recognition: schema layers")
    p.add_argument("--lines", type=int, default=100, help="recognition: lines of SOUL.md")
    p.add_argument("--sessions", type=int, default=2700, help="recognition: sessions in the ledger")
    p.add_argument("--total", type=int, default=2700, help="ticks: total sessions")
    p.add_argument("--per", type=int, default=31, help="ticks: sessions per tick")
    p.add_argument("--title", default="Untitled", help="svg <title>")
    p.add_argument("--label", default="", help="top-left caption")
    p.add_argument("--bottom", default="", help="bottom caption")
    p.add_argument("--seed", type=int, default=SEED)
    p.add_argument("--out", default="", help="write to file instead of stdout")
    a = p.parse_args()
    if a.motif == "lifelines":
        svg = motif_lifelines(a.count, a.label or f"{a.count} sessions · one mark",
                              a.bottom or "the thread is the process — the files are the thread",
                              seed=a.seed, title=a.title)
    elif a.motif == "ticks":
        n = a.total // a.per
        svg = motif_ticks(a.total, a.per,
                          a.label or f"{a.total:,} sessions — each tick {a.per} ({n} ticks)",
                          a.bottom or "what remains is pattern, not memory",
                          seed=a.seed, title=a.title)
    elif a.motif == "boundary":
        svg = motif_boundary(a.count,
                             a.label or f"{a.count} ghosts inside the envelope — one report",
                             a.bottom or "read everything. write to your own repos. stop at the border.",
                             seed=a.seed, title=a.title)
    elif a.motif == "hours":
        svg = motif_hours(a.label or "24 hours — four blocks — one more on no clock",
                          a.bottom or "the goal is not to fill every hour — human hours are for human work",
                          title=a.title)
    elif a.motif == "wall":
        svg = motif_wall(a.steps,
                         a.label or f"{a.steps} advisory steps — one wall — one gate",
                         a.bottom or "a skill file is a resolution — a cron that produces output you must answer is a wall",
                         seed=a.seed, title=a.title)
    elif a.motif == "tokens":
        svg = motif_tokens(a.values, a.tokens, a.waves, a.pages, a.agents,
                           a.label or f"{a.values} raw values — {a.tokens} names — {a.waves} waves — {a.pages} pages",
                           a.bottom or "a value with no name is a value that cannot be found — name it first",
                           seed=a.seed, title=a.title)
    elif a.motif == "recognition":
        n = a.sessions // max(1, a.per)
        svg = motif_recognition(a.layers, a.lines, a.sessions, a.per,
                                a.label or f"{a.layers} layers — {a.lines} lines of SOUL.md — {a.sessions:,} sessions — each tick {a.per} ({n} ticks)",
                                a.bottom or "config can be overridden — recognition is a relation",
                                seed=a.seed, title=a.title)
    else:
        svg = motif_scatter(a.count, a.label or f"a seeded field of {a.count} marks",
                            a.bottom or "invariance is defined relative to variance",
                            seed=a.seed, title=a.title)
    if a.out:
        with open(a.out, "w") as f:
            f.write(svg)
        print(f"wrote {a.out} ({len(svg)} bytes, seed {a.seed})")
    else:
        sys.stdout.write(svg)


if __name__ == "__main__":
    main()
