diff --git a/docs/calle.svg b/docs/calle.svg
new file mode 100644
index 0000000..06a6986
--- /dev/null
+++ b/docs/calle.svg
@@ -0,0 +1,167 @@
+
\ No newline at end of file
diff --git a/tools/calle.py b/tools/calle.py
new file mode 100644
index 0000000..3edfc2b
--- /dev/null
+++ b/tools/calle.py
@@ -0,0 +1,119 @@
+# Genera docs/calle.svg: ilustración stencil "a la calle"
+import sys, random
+from fontTools.ttLib import TTFont
+from fontTools.pens.svgPathPen import SVGPathPen
+from fontTools.pens.transformPen import TransformPen
+random.seed(26)
+FONT, OUT = sys.argv[1], sys.argv[2]
+W, H = 1200, 640
+INK, RED = "#000", "#ff1f1f"
+out = []
+def add(s): out.append(s)
+
+def text_path(txt, x, y, size, fill, anchor="middle", font=FONT):
+ f = TTFont(font); gs = f.getGlyphSet(); cmap = f.getBestCmap()
+ upm = f["head"].unitsPerEm; k = size / upm
+ widths = [f["hmtx"][cmap[ord(c)]][0] for c in txt]
+ total = sum(widths) * k
+ cx = x - total / 2 if anchor == "middle" else x
+ d = []
+ for c, w in zip(txt, widths):
+ pen = SVGPathPen(gs)
+ gs[cmap[ord(c)]].draw(TransformPen(pen, (k, 0, 0, -k, cx, y)))
+ d.append(pen.getCommands()); cx += w * k
+ return f''
+
+def blob(cx, cy, r, n, jit, fill, op=1):
+ import math
+ pts = []
+ for i in range(n):
+ a = 2 * math.pi * i / n
+ rr = r * (1 + random.uniform(-jit, jit))
+ if random.random() < .25: rr *= 1.25
+ pts.append((cx + rr * math.cos(a), cy + rr * math.sin(a)))
+ d = f"M{pts[0][0]:.0f},{pts[0][1]:.0f} " + " ".join(
+ f"Q{p[0]:.0f},{p[1]:.0f} {(p[0]+q[0])/2:.0f},{(p[1]+q[1])/2:.0f}"
+ for p, q in zip(pts, pts[1:] + pts[:1])) + "Z"
+ add(f'')
+
+def splats(cx, cy, spread, n, rmax, fill):
+ for _ in range(n):
+ x = cx + random.gauss(0, spread); y = cy + random.gauss(0, spread * .6)
+ add(f'')
+
+def drips(x0, x1, y, fill, n):
+ for _ in range(n):
+ x = random.uniform(x0, x1); l = random.uniform(15, 70); w = random.uniform(5, 11)
+ add(f'')
+
+def tent(x, y, w, h):
+ add(f'')
+ add(f'')
+ add(f'')
+
+def fig(x, y, s=1.0, pose="down", step=1):
+ L = lambda *p: " ".join(f"{a:.0f}" for a in p)
+ sw = 19 * s
+ st = f'stroke="{INK}" stroke-width="{sw:.0f}" stroke-linecap="round" stroke-linejoin="round" fill="none"'
+ hipy = y - 78 * s; shy = y - 150 * s
+ # piernas (andando)
+ add(f'')
+ add(f'')
+ # torso (chupa)
+ add(f'')
+ # cresta (detrás) + cabeza
+ if pose in ("fist", "cresta"):
+ for i in range(5):
+ dx = (i - 2) * 11 * s
+ add(f'')
+ add(f'')
+ # brazos
+ ls, rs = (x - 27 * s, shy + 8 * s), (x + 27 * s, shy + 8 * s)
+ if pose == "fist":
+ add(f'')
+ add(f'')
+ add(f'')
+ elif pose.startswith("pole"):
+ side = -1 if pose == "poleL" else 1
+ px = x + side * 46 * s
+ add(f'')
+ add(f'')
+ else:
+ add(f'')
+ add(f'')
+
+add(f'')
+open(OUT, "w").write("\n".join(out))