diff --git a/FrOSCon26/drawings/coherence.svg b/FrOSCon26/drawings/coherence.svg
new file mode 100644
index 0000000..ef857e8
--- /dev/null
+++ b/FrOSCon26/drawings/coherence.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/FrOSCon26/drawings/gen-cache-coherence-diagram.py b/FrOSCon26/drawings/gen-cache-coherence-diagram.py
new file mode 100644
index 0000000..8e820fe
--- /dev/null
+++ b/FrOSCon26/drawings/gen-cache-coherence-diagram.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+"""Cache coherence / observability diagram.
+
+Four Inkscape layers, meant to be exported one by one (Export > Page) and
+stacked as transparent overlays in Impress, same workflow as the time-scales
+diagram. Layer content is INCREMENTAL: each layer only draws what is new at
+that step; earlier layers stay visible underneath once stacked.
+
+Story:
+ 1 scaffold - two cores, two caches, one shared DRAM, all agree: x=0
+ 2 core0-write - core0's cache changes to x=1; nothing else does
+ 3 core1-read - core1 reads its own cache: still x=0 (stale, not wrong)
+ 4 propagate - some time later, x=1 reaches DRAM on its own;
+ core1's cache is untouched and still says x=0
+"""
+
+W, H = 254.0, 142.9
+
+# ---- palette (kept consistent with time-scales.svg) -----------------------
+C_LINE = "#555555"
+C_BOX = "#222222"
+C_MACH = "#14507d" # "new" value / updated state
+C_STALE = "#8a8a8a" # stale / unchanged value
+C_FILL0 = "#f2f2f2" # box fill, neutral state
+C_FILL1 = "#dce8f2" # box fill, updated state
+C_WHITE = "#ffffff"
+FONT = "DejaVu Sans, Verdana, sans-serif"
+
+F_LABEL, F_VALUE, F_TAG = 5.5, 6.5, 4.2
+
+# ---- geometry ---------------------------------------------------------
+CORE_W, CORE_H = 46.0, 20.0
+CACHE_W, CACHE_H = 46.0, 22.0
+DRAM_W, DRAM_H = 90.0, 24.0
+
+CORE0_X, CORE1_X = 44.0, 164.0
+CORE_Y = 24.0
+CACHE_Y = 58.0
+DRAM_X = (W - DRAM_W) / 2.0
+DRAM_Y = 104.0
+
+C0_CX = CORE0_X + CORE_W / 2.0
+C1_CX = CORE1_X + CORE_W / 2.0
+DRAM_CX = DRAM_X + DRAM_W / 2.0
+
+
+def esc(s):
+ return s.replace("&", "&").replace("<", "<").replace(">", ">")
+
+
+def text(x, y, s, size, fill, anchor="middle", weight="normal", family=FONT):
+ return (f'{esc(s)}')
+
+
+def rect(x, y, w, h, fill, stroke, sw=0.6, rx=2.0):
+ return (f'')
+
+
+def line(x1, y1, x2, y2, stroke=C_LINE, w=0.6, dash=None, marker=False):
+ d = f' stroke-dasharray="{dash}"' if dash else ""
+ m = ' marker-end="url(#arrow)"' if marker else ""
+ return (f'')
+
+
+def defs():
+ return (''
+ f'')
+
+
+# ==========================================================================
+# static structural pieces, reused across layers where needed
+# ==========================================================================
+def core_box(cx_left, label_suffix=""):
+ cx = cx_left + CORE_W / 2.0
+ o = [rect(cx_left, CORE_Y, CORE_W, CORE_H, C_WHITE, C_BOX, 0.8)]
+ o.append(text(cx, CORE_Y + CORE_H / 2.0 + 1.8, "Core", F_LABEL, C_BOX,
+ weight="600"))
+ return o
+
+
+def cache_box_frame(cx_left):
+ return [rect(cx_left, CACHE_Y, CACHE_W, CACHE_H, C_FILL0, C_BOX, 0.8)]
+
+
+def cache_tag(cx_left):
+ cx = cx_left + CACHE_W / 2.0
+ return [text(cx, CACHE_Y + 6.0, "Cache", F_TAG, C_BOX, weight="600")]
+
+
+def cache_value(cx_left, value_str, colour):
+ cx = cx_left + CACHE_W / 2.0
+ return [text(cx, CACHE_Y + CACHE_H / 2.0 + 5.0, value_str, F_VALUE, colour,
+ weight="700")]
+
+
+def dram_box_frame():
+ return [rect(DRAM_X, DRAM_Y, DRAM_W, DRAM_H, C_FILL0, C_BOX, 0.8)]
+
+
+def dram_tag():
+ return [text(DRAM_CX, DRAM_Y + 7.0, "DRAM", F_TAG, C_BOX, weight="600")]
+
+
+def dram_value(value_str, colour):
+ return [text(DRAM_CX, DRAM_Y + DRAM_H / 2.0 + 6.5, value_str, F_VALUE,
+ colour, weight="700")]
+
+
+def bus_lines():
+ o = []
+ o.append(line(C0_CX, CORE_Y + CORE_H, C0_CX, CACHE_Y, marker=True))
+ o.append(line(C1_CX, CORE_Y + CORE_H, C1_CX, CACHE_Y, marker=True))
+ o.append(line(C0_CX, CACHE_Y + CACHE_H, C0_CX, DRAM_Y + 6.0, C_LINE, 0.6))
+ o.append(line(C0_CX, DRAM_Y + 6.0, DRAM_X + 8.0, DRAM_Y, C_LINE, 0.6))
+ o.append(line(C1_CX, CACHE_Y + CACHE_H, C1_CX, DRAM_Y + 6.0, C_LINE, 0.6))
+ o.append(line(C1_CX, DRAM_Y + 6.0, DRAM_X + DRAM_W - 8.0, DRAM_Y, C_LINE, 0.6))
+ return o
+
+
+# ==========================================================================
+# layer 1 - scaffold: full structure, everyone agrees x = 0
+# ==========================================================================
+def layer1():
+ o = [defs()]
+ o += core_box(CORE0_X)
+ o += core_box(CORE1_X)
+ o += bus_lines()
+ o += cache_box_frame(CORE0_X)
+ o += cache_box_frame(CORE1_X)
+ o += cache_tag(CORE0_X)
+ o += cache_tag(CORE1_X)
+ o += cache_value(CORE0_X, "x = 0", C_STALE)
+ o += cache_value(CORE1_X, "x = 0", C_STALE)
+ o += dram_box_frame()
+ o += dram_tag()
+ o += dram_value("x = 0", C_STALE)
+ return o
+
+
+# ==========================================================================
+# layer 2 - core0 writes: only its own cache changes
+# ==========================================================================
+def layer2():
+ o = []
+ # cover old value + box fill with an updated-state box, then new value
+ o.append(rect(CORE0_X, CACHE_Y, CACHE_W, CACHE_H, C_FILL1, C_MACH, 1.1))
+ o += cache_tag(CORE0_X)
+ o += cache_value(CORE0_X, "x = 1", C_MACH)
+ return o
+
+
+# ==========================================================================
+# layer 3 - core1 reads its own cache: emphasise the access, value unchanged
+# ==========================================================================
+def layer3():
+ o = []
+ cx = CORE1_X + CACHE_W / 2.0
+ # small self-loop "read" arrow from core1 down into its own cache
+ top = CORE_Y + CORE_H + 1.5
+ bot = CACHE_Y - 1.5
+ r = 5.0
+ o.append(f'')
+ o.append(rect(CORE1_X, CACHE_Y, CACHE_W, CACHE_H, C_FILL0, C_STALE, 1.1))
+ o += cache_tag(CORE1_X)
+ o += cache_value(CORE1_X, "x = 0", C_STALE)
+ return o
+
+
+# ==========================================================================
+# layer 4 - later, unobserved: x = 1 reaches DRAM; core1 still stale
+# ==========================================================================
+def layer4():
+ o = []
+ # a faint "time passing" mark between the structure and nothing else
+ ty = DRAM_Y - 9.0
+ dots = "".join(f'' for i in range(5))
+ o.append(dots)
+ o.append(rect(DRAM_X, DRAM_Y, DRAM_W, DRAM_H, C_FILL1, C_MACH, 1.1))
+ o += dram_tag()
+ o += dram_value("x = 1", C_MACH)
+ return o
+
+
+# ==========================================================================
+def layer(lid, label, body, display="inline"):
+ return (f'\n '
+ + "\n ".join(body) + "\n")
+
+
+def build():
+ layers = [
+ layer("layer-1-scaffold", "1 - scaffold (x=0 everywhere)", layer1()),
+ layer("layer-2-core0-write", "2 - core0 writes x=1", layer2()),
+ layer("layer-3-core1-read", "3 - core1 reads: still x=0", layer3()),
+ layer("layer-4-propagate", "4 - later: DRAM sees x=1, core1 still stale",
+ layer4()),
+ ]
+ return f'''
+
+'''
+
+
+if __name__ == "__main__":
+ open("/mnt/user-data/outputs/coherence.svg", "w").write(build())
+ print("written")