# -*- coding: utf-8 -*-
"""Generates the AISA Next UI wireframes (schematic SVGs) — one shell, one vocabulary, 18 screens plus one action variant.
Run: python gen_wireframes.py <outdir>
"""
import sys, os, html, textwrap

OUT = sys.argv[1] if len(sys.argv) > 1 else "."
W, H = 1280, 820

def wrap(s, width_px, size=12, factor=0.54):
    """Split text so that each line fits width_px at the given font size."""
    n = max(20, int(width_px / (size * factor)))
    return textwrap.wrap(s, n) or [""]
INK, MUTE, LINE, FILL = "#1f2328", "#6b7280", "#c9ced6", "#f3f4f6"
AMBER, AMBER_L = "#e8a33d", "#fdf1de"      # human decision / gate
OK, OK_L = "#3b8f5a", "#e6f4ea"            # running / green
INFO, INFO_L = "#3b6fb6", "#eaf0fb"        # link / parked / target state
WARN, WARN_L = "#c0392b", "#fbe9e7"        # failed / refusal
MONO = "ui-monospace, Consolas, Menlo, monospace"

def esc(s): return html.escape(str(s), quote=True)

class Svg:
    def __init__(self, title):
        self.parts = []
        self.title = title
    def add(self, s): self.parts.append(s)
    def rect(self, x, y, w, h, fill="#fff", stroke=LINE, rx=6, sw=1, dash=None, opacity=None):
        extra = f' stroke-dasharray="{dash}"' if dash else ""
        op = f' opacity="{opacity}"' if opacity else ""
        self.add(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{fill}" stroke="{stroke}" stroke-width="{sw}"{extra}{op}/>')
    def line(self, x1, y1, x2, y2, stroke=LINE, sw=1, dash=None):
        extra = f' stroke-dasharray="{dash}"' if dash else ""
        self.add(f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{stroke}" stroke-width="{sw}"{extra}/>')
    def text(self, x, y, s, size=13, fill=INK, weight=400, anchor="start", mono=False, italic=False, ls=None):
        fam = f' font-family="{MONO}"' if mono else ""
        it = ' font-style="italic"' if italic else ""
        lsp = f' letter-spacing="{ls}"' if ls else ""
        self.add(f'<text x="{x}" y="{y}" font-size="{size}" fill="{fill}" font-weight="{weight}" text-anchor="{anchor}"{fam}{it}{lsp}>{esc(s)}</text>')
    def circle(self, cx, cy, r, fill, stroke="none"):
        self.add(f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="{fill}" stroke="{stroke}"/>')
    def chip(self, x, y, label, kind="mute", size=11):
        # returns the width used
        w = int(len(label) * size * 0.58) + 14
        fills = {"mute": ("#fff", LINE, MUTE), "gate": (AMBER_L, AMBER, "#7a4d00"), "ok": (OK_L, OK, "#1f5c37"),
                 "info": (INFO_L, INFO, "#23477a"), "warn": (WARN_L, WARN, "#7a1f16"), "ink": (FILL, LINE, INK),
                 "target": ("#fff", INFO, INFO), "mono": ("#fff", LINE, INK)}
        f, s, t = fills.get(kind, fills["mute"])
        self.rect(x, y, w, 18, fill=f, stroke=s, rx=9)
        self.text(x + 7, y + 13, label, size=size, fill=t, mono=(kind == "mono"))
        return w
    def chips(self, x, y, items, gap=6):
        for label, kind in items:
            x += self.chip(x, y, label, kind) + gap
        return x
    def button(self, x, y, label, kind="mute", w=None, size=12):
        w = w or int(len(label) * size * 0.6) + 22
        fills = {"primary": (INK, INK, "#fff"), "gate": (AMBER_L, AMBER, "#7a4d00"), "mute": ("#fff", LINE, INK),
                 "warn": (WARN_L, WARN, "#7a1f16"), "ok": (OK_L, OK, "#1f5c37"), "disabled": (FILL, LINE, MUTE)}
        f, s, t = fills.get(kind, fills["mute"])
        self.rect(x, y, w, 26, fill=f, stroke=s, rx=6)
        self.text(x + w / 2, y + 17, label, size=size, fill=t, weight=600, anchor="middle")
        return w
    def buttons(self, x, y, items, gap=8):
        for label, kind in items:
            x += self.button(x, y, label, kind) + gap
        return x
    def panel(self, x, y, w, h, title=None, sub=None):
        self.rect(x, y, w, h, fill="#fff")
        if title:
            self.text(x + 12, y + 20, title, size=12.5, weight=700, fill=INK)
            if sub: self.text(x + 12 + len(title) * 7.6 + 8, y + 20, sub, size=11, fill=MUTE)
            self.line(x, y + 30, x + w, y + 30)
    def table(self, x, y, w, cols, rows, rowh=26, head=True, colw=None, size=12):
        n = len(cols)
        colw = colw or [w / n] * n
        cy = y
        if head:
            self.rect(x, cy, w, rowh, fill=FILL, stroke=LINE, rx=0)
            cx = x
            for c, cw in zip(cols, colw):
                self.text(cx + 8, cy + 17, c, size=11, fill=MUTE, weight=700)
                cx += cw
            cy += rowh
        for r in rows:
            self.rect(x, cy, w, rowh, fill="#fff", stroke=LINE, rx=0)
            cx = x
            for cell, cw in zip(r, colw):
                if isinstance(cell, tuple):            # ("label", kind) -> chip
                    self.chip(cx + 6, cy + 4, cell[0], cell[1])
                elif isinstance(cell, list):           # list of chips
                    self.chips(cx + 6, cy + 4, cell)
                else:
                    self.text(cx + 8, cy + 17, cell, size=size)
                cx += cw
            cy += rowh
        return cy
    def bar(self, x, y, w, pct, kind="ok", h=8):
        self.rect(x, y, w, h, fill=FILL, stroke="none", rx=4)
        col = {"ok": OK, "gate": AMBER, "warn": WARN, "info": INFO}[kind]
        self.rect(x, y, max(4, w * pct / 100), h, fill=col, stroke="none", rx=4)
    def clip_start(self, cid, x, y, w, h):
        self.add(f'<clipPath id="{cid}"><rect x="{x}" y="{y}" width="{w}" height="{h}"/></clipPath><g clip-path="url(#{cid})">')
    def clip_end(self):
        self.add('</g>')
    def render(self):
        body = "\n".join(self.parts)
        return (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}" '
                f'font-family="system-ui, Segoe UI, Roboto, sans-serif" role="img" aria-label="{esc(self.title)}">\n'
                f'<rect width="{W}" height="{H}" fill="#fafafa"/>\n{body}\n</svg>\n')

RAIL = [  # (group, [(label, badge)])
    ("WORK", [("Inbox", "5"), ("Cases", "23")]),
    ("DECIDE", [("Decisions", "3")]),
    ("BUILD", [("Studio", ""), ("Knowledge", "2")]),
    ("OBSERVE", [("Metrics", "")]),
    ("CONTROL", [("Control", "1")]),
]

def shell(s, workspace, crumb, roles, env="PROD", client=False):
    """Top bar + left rail. Returns the content origin (x, y, w, h)."""
    # top bar
    s.rect(0, 0, W, 48, fill="#fff", stroke=LINE, rx=0)
    s.text(16, 30, "Serdica", size=14, weight=700)
    s.text(84, 30, "AI Support" if not client else "My requests", size=13, fill=MUTE)
    s.text(184, 30, crumb, size=13, fill=INK)
    # search
    s.rect(560, 12, 220, 24, fill=FILL, stroke=LINE, rx=12)
    s.text(574, 29, "Search…  /", size=12, fill=MUTE)
    # environment + kill switch + user
    x = 796
    if not client:
        x += s.chip(x, 15, "estate: Bulstrad", "mute") + 10
        s.circle(x + 6, 24, 5, OK); s.text(x + 16, 29, "kill switch off", size=11, fill=MUTE); x += 100
    s.text(W - 16, 29, f"{roles}", size=12, fill=MUTE, anchor="end")
    if client:
        return (24, 72, W - 48, H - 72 - 64)
    # rail
    s.rect(0, 48, 200, H - 48, fill="#fcfcfd", stroke=LINE, rx=0)
    y = 72
    for grp, items in RAIL:
        s.text(16, y, grp, size=10, fill=MUTE, weight=700, ls="1")
        y += 12
        for label, badge in items:
            on = (label == workspace)
            if on:
                s.rect(6, y - 2, 188, 26, fill=INFO_L, stroke="none", rx=6)
                s.rect(6, y - 2, 3, 26, fill=INFO, stroke="none", rx=1)
            s.text(18, y + 15, label, size=13, fill=INK if on else "#374151", weight=600 if on else 400)
            if badge:
                s.rect(160, y + 3, 26, 16, fill=FILL if not on else "#fff", stroke=LINE, rx=8)
                s.text(173, y + 15, badge, size=10.5, fill=MUTE, anchor="middle")
            y += 30
        y += 14
    s.line(0, H - 110, 200, H - 110)
    s.text(16, H - 86, "Client app  ↗", size=12, fill=INFO)
    s.text(16, H - 68, "role decides which doors exist", size=10.5, fill=MUTE)
    return (216, 64, W - 232, H - 64 - 64)

def footer(s, sid, line, show_legend=True):
    s.rect(0, H - 52, W, 52, fill="#fff", stroke=LINE, rx=0)
    s.text(16, H - 32, sid, size=11.5, fill=MUTE, weight=700)
    s.text(200, H - 32, line, size=11.5, fill=MUTE)
    if show_legend: legend(s, 200, H - 22)

def legend(s, x, y):
    s.text(x - 184, y + 13, "legend", size=10.5, fill=MUTE, weight=700, ls="1")
    s.chip(x, y, "human decision", "gate"); s.chip(x + 112, y, "running / green", "ok")
    s.chip(x + 226, y, "parked / link / target", "info"); s.chip(x + 372, y, "refused / failed", "warn")
    s.chip(x + 494, y, "ENV · SYSTEM · OBJECT = where", "mono")

def save(s, name):
    path = os.path.join(OUT, name)
    with open(path, "w", encoding="utf-8") as f:
        f.write(s.render())
    print("wrote", name)


# ---------------------------------------------------------------- shared chat vocabulary
KIND_COL = {"brief": INFO, "agent": OK, "tool": MUTE, "question": AMBER, "plan": INK, "decision": AMBER,
            "verdict": OK, "write": WARN, "handover": INFO, "status": MUTE, "human": INK, "consult": INFO}

def card(s, x, y, w, kind, title, lines=(), chips=None, buttons=None, meta=None, size=12):
    """One chat card. Left colour bar = kind. Long lines wrap. Returns the y below the card."""
    body = []
    for ln in lines:
        body += wrap(ln, w - 28, size, 0.46) if not ln.startswith("$") else [ln]
    tw = w - 14 - len(kind) * 7 - 10 - (len(meta or "") * 6.2 + 20) - 10
    tl = wrap(title, tw, 12.5, 0.5)
    h = 34 + (len(tl) - 1) * 17 + len(body) * 17 + (24 if chips else 0) + (34 if buttons else 0)
    s.rect(x, y, w, h, fill="#fff")
    s.rect(x, y, 4, h, fill=KIND_COL.get(kind, MUTE), stroke="none", rx=2)
    s.text(x + 14, y + 19, kind.upper(), size=9.5, fill=KIND_COL.get(kind, MUTE), weight=700, ls="1")
    ty = y + 19
    for t in tl:
        s.text(x + 14 + len(kind) * 7 + 10, ty, t, size=12.5, weight=600); ty += 17
    if meta: s.text(x + w - 10, y + 19, meta, size=10.5, fill=MUTE, anchor="end")
    yy = ty
    for ln in body:
        s.text(x + 14, yy, ln, size=size, fill=INK if not ln.startswith("·") else "#374151", mono=ln.startswith("$"))
        yy += 17
    if chips:
        s.chips(x + 14, yy - 6, chips); yy += 24
    if buttons:
        s.buttons(x + 14, yy - 4, buttons); yy += 34
    return y + h + 8

def para(s, x, y, w, text, size=12, fill=INK, factor=0.46, lh=17, italic=False, mono=False):
    """Wrapped paragraph; returns the y after the last line."""
    for ln in wrap(text, w, size, factor if not mono else 0.62):
        s.text(x, y, ln, size=size, fill=fill, italic=italic, mono=mono); y += lh
    return y

def where_chip_row(s, x, y, env, system, obj, mode=None):
    x = s.chip(x, y, env, "mono") + x + 4
    x = s.chip(x, y, system, "mono") + x + 4
    x = s.chip(x, y, obj, "mono") + x + 4
    if mode: s.chip(x, y, mode, "ok" if mode == "read" else ("gate" if mode == "write" else "warn"))

def composer(s, x, y, w, hint, mode_line):
    s.rect(x, y, w, 58, fill="#fff", stroke=INK, rx=8)
    s.text(x + 14, y + 22, hint, size=12.5, fill=MUTE)
    s.text(x + 14, y + 44, mode_line, size=10.5, fill=MUTE, mono=True)
    s.button(x + w - 72, y + 16, "Send", "primary", w=60)

def status_line(s, x, y, w, items):
    s.rect(x, y, w, 22, fill=FILL, stroke="none", rx=4)
    s.text(x + 10, y + 15, "   ·   ".join(items), size=11, fill=MUTE, mono=True)

def stage_ladder(s, x, y, stages, current, step=44):
    """stages: list of (code, label, gate_or_None). current index."""
    for i, (code, label, gate) in enumerate(stages):
        yy = y + i * step
        done = i < current; now = i == current
        col = OK if done else (INFO if now else LINE)
        s.circle(x + 10, yy + 10, 8 if now else 6, col if (done or now) else "#fff", stroke=col)
        if i < len(stages) - 1: s.line(x + 10, yy + 18, x + 10, yy + step - 8, stroke=LINE)
        s.text(x + 26, yy + 14, f"{code}  {label}", size=12, weight=700 if now else 400, fill=INK if (done or now) else MUTE)
        if gate: s.chip(x + 26, yy + 19, gate, "gate", size=9.5)

# ---------------------------------------------------------------- 01 Inbox
def scr_inbox():
    s = Svg("01 Inbox — arrivals re-briefed from their source")
    x, y, w, h = shell(s, "Inbox", "Inbox", "Mira D. · Operator")
    s.text(x, y + 18, "Inbox", size=18, weight=700)
    s.text(x + 70, y + 18, "every arrival is re-briefed from its source before anyone reads it — the brief says where the source is", size=12, fill=MUTE)
    s.chips(x, y + 30, [("all 5", "ink"), ("ticket system 2", "mute"), ("mailbox 2", "mute"), ("helpdesk 1", "mute"), ("monitor 0 · policy-ignored 1", "mute"), ("duplicates 1", "gate")])
    # list
    lx, ly, lw = x, y + 62, 396
    s.panel(lx, ly, lw, 640, "Arrivals", "newest first")
    items = [
        ("Premium differs between the two systems — policy ••••7731", [("ticket system", "info"), ("SD-4471", "mono"), ("09:12", "mute")], "Support · incident · §1 Sev 2 · duplicate: none", True),
        ("New vehicle model missing in pricing — Renault Austral", [("mailbox", "info"), ("servicedesk@", "mono"), ("08:40", "mute")], "Configuration · request · §2 P2", False),
        ("Transfer to INSIS fails for annex 3 — policy ••••0028", [("helpdesk", "info"), ("00101281", "mono"), ("08:15", "mute")], "Support · incident · §1 Sev 2 · duplicate? → case #1040", False),
        ("Question about green-card blank ranges", [("mailbox", "info"), ("servicedesk@", "mono"), ("07:55", "mute")], "Support · question · §1 Sev 4", False),
        ("Zabbix: disk 80 % on qa-app-2", [("monitor", "mute"), ("auto", "mono")], "ignored sender (policy) — kept for the record", False),
    ]
    yy = ly + 40
    for title, chips, cls, sel in items:
        if sel: s.rect(lx + 6, yy - 6, lw - 12, 84, fill=INFO_L, stroke="none", rx=6)
        s.text(lx + 16, yy + 10, title, size=12.5, weight=600)
        s.chips(lx + 16, yy + 22, chips)
        s.text(lx + 16, yy + 60, cls, size=11, fill=MUTE)
        yy += 96
    s.text(lx + 16, yy + 10, "Duplicates fold into the open case; policy-ignored senders stay visible but silent.", size=11, fill=MUTE, italic=True)
    # brief
    bx, by, bw = x + lw + 16, y + 62, w - lw - 16
    s.panel(bx, by, bw, 640, "Brief", "arrival from ticket system SD-4471 · built by platform.intake · case opened automatically; one action corrects or re-routes")
    yy = by + 46
    s.text(bx + 14, yy, "SOURCE", size=10, fill=MUTE, weight=700, ls="1"); yy += 8
    s.chips(bx + 14, yy, [("ticket system · SD-4471 · open ↗", "info"), ("reporter: L. Andreev (Bulstrad)", "mute"), ("arrived 09:12", "mute")]); yy += 24
    s.chips(bx + 14, yy, [("origin key  ticket:SD-4471", "mono"), ("channel: ticket system (authoritative)", "mute")]); yy += 36
    s.text(bx + 14, yy, "WHAT WAS SAID (normalised)", size=10, fill=MUTE, weight=700, ls="1"); yy += 18
    for ln in ["Reporter compares the premium of policy ••••7731 in the two systems and sees 512.40 EUR vs 498.10 EUR.",
               "Asks which amount is right and to align the systems before the instalment is collected. One screenshot attached."]:
        s.text(bx + 14, yy, ln, size=12); yy += 17
    yy += 8
    s.text(bx + 14, yy, "IDENTIFIERS → HANDLES", size=10, fill=MUTE, weight=700, ls="1"); yy += 8
    yy = s.table(bx + 14, yy, bw - 28, ["mentioned", "resolves to (where)", "check"],
                 [["policy 4704…7731", "PROD · IPAL · policy ••••7731", ("resolves", "ok")],
                  ["policy 4704…7731", "PROD · INSIS · policy ••••7731", ("resolves", "ok")],
                  ["screenshot.png", "evidence/1 (attachment, kept with the case)", ("stored", "ok")]],
                 colw=[150, 300, (bw - 28) - 450], rowh=24)
    yy += 12
    s.text(bx + 14, yy, "DUPLICATE VERDICT", size=10, fill=MUTE, weight=700, ls="1")
    s.chip(bx + 150, yy - 13, "no open case shares this origin key or these handles", "ok"); yy += 22
    s.text(bx + 14, yy, "PROPOSED", size=10, fill=MUTE, weight=700, ls="1"); yy += 8
    yy = s.table(bx + 14, yy, bw - 28, ["owning module", "case type", "contract", "clocks (two)", "customer"],
                 [["Support", "incident", "§1 · Sev 2", "response 48 h · resolution 5 bd", "Bulstrad"]],
                 colw=[120, 100, 100, 220, (bw - 28) - 540], rowh=24)
    yy += 10
    s.text(bx + 14, yy, "PRECEDENTS", size=10, fill=MUTE, weight=700, ls="1")
    s.chips(bx + 110, yy - 13, [("case #0977 · resolved · same symptom", "info"), ("knowledge: premium-difference pattern", "info")]); yy += 22
    s.text(bx + 14, yy, "INITIATOR", size=10, fill=MUTE, weight=700, ls="1")
    s.text(bx + 110, yy, "platform.intake (agent) — you are the first person to see this; nothing has been sent to anyone.", size=11.5, fill=MUTE); yy += 26
    s.buttons(bx + 14, yy, [("Open running case #1042", "primary"), ("Correct & route…", "mute"), ("Merge into #1040", "mute"), ("Dismiss", "mute")])
    footer(s, "01 · Inbox", "backs: arrivals → INTAKE brief (origin key · handles · duplicate verdict · proposed classification · precedents). Operator: inspect / correct / merge / dismiss. Viewer: read.")
    save(s, "wf-inbox.svg")

# ---------------------------------------------------------------- 02 Cases
def scr_cases():
    s = Svg("02 Cases — every case, its state and where it is")
    x, y, w, h = shell(s, "Cases", "Cases", "Mira D. · Operator · Viewer")
    s.text(x, y + 18, "Cases", size=18, weight=700)
    s.text(x + 70, y + 18, "one row per case — the state word and the where-chips are the same ones used inside the chat", size=12, fill=MUTE)
    s.chips(x, y + 30, [("all 23", "ink"), ("running 6", "ok"), ("at gate 3", "gate"), ("held 2", "gate"), ("parked 5", "info"), ("opened 0", "mute"), ("resolved 4", "mute"), ("closed 3", "mute")])
    s.text(x + w - 8, y + 42, "mine · module ▾ · customer ▾ · env ▾ · saved views ▾", size=11.5, fill=MUTE, anchor="end")
    cols = ["case", "module · stage", "state", "where (env · system · object)", "controller", "waiting on", "clock"]
    colw = [250, 150, 152, 216, 96, 100, w - 964]
    rows = [
        ["#1042  Premium differs between systems", "Support · S2 investigation", ("running", "ok"), "PROD · IPAL + INSIS · policy ••••7731", "Mira D. (you)", "—", "resp 46 h"],
        ["#1041  New vehicle model in pricing", "Configuration · S3 ipal", ("at gate · H3", "gate"), "TEST · IPAL · nomenclature", "—", "Approver", "offer 6 bd"],
        ["#1040  Transfer fails for annex 3", "Support · S5 application", ("held · H5 · HW", "gate"), "PROD · INSIS · annex ••••0028/3", "Operator", "Approver", "res 3 bd"],
        ["#1039  Rating version drift after deploy", "Development · S6 scripts", ("parked · provider-down", "info"), "DEV · git · MR !412", "—", "provider", "—"],
        ["#1038  Transfer fails (duplicate)", "Support · —", ("closed · merged", "mute"), "→ case #1040", "—", "—", "—"],
        ["#1037  Provider report review 2026-08", "Support · S2 investigation", ("parked · question", "info"), "QA · HLT · report 2026-08", "Operator", "customer", "resp 12 h"],
        ["#1036  Sync framework 1101 for child", "Configuration · S5 serdica", ("at gate · H5-send", "gate"), "PROD · INSIS · policy 1101••••91", "—", "Approver", "res 1 bd"],
        ["#1035  Annex commission only decreases", "Support · S6 verification", ("running", "ok"), "TEST · IPAL · annex ••••0417/2", "Operator", "—", "res 4 bd"],
        ["#1034  Green-card ranges for agency 18617", "Configuration · S5 serdica", ("resolved", "ok"), "PROD · IPAL · BSO ranges", "—", "customer · CG-11", "—"],
        ["#1033  Print template debit note fails", "Support · S4 confirmation", ("parked · budget", "info"), "PROD · BI · template DN-3", "Operator", "Operator · extend", "res 2 bd"],
        ["#1032  Nightly consolidation proposals", "Platform · —", ("running", "ok"), "platform · knowledge · 4 proposals", "curator role", "—", "—"],
    ]
    s.table(x, y + 60, w, cols, rows, rowh=30, colw=colw)
    yy = y + 60 + 30 * (len(rows) + 1) + 14
    s.text(x, yy, "state words:  opened · running · parked (question · gate · sub-case · connector-down · provider-down · budget · capability-gap) · at gate · held · resolved · closed", size=11, fill=MUTE)
    s.text(x, yy + 18, "row click → Case chat.  Bulk (Operator): park · take control · hand over.  Approver sees the same list filtered to “waiting on: Approver”.", size=11, fill=MUTE)
    yy += 44
    third = (w - 32) / 3
    s.panel(x, yy, third, 150, "Who is waited on", "right now")
    ty = yy + 46
    for who, n, k in [("Operator (a question or a steer)", "4", "ok"), ("Approver (a gate)", "3", "gate"), ("customer (an answer or an acceptance)", "3", "info"), ("connector / provider (parked)", "2", "info")]:
        s.text(x + 12, ty, who, size=11.5); s.chip(x + third - 44, ty - 13, n, k); ty += 24
    s.panel(x + third + 16, yy, third, 150, "Clocks at risk", "next 24 h")
    ty = yy + 46
    for ln in ["#1037 · response · 12 h left · parked on the customer", "#1036 · resolution · 1 bd left · waiting on Approver", "#1033 · resolution · budget hold since yesterday"]:
        s.text(x + third + 28, ty, ln, size=11.5); ty += 24
    s.text(x + third + 28, ty + 4, "breaches are reported, never hidden", size=10.5, fill=MUTE, italic=True)
    s.panel(x + 2 * (third + 16), yy, third, 150, "Keyboard", "the CLI habit")
    ty = yy + 46
    for k_, v in [("j / k", "move · Enter opens the chat"), ("/", "search cases, tickets, policies"), ("g d · g i · g s", "go to Decisions · Inbox · Studio"), ("?", "all shortcuts")]:
        s.text(x + 2 * (third + 16) + 12, ty, k_, size=11.5, mono=True); s.text(x + 2 * (third + 16) + 120, ty, v, size=11.5, fill=MUTE); ty += 24
    footer(s, "02 · Cases", "backs: CASES · SESSIONS (controller) · GATES (waiting on) · clocks from the classification. One list, role-filtered — not a separate screen per role.")
    save(s, "wf-cases.svg")

SUPPORT_STAGES = [("S1", "classification", "H1"), ("S2", "investigation", None), ("S3", "solution take", "H2"), ("S4", "confirmation", "H3"),
                  ("S5", "application", "H5 · HW"), ("S6", "verification", None), ("S7", "reversal", "H7"), ("S8", "precipitation", None)]

def case_frame(s, x, y, w, h, current, title_state, controller="you"):
    """Left ladder + centre thread bounds + right rail bounds."""
    s.rect(x, y, 196, h, fill="#fff")
    s.text(x + 12, y + 22, "#1042", size=15, weight=700)
    s.chip(x + 66, y + 9, title_state[0], title_state[1])
    s.text(x + 12, y + 42, "Support · incident · §1 Sev 2", size=11, fill=MUTE)
    s.text(x + 12, y + 58, "Bulstrad · resp 46 h left", size=11, fill=MUTE)
    s.line(x, y + 70, x + 196, y + 70)
    s.text(x + 12, y + 90, "STAGES · Support", size=10, fill=MUTE, weight=700, ls="1")
    stage_ladder(s, x + 12, y + 102, SUPPORT_STAGES, current)
    sy = y + 102 + len(SUPPORT_STAGES) * 44 + 4
    s.line(x, sy, x + 196, sy)
    s.text(x + 12, sy + 20, "SESSIONS", size=10, fill=MUTE, weight=700, ls="1")
    s.circle(x + 18, sy + 38, 4, OK); s.text(x + 28, sy + 42, f"3 · live · controller: {controller}", size=11.5)
    s.circle(x + 18, sy + 58, 4, LINE); s.text(x + 28, sy + 62, "2 · stopped · 08:40 → 09:05", size=11.5, fill=MUTE)
    s.circle(x + 18, sy + 78, 4, LINE); s.text(x + 28, sy + 82, "1 · stopped · intake", size=11.5, fill=MUTE)
    s.text(x + 12, sy + 108, "viewers: 2  ·  many read, one controls", size=10.5, fill=MUTE)
    cx, cw = x + 208, w - 208 - 264
    rx, rw = x + w - 252, 252
    return (cx, cw), (rx, rw)

def where_rail(s, rx, ry, rw, rh, extra=None):
    s.rect(rx, ry, rw, rh, fill="#fff")
    s.text(rx + 12, ry + 22, "Where", size=13, weight=700)
    s.text(rx + 60, ry + 22, "what this case touched", size=10.5, fill=MUTE)
    s.line(rx, ry + 30, rx + rw, ry + 30)
    yy = ry + 46
    s.text(rx + 12, yy, "SOURCE", size=10, fill=MUTE, weight=700, ls="1"); yy += 6
    s.chip(rx + 12, yy, "ticket system · SD-4471 ↗", "info"); yy += 30
    s.text(rx + 12, yy, "SYSTEMS TOUCHED", size=10, fill=MUTE, weight=700, ls="1"); yy += 6
    where_chip_row(s, rx + 12, yy, "PROD", "IPAL", "policy ••••7731", "read"); yy += 24
    where_chip_row(s, rx + 12, yy, "PROD", "INSIS", "policy ••••7731", "read"); yy += 24
    where_chip_row(s, rx + 12, yy, "PROD", "INSIS", "annex 0 · covers", "read"); yy += 34
    s.text(rx + 12, yy, "PAPERS", size=10, fill=MUTE, weight=700, ls="1"); yy += 18
    for name, st in [("Plan", "v2 · current"), ("Semantic contract", "v3 · 2 facts"), ("Write log", "empty · never shows rows"), ("Build state", "—"), ("MISSING", "0 items"), ("Report", "draft")]:
        s.text(rx + 12, yy, name, size=11.5, fill=INFO); s.text(rx + rw - 12, yy, st, size=10.5, fill=MUTE, anchor="end"); yy += 18
    yy += 10
    s.text(rx + 12, yy, "LINKED", size=10, fill=MUTE, weight=700, ls="1"); yy += 18
    s.text(rx + 12, yy, "precedent case #0977 · resolved", size=11.5, fill=INFO); yy += 18
    s.text(rx + 12, yy, "knowledge · premium-difference pattern", size=11.5, fill=INFO); yy += 18
    s.text(rx + 12, yy, "consults: none · sub-cases: none", size=11.5, fill=MUTE); yy += 28
    if extra:
        for ln in extra: s.text(rx + 12, yy, ln, size=11, fill=MUTE); yy += 16
    s.text(rx + 12, ry + rh - 14, "Expand ↗  (full map, all sessions)", size=11, fill=INFO)

# ---------------------------------------------------------------- 03 Case chat
def scr_case_chat():
    s = Svg("03 Case chat — the workspace: brief, steps, tools, questions, plan; where-rail on the right")
    x, y, w, h = shell(s, "Cases", "Cases  ›  #1042 Premium differs between the two systems", "Mira D. · Operator · controller")
    (cx, cw), (rx, rw) = case_frame(s, x, y, w, h - 30, 1, ("running", "ok"))
    s.clip_start("thread3", cx - 2, y - 2, cw + 4, h - 30 - 96 - 6)
    yy = y
    yy = card(s, cx, yy, cw, "brief", "SD-4471 — premium differs between systems · policy ••••7731",
              ["Support · incident · §1 Sev 2 · clocks started 09:12 · classification recorded automatically (H1) at 09:15"],
              chips=[("PROD · IPAL · policy ••••7731", "mono"), ("PROD · INSIS · policy ••••7731", "mono"), ("source ↗", "info")], meta="09:15")
    yy = card(s, cx, yy, cw, "agent", "S2 investigation — reading the premium on both systems",
              ["No ready solution at S3; investigating. Step 1/3: read premium and covers for annex 0 on both systems; compare cover by cover."], meta="09:16")
    # tool cards
    s.rect(cx + 20, yy, cw - 20, 56, fill=FILL, stroke=LINE)
    s.text(cx + 32, yy + 17, "TOOL  sql · read", size=9.5, fill=MUTE, weight=700, ls="1")
    where_chip_row(s, cx + 130, yy + 4, "PROD", "IPAL", "PR_POLICY_PREMIUM", "read")
    s.text(cx + 32, yy + 35, "$ premium for policy ••••7731, annex 0   →  1 row · 512.40 EUR · 0.3 s", size=11.5, mono=True)
    s.text(cx + 32, yy + 49, "read-only connector scope · result kept with the case, not in the chat log", size=10.5, fill=MUTE)
    yy += 64
    s.rect(cx + 20, yy, cw - 20, 56, fill=FILL, stroke=LINE)
    s.text(cx + 32, yy + 17, "TOOL  sql · read", size=9.5, fill=MUTE, weight=700, ls="1")
    where_chip_row(s, cx + 130, yy + 4, "PROD", "INSIS", "GEN_RISK_COVERED", "read")
    s.text(cx + 32, yy + 35, "$ covers for policy ••••7731, annex 0   →  6 rows · sum 498.10 EUR · 0.4 s", size=11.5, mono=True)
    s.text(cx + 32, yy + 49, "diff: loading L3 (rate 2.9 %) present on IPAL, absent on INSIS", size=10.5, fill=MUTE)
    yy += 64
    yy = card(s, cx, yy, cw, "agent", "Finding — 14.30 EUR = one loading (L3) missing on INSIS",
              ["Semantic contract v3 updated: fact 2 “INSIS annex 0 lacks loading L3”. Precedent #0977 had the same shape.",
               "I cannot tell which amount the customer intends; that is a person’s call."], meta="09:21")
    yy = card(s, cx, yy, cw, "question", "Which amount is intended for this policy?  (parked · question · 2 min)",
              ["Answered by Mira D. at 09:24: “IPAL 512.40 EUR is right — L. Andreev confirmed by phone.”"],
              chips=[("IPAL 512.40 EUR ✓", "ok"), ("INSIS 498.10 EUR", "mute"), ("ask the customer", "mute")], meta="09:22")
    yy = card(s, cx, yy, cw, "plan", "Plan v2 — S3 solution take: one write at S5; packet to H3 at S4",
              ["1 write · INSIS · GEN_RISK_COVERED · insert loading L3 for annex 0 · shape insert-loading-row (approved) · reversible · 5-step write",
               "S6 verification re-reads the customer’s symptom on both systems; the reply to the customer is a send (H5-send)."],
              buttons=[("Open plan", "mute"), ("Open packet preview", "mute")], meta="09:25")
    s.clip_end()
    status_line(s, cx, y + h - 30 - 96, cw, ["running", "S2 → S3", "3 tool calls", "12 min", "human 0.4 h", "budget 1.6 h left"])
    composer(s, cx, y + h - 30 - 66, cw, "Message the case…   /steer  /answer  /stop  /handover  /consult  /park  /where  /papers  /help",
             "permission: you control session 3 · the agent asks, you decide")
    where_rail(s, rx, y, rw, h - 30)
    footer(s, "03 · Case chat", "the workspace. Cards: brief · agent · tool · question · plan · decision · verdict · write · handover · status. Every card carries where-chips. Right rail = Where.")
    save(s, "wf-case-chat.svg")

# ---------------------------------------------------------------- 04 Case at gate
def scr_case_gate():
    s = Svg("04 Case at a gate — the decision is a card in the same chat; only the role holder sees the options")
    x, y, w, h = shell(s, "Cases", "Cases  ›  #1042 Premium differs between the two systems", "Georgi S. · Approver(PROD)")
    (cx, cw), (rx, rw) = case_frame(s, x, y, w, h - 30, 4, ("at gate · H5", "gate"), controller="Mira D.")
    s.clip_start("thread4", cx - 2, y - 2, cw + 4, h - 30 - 96 - 6)
    yy = y
    yy = card(s, cx, yy, cw, "verdict", "S4 confirmation — H3 given by Georgi S. at 09:40: plan v2 as proposed",
              ["Plan presented at S3 (H2). The holder of Approver(PROD) confirmed the mechanism and the single write."], meta="09:40")
    yy = card(s, cx, yy, cw, "agent", "S5 application — step 2 of plan v2: requesting the write", ["The write executor holds the row until the gate is decided. Nothing has been written."], meta="09:41")
    # decision card — focused: who proposes what, one line each, the options, details collapsed
    dh = 262
    s.rect(cx, yy, cw, dh, fill=AMBER_L, stroke=AMBER, sw=1.5)
    s.rect(cx, yy, 4, dh, fill=AMBER, stroke="none", rx=2)
    s.text(cx + 14, yy + 20, "DECISION", size=9.5, fill="#7a4d00", weight=700, ls="1")
    s.text(cx + 88, yy + 20, "H5 · HW-instance — write 1 row", size=12.5, weight=600)
    s.text(cx + cw - 10, yy + 20, "09:41 · waiting 8 min", size=10.5, fill=MUTE, anchor="end")
    s.text(cx + 14, yy + 40, "Support › application agent › case #1042 · step 2 of plan v2 · to you (Approver PROD) · controller Mira D.", size=11.5, fill="#374151")
    ty = yy + 66
    for k, v in [("WHAT", "insert 1 row · loading L3 · rate 2.9 % · annex 0 · 14.30 EUR · reversible (delete by id)"), ("WHERE", None),
                 ("WHY", "semantic contract v3 fact 2 · the answer “IPAL is right” · same shape as case #0977")]:
        s.text(cx + 14, ty, k, size=10, fill="#7a4d00", weight=700, ls="1")
        if k == "WHERE": where_chip_row(s, cx + 90, ty - 13, "PROD", "INSIS", "GEN_RISK_COVERED", "write")
        else: s.text(cx + 90, ty, v, size=12)
        ty += 24
    s.buttons(cx + 14, ty + 2, [("Approve — grant 30 min", "gate"), ("Reply with a different approach…", "mute"), ("Refuse…", "warn")])
    s.text(cx + 14, ty + 50, "▸ Details", size=11.5, weight=600)
    s.text(cx + 84, ty + 50, "evidence 3 · shape insert-loading-row 2 / 3 · blast radius 1 row · policy checks 4 ✓ · grant 30 min", size=10.5, fill=MUTE)
    para(s, cx + 14, ty + 72, cw - 28, "You see the options because you hold Approver for PROD writes — a permission, not a second person: an Operator who holds it decides their own case in one action. This estate withholds it for PROD, so Mira D. sees “waiting for Georgi S. — 8 min” here.", 10.5, MUTE, italic=True, lh=14)
    yy += dh + 10
    s.clip_end()
    status_line(s, cx, y + h - 30 - 96, cw, ["at gate", "H5 · HW-instance", "waiting 8 min", "human 0.7 h", "grant on approve: 30 min"])
    composer(s, cx, y + h - 30 - 66, cw, "Reply to the agent — a question, or a different approach (it re-plans)…", "permission: Approver(PROD, write) · read + decide · not controlling")
    where_rail(s, rx, y, rw, h - 30, extra=["THIS DECISION", "grant = env × system × target × mode", "artefact hash 9f3c…e1 · expiry 30 min", "executor: write executor (not the agent)"])
    footer(s, "04 · Case at a gate", "backs: GATES · GATE_DECISIONS · GRANTS · WRITE_SHAPES · HELD_BATCHES. Same chat, one more card kind; the role check decides who sees the options. PROD is customer-facing → H5.")
    save(s, "wf-case-gate.svg")



# ---------------------------------------------------------------- 05 Where (expanded)
def scr_where():
    s = Svg("05 Where — the full map of what a case touched, by environment and system")
    x, y, w, h = shell(s, "Cases", "Cases  ›  #1042  ›  Where", "any role · read")
    s.text(x, y + 18, "Where — case #1042", size=18, weight=700)
    s.text(x + 200, y + 18, "every handle, every system, every mode, every paper, every session — one map; the same chips as in the chat", size=12, fill=MUTE)
    envs = [("PROD", "customer-facing · H5 applies", [("IPAL", [("policy ••••7731", "read"), ("PR_POLICY_PREMIUM", "read")]),
                                                      ("INSIS", [("policy ••••7731", "read"), ("GEN_RISK_COVERED", "write · pending gate")]),
                                                      ("ticket system", [("SD-4471", "read · source"), ("reply draft", "send · H5 · later")])], 400),
            ("TEST", "working env", [("IPAL", [("— not touched", None)]), ("INSIS", [("— not touched", None)])], 170),
            ("DEV", "estate", [("git", [("— not touched", None)])], 170)]
    ex = x
    for env, note, systems, colw in envs:
        s.rect(ex, y + 40, colw, 420, fill="#fff")
        s.text(ex + 12, y + 62, env, size=14, weight=700, mono=True)
        s.text(ex + 64, y + 62, note, size=10.5, fill=MUTE)
        s.line(ex, y + 72, ex + colw, y + 72)
        yy = y + 90
        for sysname, objs in systems:
            s.rect(ex + 10, yy, colw - 20, 22 + len(objs) * 22, fill=FILL, stroke=LINE)
            s.text(ex + 20, yy + 16, sysname, size=12, weight=700)
            oy = yy + 22
            for obj, mode in objs:
                s.text(ex + 22, oy + 15, obj, size=11.5, fill=INK if mode else MUTE, mono=bool(mode))
                if mode:
                    kind = "ok" if mode.startswith("read") else ("gate" if ("gated" in mode or "H5" in mode) else "warn")
                    s.chip(ex + colw - 20 - 8 - (len(mode) * 6.4 + 14), oy + 3, mode, kind, size=10.5)
                oy += 22
            yy += 22 + len(objs) * 22 + 12
        ex += colw + 16
    px = x + 400 + 170 + 170 + 48
    pw = w - (400 + 170 + 170 + 48)
    s.panel(px, y + 40, pw, 200, "Papers", "kept with the case")
    yy = y + 84
    for name, st in [("Plan", "v2"), ("Semantic contract", "v3 · 2 facts"), ("Write log", "0 writes · ids only"), ("Build state", "—"), ("MISSING", "0"), ("Report", "draft")]:
        s.text(px + 12, yy, name, size=11.5, fill=INFO); s.text(px + pw - 12, yy, st, size=11, fill=MUTE, anchor="end"); yy += 22
    s.panel(px, y + 252, pw, 208, "Sessions & links")
    yy = y + 296
    for ln, col in [("session 3 · live · controller Operator", OK), ("session 2 · stopped · 08:40 → 09:05", MUTE), ("session 1 · stopped · intake", MUTE),
                    ("sub-cases: none", MUTE), ("consults: none", MUTE), ("precedent #0977 · knowledge article", INFO), ("held batches: 1 (S5 write)", "#7a4d00")]:
        s.text(px + 12, yy, ln, size=11.5, fill=col); yy += 22
    s.text(x, y + 490, "modes:", size=11, fill=MUTE); s.chips(x + 46, y + 477, [("read", "ok"), ("write · gated", "gate"), ("ddl · gated (HW-ddl)", "warn"), ("send · H5", "gate")])
    s.text(x, y + 520, "Objects show handles, not raw identifiers; the case keeps the mapping. Nothing here is typed by a person — it is the trace of what the agent actually touched.", size=11.5, fill=MUTE)
    footer(s, "05 · Where", "backs: TASKS (tool calls with env · system · object · mode) · PAPERS · SESSIONS · GATES · CONNECTOR_SCOPES. Reachable from every card’s where-chips and from the rail.")
    save(s, "wf-case-where.svg")



# ---------------------------------------------------------------- 06 Decisions
def scr_decisions():
    s = Svg("06 Decisions — every waiting gate across all cases, who proposes it and who it waits on")
    x, y, w, h = shell(s, "Decisions", "Decisions", "Georgi S. · Approver(PROD) · Operator")
    s.text(x, y + 18, "Decisions", size=18, weight=700)
    s.text(x + 100, y + 18, "the Approver’s front door — every row names the agent that proposes and the person it waits on; the row opens the case at its gate", size=12, fill=MUTE)
    s.chips(x, y + 30, [("pending 3", "gate"), ("mine 2", "ink"), ("held batches 2", "gate"), ("shapes 4", "mute"), ("history", "mute")])
    cols = ["gate", "case", "proposed by (module › agent)", "what", "where", "age", "waits on"]
    colw = [100, 204, 186, 170, 176, 52, w - 888]
    rows = [[("H5 · HW-inst.", "gate"), "#1040 Transfer fails for annex 3", "Support › application agent", "plan v2 step 2 · 1 insert · reversible", "PROD · INSIS · GEN_RISK_COVERED", "8 min", "Georgi S. · Approver(PROD)"],
            [("H3 confirm", "gate"), "#1041 New vehicle model in pricing", "Support › resolution agent", "plan v1 · add make + model rows", "TEST · IPAL · nomenclature", "42 min", "Reneta E. · Approver(TEST)"],
            [("H5-send", "gate"), "#1036 Sync framework 1101", "Configuration › serdica agent", "reply to T. Yotova · 2 attachments", "Bulstrad · ticket SD-4402", "1 h", "Vladimir M. · Approver(send)"]]
    yy = s.table(x, y + 60, w, cols, rows, rowh=30, colw=colw, size=11.5)
    yy += 16
    half = (w - 16) / 2
    s.panel(x, yy, half, 170, "Held batches", "writes the executor is holding — each is a case, not a queue item")
    hb = [("#1040 · batch 2", "12 rows · update · INSIS PROD", "Georgi S. · HW-approve · 8 min", "gate"),
          ("#1029 · batch 1", "DDL · index · IPAL PROD", "Vladimir M. · HW-ddl · 2 h", "warn")]
    hy = yy + 46
    for a, b, c, k in hb:
        s.text(x + 12, hy, a, size=12, weight=600); s.text(x + 126, hy, b, size=11.5); s.chip(x + 306, hy - 13, c, k); hy += 30
    para(s, x + 12, hy + 6, half - 24, "Release · Refuse · Split — all three are decisions with a packet; releasing writes nothing until the grant exists.", 11, MUTE, lh=15)
    sx = x + half + 16
    s.panel(sx, yy, half, 170, "Shapes — trainable gates", "3 clean person-confirmed instances → propose HW-approve")
    sh = [("insert-loading-row", "draft", "3 / 3 clean → proposal to Vladimir M.", "mute"), ("update-commission-rate", "draft", "1 / 3 clean", "mute"),
          ("delete-duplicate-rate", "suspended", "refused 12.08 by Georgi S.", "warn"), ("add-nomenclature-rows", "draft", "2 / 3 clean", "mute")]
    hy = yy + 46
    for a, st, prog, k in sh:
        s.text(sx + 12, hy, a, size=12, mono=True); s.chip(sx + 200, hy - 13, st, "ok" if st == "approved" else ("warn" if st == "suspended" else "mute")); s.text(sx + 300, hy, prog, size=11.5, fill=MUTE); hy += 24
    s.text(sx + 12, hy + 4, "The target Approver confirms a shape; a failed matching instance suspends it.", size=11, fill=MUTE)
    yy += 186
    s.panel(x, yy, w, 200, "History", "last decisions · every one names a gate, a case, a person and a reason")
    s.table(x + 12, yy + 44, w - 24, ["when", "gate", "case", "decision", "by", "reason / note"],
            [["09:40", ("H3 confirm", "gate"), "#1035 Annex commission only decreases", ("approved", "ok"), "Reneta E. · Approver(TEST)", "mechanism verified on TEST; plan v1 as proposed"],
             ["09:24", ("question", "info"), "#1042 Premium differs between systems", ("answered", "ok"), "Mira D. · controller", "“IPAL is right — customer confirmed by phone”"],
             ["09:15", ("H1 classification", "gate"), "#1042 Premium differs between systems", ("recorded", "ok"), "classification policy", "opened automatically — Support · incident · §1 Sev 2"],
             ["yesterday", ("HW-instance", "gate"), "#1029 Index on SRD_CUST", ("refused", "warn"), "Georgi S. · Approver(PROD)", "DDL — goes through HW-ddl and the write executor (D104)"],
             ["yesterday", ("publish", "gate"), "Support · S2 classify · v12", ("published", "ok"), "Vladimir M. · Prompt publisher", "eval 85 % ≥ 80 %; rollback point v11"]],
            colw=[76, 118, 236, 92, 176, w - 24 - 698], rowh=26, size=11.5)
    para(s, x, yy + 214, w, "Gate kinds on this list:  H1–H7 · Hd · HW-approve · HW-instance · HW-shared · HW-irreversible · HW-ddl · H5-SIM · H5-send · CG-11 · publish.   H1–H3 and H6 reach the controlling Operator in the chat; they surface here only when unanswered past their clock.", 10.5, MUTE, lh=14)
    footer(s, "06 · Decisions", "backs: GATES · HELD_BATCHES · WRITE_SHAPES · GATE_DECISIONS. Row → the case at its gate (screen 04) or the plan gate (screen 07). The SLA-visible queue of human work.")
    save(s, "wf-decisions.svg")

# ---------------------------------------------------------------- 07 Plan gate
def scr_plan_gate():
    s = Svg("07 Plan gate — the agent proposes a plan; a person approves it, replies with another approach, or refuses")
    x, y, w, h = shell(s, "Decisions", "Decisions  ›  #1040  ›  plan v2", "Georgi S. · Approver(PROD) · Operator")
    # header band: who proposes what to whom
    s.rect(x, y, w, 74, fill="#fff")
    s.rect(x, y, 4, 74, fill=AMBER, stroke="none", rx=2)
    s.chip(x + 16, y + 12, "PLAN GATE · H3 confirmation at S4", "gate")
    s.chip(x + 236, y + 12, "a gate: the plan is what you approve — each write or send step asks again when it runs", "mute")
    s.text(x + 16, y + 50, "Support", size=14, weight=700); s.text(x + 78, y + 50, "›", size=14, fill=MUTE)
    s.text(x + 92, y + 50, "solution-take agent", size=14, weight=700); s.text(x + 236, y + 50, "›", size=14, fill=MUTE)
    s.text(x + 250, y + 50, "case #1040 Transfer fails for annex 3 — policy ••••0028", size=14, weight=700)
    s.text(x + 16, y + 66, "proposes plan v2  ·  to Georgi S. (Approver PROD)  ·  requested 09:41  ·  controller Mira D.  ·  customer Bulstrad · §1 Sev 2 · resolution 3 bd left", size=11.5, fill=MUTE)
    # left: the plan
    lw = 636
    ty = y + 90
    s.panel(x, ty, lw, h - 90 - 30, "The plan", "v2 · 4 steps · 1 write · 1 send · what changes and where")
    yy = ty + 50
    steps = [("done", "1", "Read the annex chain for policy ••••0028 on both systems; find where the transfer stopped.",
              [("PROD", "INSIS", "annex ••••0028/3", "read"), ("PROD", "IPAL", "annex ••••0028/3", "read")], None, "done 09:32 · 2 reads"),
             ("now", "2", "Insert the missing loading row for annex 3 on INSIS — 1 row · GEN_RISK_COVERED · 14.30 EUR · reversible (delete by id).",
              [("PROD", "INSIS", "GEN_RISK_COVERED", "write")], ("H5 · HW-instance · asks again when it runs", "gate"), "shape insert-loading-row · training · 2 / 3 clean"),
             ("next", "3", "Re-read the premium on both systems; expect equality; re-check the customer’s symptom on the transfer screen.",
              [("PROD", "INSIS", "policy ••••0028", "read"), ("PROD", "IPAL", "policy ••••0028", "read")], None, "S6 verification"),
             ("next", "4", "Reply to L. Andreev in customer words; attach the before/after premium.",
              [("Bulstrad", "ticket system", "SD-4471", "send")], ("H5-send · asks again", "gate"), "S5 · customer-visible")]
    for st, n, txt, wheres, gate, note in steps:
        col = OK if st == "done" else (AMBER if st == "now" else LINE)
        s.circle(x + 26, yy - 2, 10, col if st != "next" else "#fff", stroke=col)
        s.text(x + 26, yy + 2, n, size=11, weight=700, fill="#fff" if st != "next" else MUTE, anchor="middle")
        ny = para(s, x + 46, yy + 2, lw - 60, txt, 12.5, INK, lh=17)
        cx = x + 46
        for env, sysn, obj, mode in wheres:
            cx = s.chip(cx, ny - 8, env, "mono") + cx + 4
            cx = s.chip(cx, ny - 8, sysn, "mono") + cx + 4
            cx = s.chip(cx, ny - 8, obj, "mono") + cx + 4
            cx = s.chip(cx, ny - 8, mode, "ok" if mode == "read" else "gate") + cx + 14
        if gate: s.chip(cx, ny - 8, gate[0], gate[1])
        s.text(x + 46, ny + 26, note, size=10.5, fill=MUTE)
        if st != "next" or n != "4": s.line(x + 26, yy + 10, x + 26, ny + 34, stroke=LINE)
        yy = ny + 46
    yy += 4
    s.text(x + 14, yy, "WHY THIS PLAN", size=10, fill=MUTE, weight=700, ls="1"); yy += 6
    yy = para(s, x + 14, yy + 12, lw - 28, "The transfer stopped at annex 3 because INSIS lacks loading L3 (semantic contract v3, fact 2); IPAL is right per the customer (question answered 09:24); the same shape resolved case #0977.", 12, INK, lh=17) + 10
    s.text(x + 14, yy, "ALTERNATIVES THE AGENT REJECTED", size=10, fill=MUTE, weight=700, ls="1"); yy += 18
    for ln in ["• Re-run the transfer job — would duplicate the covers (N² pattern, knowledge: transfer failure · annex chain).",
               "• Change the IPAL side instead — contradicts the customer’s answer; IPAL holds the intended amount.",
               "• Delete and re-transfer the annex — irreversible on INSIS (HW-irreversible), not needed for one row."]:
        yy = para(s, x + 14, yy, lw - 28, ln, 11.5, "#374151", lh=16) + 4
    # right: your decision
    rx, rw = x + lw + 16, w - lw - 16
    s.panel(rx, ty, rw, 372, "Your decision", "as in a coding CLI’s plan mode — pick one")
    yy = ty + 50
    opts = [("Approve the plan", "step 2 asks again when it runs (H5 · HW-instance) — a grant of 30 min is issued then", True, "gate"),
            ("Approve and pre-approve step 2", "one decision for plan and write, where policy permits — not here: No risk acceptance exists for this PROD shape", False, "disabled"),
            ("Reply with a different approach", "tell the agent what to do differently; it re-plans and comes back with v3", True, "mute"),
            ("Refuse", "with a reason; the case parks (gate) and the controller is told", True, "warn")]
    for label, desc, on, k in opts:
        s.circle(rx + 22, yy - 4, 7, "#fff", stroke=INK if on else LINE)
        if label.startswith("Approve the plan"): s.circle(rx + 22, yy - 4, 3.5, INK)
        s.text(rx + 36, yy, label, size=12.5, weight=600, fill=INK if on else MUTE)
        yy = para(s, rx + 36, yy + 16, rw - 50, desc, 11, MUTE if on else "#9aa0a6", lh=14) + 10
    s.rect(rx + 14, yy, rw - 28, 54, fill="#fff", stroke=LINE, rx=6)
    s.text(rx + 24, yy + 20, "Tell the agent what to do differently…", size=12, fill=MUTE)
    s.text(rx + 24, yy + 40, "e.g. “fix on TEST first and show me the diff before PROD”", size=10.5, fill=MUTE, italic=True)
    yy += 66
    s.buttons(rx + 14, yy, [("Approve", "gate"), ("Send reply", "mute"), ("Refuse…", "warn")])
    # details, collapsed
    dy = ty + 388
    s.panel(rx, dy, rw, h - 90 - 30 - 388, "Details", "collapsed — one click each")
    yy = dy + 48
    for k_, v in [("Evidence", "3 · two reads · screenshot evidence/1"), ("Statement", "INSERT … parameters shown · rows never"), ("Reversible", "yes · delete by returned id"),
                  ("Blast radius", "1 policy · 1 annex · 1 row · PROD"), ("Policy checks", "kill switch off · scope · budget · role — 4 ✓"), ("Shape", "insert-loading-row · training · 2 / 3 clean"),
                  ("Grant at step 2", "PROD · INSIS · GEN_RISK_COVERED · write · 30 min"), ("History", "proposed 09:41 · Mira D. asked 09:43 · opened 09:49")]:
        s.text(rx + 14, yy, "▸ " + k_, size=11.5, weight=600); s.text(rx + rw - 14, yy, v, size=10.5, fill=MUTE, anchor="end"); yy += 22
    footer(s, "07 · Plan gate", "backs: GATES (H2 presents at S3 · H3 confirms at S4) · PAPERS Plan v2 · GATE_DECISIONS · WRITE_SHAPES · GRANTS (issued when a write step runs). A reply re-opens planning, not the gate.")
    save(s, "wf-plan-gate.svg")


# ---------------------------------------------------------------- 08 Studio
def scr_studio():
    s = Svg("08 Studio — prompts, skills and eval sets per module and stage; draft → eval → publish → rollback")
    x, y, w, h = shell(s, "Studio", "Studio", "Prompt publisher · Operator")
    s.text(x, y + 18, "Studio", size=18, weight=700)
    s.text(x + 72, y + 18, "what the agents are made of — every artefact is versioned, evaluated against a set, and published by the prompt-publisher role", size=12, fill=MUTE)
    tw = 264
    s.panel(x, y + 40, tw, h - 80, "Artefacts", "module › stage › artefact")
    tree = [("Platform", 0, None), ("intake brief · prompt v9", 1, "published"), ("Support", 0, None), ("S1 intake · prompt v7", 1, "published"),
            ("S2 classify · prompt v12 · v13 draft", 1, "draft"), ("S3 analyse · analyze-policy v4", 1, "published"), ("S5 fix · write-request v2", 1, "published"),
            ("S7 reply · prompt v5", 1, "published"), ("eval set classify-A · 20 cases", 1, "set"), ("Configuration", 0, None), ("S2 · skill add-car v6", 1, "published"),
            ("S3 · abacus configure v3", 1, "eval"), ("Development", 0, None), ("S6 port · prompt v2", 1, "published"), ("Consults", 0, None), ("estate probe · skill v1", 1, "draft")]
    yy = y + 84
    for label, lvl, st in tree:
        sel = label.startswith("S2 classify")
        if sel: s.rect(x + 6, yy - 14, tw - 12, 22, fill=INFO_L, stroke="none", rx=4)
        s.text(x + 14 + lvl * 16, yy, ("▾ " if lvl == 0 else "") + label, size=12 if lvl == 0 else 11.5, weight=700 if lvl == 0 else 400)
        if st: s.chip(x + tw - 14 - (len(st) * 6.4 + 14), yy - 13, st, {"published": "ok", "draft": "mute", "eval": "info", "set": "mute"}[st], size=10.5)
        yy += 26 if lvl == 0 else 24
    cx, cw = x + tw + 16, w - tw - 16 - 292
    s.panel(cx, y + 40, cw, 320, "Support · S2 classify · prompt", "versions")
    s.table(cx + 12, y + 84, cw - 24, ["version", "state", "author (owner)", "eval classify-A", "since"],
            [["v13", ("draft", "mute"), "owner · Support S2", "18 / 20 · 90 %", "today 08:10"],
             ["v12", ("published", "ok"), "owner · Support S2", "17 / 20 · 85 %", "02.09 · rollback pt."],
             ["v11", ("retired", "mute"), "owner · Support S2", "15 / 20 · 75 %", "14.08"],
             ["v10", ("retired", "mute"), "—", "14 / 20", "01.08"]],
            colw=[60, 96, 124, 112, cw - 24 - 392], rowh=26)
    s.buttons(cx + 12, y + 230, [("New draft", "primary"), ("Run eval", "mute"), ("Publish v13", "gate"), ("Rollback", "mute")])
    ty = para(s, cx + 12, y + 280, cw - 24, "publish = gate “publish”: green eval set, the publisher role for this stage, rollback point recorded — a role, not a review. New sessions get the new version; running sessions keep theirs.", 11, MUTE, lh=15)
    para(s, cx + 12, ty + 4, cw - 24, "A draft may be tried on ONE case by its controller (“try v13 here”) — that is how prompts are trained by people, case by case.", 11, MUTE, lh=15)
    s.panel(cx, y + 376, cw, h - 80 - 336, "Where this artefact is used")
    yy = y + 420
    for ln in ["Support S2 classify — every Support case (486 in the baseline year)", "Consulted by: platform.intake (proposed classification field)", "Reads: knowledge · classification patterns (active) · contract clauses §1 / §2",
               "Writes: nothing — classification is a paper, the Operator confirms it (H1)"]:
        yy = para(s, cx + 12, yy, cw - 24, "• " + ln, 11.5, "#374151", lh=16) + 6
    ex = cx + cw + 16
    s.panel(ex, y + 40, 276, h - 80, "Eval set classify-A", "keyed by a person")
    yy = y + 84
    s.text(ex + 12, yy, "last run · v13 · today 08:12", size=11.5, fill=MUTE); yy += 22
    s.bar(ex + 12, yy, 252, 90, "ok"); yy += 22
    s.text(ex + 12, yy, "18 / 20 pass · 2 fail", size=12, weight=600); yy += 26
    s.text(ex + 12, yy, "FAILURES", size=10, fill=MUTE, weight=700, ls="1"); yy += 18
    for ln in ["case 0912 · expected §2 P2 · got §1 Sev 3", "case 0871 · expected question · got incident"]:
        s.text(ex + 12, yy, ln, size=11.5, fill=INFO); yy += 18
    yy += 8
    s.text(ex + 12, yy, "ADD FROM", size=10, fill=MUTE, weight=700, ls="1"); yy += 18
    for ln in ["closed case with a corrected classification (H1 changed the proposal) → 1 click", "a refused gate → the packet becomes a negative example"]:
        yy = para(s, ex + 12, yy, 250, "• " + ln, 11, "#374151", lh=15) + 4
    yy += 10
    s.buttons(ex + 12, yy, [("Run on v13", "mute"), ("Open cases", "mute")])
    footer(s, "08 · Studio", "backs: SKILLS · EVAL_SETS · prompt governance tables (versions: draft · eval · published · retired) · gate “publish” · rollback point. Publishing is a role, not a review; one owner per prompt.")
    save(s, "wf-studio.svg")

# ---------------------------------------------------------------- 09 Studio version (diff + publish)
def scr_studio_version():
    s = Svg("09 Studio — one version: the diff, the eval evidence, and the publish decision")
    x, y, w, h = shell(s, "Studio", "Studio  ›  Support · S2 classify  ›  v13 (draft) vs v12 (published)", "Prompt publisher")
    lw = 560
    s.panel(x, y, lw, h - 40, "Diff", "v12 → v13 · 3 changes · by the prompt’s owner · reason: “§2 requests misread as incidents”")
    yy = y + 48
    lines = [(" ", "You classify an arrival for the Support module. Output: type,"),
             (" ", "contract section, severity, two clocks."),
             (" ", "Rules:"),
             ("-", "  If the reporter asks for a new value in a nomenclature, treat it"),
             ("-", "  as an incident."),
             ("+", "  If the reporter asks for a new value in a nomenclature (vehicle"),
             ("+", "  model, agency, blank range), it is a CONFIGURATION request under"),
             ("+", "  §2 — propose owning module Configuration and priority P1–P3."),
             (" ", "  A question with no defect and no request is §1 Sev 4."),
             ("-", "  Prefer Sev 2 when premium amounts are mentioned."),
             ("+", "  Severity follows business impact, never the presence of amounts;"),
             ("+", "  cite the §1.5 clause you used."),
             (" ", "Always name the precedent cases you relied on, or say “none”.")]
    for mark, ln in lines:
        col = {"+": OK_L, "-": WARN_L, " ": "#fff"}[mark]
        s.rect(x + 12, yy - 13, lw - 24, 18, fill=col, stroke="none", rx=0)
        s.text(x + 18, yy, mark, size=11.5, mono=True, fill=OK if mark == "+" else (WARN if mark == "-" else MUTE))
        s.text(x + 34, yy, ln, size=11.5, mono=True); yy += 18
    yy += 14
    s.text(x + 12, yy, "TRIED ON CASES", size=10, fill=MUTE, weight=700, ls="1"); yy += 18
    for ln in ["#1041 · controller chose “try v13 here” · proposal accepted by H1 without change", "#1037 · v13 · proposal corrected by Operator (question → incident)"]:
        s.text(x + 12, yy, "• " + ln, size=11.5, fill="#374151"); yy += 18
    yy += 14
    s.text(x + 12, yy, "PUBLISH CHECKS", size=10, fill=MUTE, weight=700, ls="1"); yy += 6
    s.chips(x + 12, yy, [("eval 90 % ≥ threshold 85 %", "ok"), ("publisher role for Support S2", "ok"), ("rollback point v12", "ok")]); yy += 24
    s.chips(x + 12, yy, [("no running session pinned to v12", "info"), ("2 cases tried v13 (1 corrected)", "info")]); yy += 36
    s.buttons(x + 12, yy, [("Publish v13", "gate"), ("Return to author with notes…", "mute"), ("Retire draft", "warn")])
    para(s, x + 12, yy + 44, lw - 24, "Publishing is a gate (“publish”) and is written to the register of decisions with the eval run it relied on.", 11, MUTE, italic=True, lh=15)
    rx, rw = x + lw + 16, w - lw - 16
    s.panel(rx, y, rw, 340, "Eval run · classify-A · v13", "20 cases · 08:12 today · 41 s")
    s.table(rx + 12, y + 44, rw - 24, ["case", "expected", "v12 got", "v13 got", "verdict"],
            [["0871", "question · §1 Sev 4", "incident · Sev 3", "incident · Sev 3", ("fail (both)", "warn")],
             ["0912", "request · §2 P2", "incident · Sev 3", "request · §2 P2", ("fixed", "ok")],
             ["0933", "incident · §1 Sev 2", "incident · Sev 2", "incident · Sev 2", ("pass", "ok")],
             ["0940", "request · §2 P3", "request · §2 P3", "request · §2 P2", ("fail (new)", "warn")],
             ["0955", "incident · §1 Sev 1", "incident · Sev 1", "incident · Sev 1", ("pass", "ok")],
             ["…", "15 more", "", "", ("pass", "ok")]],
            colw=[50, 124, 100, 100, rw - 24 - 374], rowh=26, size=11)
    ty = para(s, rx + 12, y + 256, rw - 24, "A fail is a case, too: open it, fix the answer key or the prompt, re-run. Answer keys are written by people, never by the agent.", 11, MUTE, lh=15)
    para(s, rx + 12, ty + 4, rw - 24, "Compare columns: what changed for the better (0912) and what regressed (0940) — both are visible before publishing.", 11, MUTE, lh=15)
    s.panel(rx, y + 356, rw, h - 40 - 356, "Roll-out and rollback")
    yy = y + 400
    for ln in ["• New sessions of Support S2 pick v13 the moment it is published.", "• Running sessions keep the version they started with (pinned).", "• Rollback to v12 is one click by a Prompt publisher; it is itself a “publish” decision.",
               "• Every case records which version it ran — Metrics compares versions on real cases.", "• The same lifecycle applies to skills and eval sets."]:
        yy = para(s, rx + 12, yy, rw - 24, ln, 11.5, "#374151", lh=16) + 6
    footer(s, "09 · Studio version", "backs: prompt governance tables (versions) · eval runs · GATE_DECISIONS (publish). Publishing is a role, not a review; the eval evidence is attached.")
    save(s, "wf-studio-version.svg")

# ---------------------------------------------------------------- 10 Knowledge
def scr_knowledge():
    s = Svg("10 Knowledge — the AGENTS.md tree, its proposals queue, and one experience entry; case-local → proposed → active → retired")
    x, y, w, h = shell(s, "Knowledge", "Knowledge", "Mira D. · Operator · curator support/experience")
    s.text(x, y + 18, "Knowledge", size=18, weight=700)
    s.text(x + 104, y + 18, "the memory tree of Agents Memory § 1 — one AGENTS.md per node, one writing profile per node; nothing becomes active without confirmation", size=12, fill=MUTE)
    tw = 290
    s.panel(x, y + 40, tw, h - 80, "Memory tree", "memory/ · node = AGENTS.md + articles")
    # (label, level, badge, state)  badge = active article count
    tree = [("memory/AGENTS.md — the protocol", 0, "", ""),
            ("platform/", 0, "", ""), ("intake/", 1, "9", ""), ("experience/", 1, "31", ""), ("handovers/", 1, "6", ""),
            ("connectors/", 0, "", ""), ("oracle-ipal/", 1, "14", ""), ("oracle-insis/", 1, "17", ""), ("abacus-gateway/ jira/ mail/ hdesk/", 1, "22", ""), ("gitlab/ kibana/ rabbitmq/ browser/", 1, "11", ""),
            ("configuration/", 0, "", ""), ("normalization/", 1, "8", ""), ("abacus/", 1, "12", ""), ("ipal/", 1, "15", ""), ("offer/", 1, "7", ""), ("serdica/", 1, "10", ""),
            ("source/", 0, "", ""), ("serdica-backend/ › <microservice>/", 1, "23", ""), ("serdica-ui/", 1, "6", ""), ("intentgpt/", 1, "—", "first case"), ("db-plsql/", 1, "9", ""),
            ("support/", 0, "", ""), ("methodologies/", 1, "13", ""), ("experience/ — past cases (§ 6)", 1, "509", "selected")]
    yy = y + 82
    for label, lvl, badge, st in tree:
        if st == "selected": s.rect(x + 6, yy - 14, tw - 12, 22, fill=INFO_L, stroke="none", rx=4)
        s.text(x + 14 + lvl * 16, yy, ("▾ " if lvl == 0 and label.endswith("/") else "") + label, size=12 if lvl == 0 else 11.5, weight=700 if lvl == 0 else 400, mono=(lvl == 1), fill=INK if st != "first case" else MUTE)
        if badge: s.text(x + tw - 14, yy, badge, size=10.5, fill=MUTE, anchor="end", mono=True)
        yy += 25 if lvl == 0 else 22
    s.text(x + 14, yy + 6, "counts = active entries · node rule: Agents Memory § 2 rule 4", size=10, fill=MUTE)
    cx, cw = x + tw + 16, w - tw - 16 - 276
    s.panel(cx, y + 40, cw, 330, "Proposals", "4 waiting · from the agents that worked a case, or from the consolidation pass")
    props = [("case #1042", "support/experience", "entry · premium differs between systems · loading L3 missing", "gate"),
             ("case #1036", "configuration/serdica", "framework 1101 · mapping must exist before the child issues", "gate"),
             ("consolidation", "support/experience", "merge 3 entries on transfer failures · retire 2 · refresh AGENTS.md", "info"),
             ("case #1033", "support/methodologies", "customer-reply wording · “debit note”, never “DN”", "gate")]
    yy = y + 84
    for src, node, txt, k in props:
        s.text(cx + 12, yy, src, size=11, fill=MUTE); s.text(cx + 100, yy, "→  " + node, size=11, mono=True, fill=INFO)
        s.chip(cx + cw - 12 - 72, yy - 13, "proposed", k)
        s.text(cx + 12, yy + 16, txt, size=11.5); yy += 38
    yy -= 6
    s.buttons(cx + 12, yy, [("Confirm → active", "ok"), ("Return to case", "mute"), ("Retire", "warn"), ("Open case", "mute")])
    ty = para(s, cx + 12, yy + 46, cw - 24, "Confirmation (D56): the node’s curator reviews here, or two independent cases use the entry without contradiction. Only the node’s owning profile writes the shared tree; the entry keeps “taught by #1042” for ever.", 11, MUTE, lh=15)
    para(s, cx + 12, ty + 4, cw - 24, "Case-local memory lives in the case’s papers until proposed. The consolidation (“dreaming”) pass merges, prunes and refreshes a node’s AGENTS.md — as proposals, never as active writes.", 11, MUTE, lh=15)
    s.panel(cx, y + 386, cw, h - 80 - 346, "States")
    yy = y + 430
    xx = cx + 12
    for st, k, note in [("case-local", "mute", "in the case’s papers"), ("proposed", "gate", "waiting for confirmation"), ("active", "ok", "read by every agent"), ("retired", "info", "kept, not read")]:
        s.chip(xx, yy - 13, st, k); s.text(xx, yy + 18, note, size=10.5, fill=MUTE); xx += 124
        if st != "retired": s.text(xx - 22, yy, "→", size=14, fill=MUTE)
    para(s, cx + 12, yy + 50, cw - 24, "Every agent may read any node (§ 6b); it may answer only for its own domain. Retired entries follow the “knowledge” class of the retention policy.", 11, MUTE, lh=15)
    rx, rw = cx + cw + 16, 260
    s.panel(rx, y + 40, rw, h - 80, "Entry", "support/experience · v4")
    yy = y + 84
    for k_, v in [("SYMPTOM SIGNATURE", "“premium differs between the two systems” · no system error"), ("SYSTEMS · ENV", "IPAL + INSIS · PROD · annex 0"),
                  ("MECHANISM", "INSIS lacks one loading row after an annex (data layer)"), ("FIX SHAPE", "data fix · insert-loading-row"),
                  ("LINKS", "analysis #0977 · KI-31 · checklist compare-covers"), ("REUSABLE", "query pack compare-covers-by-annex"),
                  ("TAUGHT BY", "#0977 · #1012 · #1042 (proposed)"), ("READ BY", "Support S2 investigation · S3 solution take"), ("LAST USED", "today 09:21 · case #1042")]:
        s.text(rx + 12, yy, k_, size=9.5, fill=MUTE, weight=700, ls="1"); yy = para(s, rx + 12, yy + 15, rw - 24, v, 11.5, INK, lh=15) + 8
    s.buttons(rx + 12, yy + 2, [("Edit (new version)", "mute"), ("Retire", "warn")])
    s.rect(rx + 12, y + h - 80 - 40, rw - 24, 24, fill=FILL, stroke=LINE, rx=12)
    s.text(rx + 24, y + h - 80 - 23, "Search by symptom or node…", size=11.5, fill=MUTE)
    footer(s, "10 · Knowledge", "backs: the memory tree (Agents Memory § 1, one AGENTS.md per node) · entries (case-local → proposed → active → retired · taught-by · read-by) · consolidation proposals · AUDIT_LEDGER memory events.")
    save(s, "wf-knowledge.svg")

# ---------------------------------------------------------------- 11 Metrics
def scr_metrics():
    s = Svg("11 Metrics — hours and gates, not money; targets with their basis; per module, stage and version")
    x, y, w, h = shell(s, "Metrics", "Metrics", "Viewer · everyone")
    s.text(x, y + 18, "Metrics", size=18, weight=700)
    s.text(x + 78, y + 18, "measured in hours and minutes of people, counts of gates and cases — every target shows the basis it was set on", size=12, fill=MUTE)
    s.text(x + w, y + 18, "period: last 30 days ▾ · module: all ▾ · compare: previous 30 days", size=11.5, fill=MUTE, anchor="end")
    tiles = [("human minutes per case", "38 min", "target ≤ 45 · basis: 331 routine cases", "ok"), ("first person on a new arrival", "11 min", "target ≤ 30 · basis: two clocks", "ok"),
             ("gate wait (median)", "9 min", "target ≤ 15 · Approver queue", "ok"), ("gates per case", "3.1", "H1 · H3 · HW typical", "mute"),
             ("writes without a person", "0", "target 0 · policy", "ok"), ("thin shapes (bus factor)", "8 → 3", "target 0 by phase end · D115", "gate")]
    tx = x
    for title, val, sub, k in tiles:
        s.rect(tx, y + 40, 164, 92, fill="#fff")
        s.text(tx + 12, y + 60, title, size=10.5, fill=MUTE)
        s.text(tx + 12, y + 90, val, size=22, weight=700, fill={"ok": OK, "gate": "#7a4d00", "mute": INK}[k])
        s.text(tx + 12, y + 118, sub, size=9.5, fill=MUTE)
        tx += 172
    half = (w - 16) / 2
    s.panel(x, y + 148, half, 250, "Cases by state and module", "30 days")
    s.table(x + 12, y + 190, half - 24, ["module", "opened", "resolved", "at gate now", "parked now", "median to resolve"],
            [["Support", "41", "37", "2", "3", "26 h"], ["Configuration", "12", "11", "1", "1", "31 h"], ["Development", "3", "2", "0", "1", "5 bd"], ["Platform", "—", "—", "0", "0", "—"]],
            colw=[110, 64, 74, 92, 90, half - 24 - 430], rowh=26)
    s.text(x + 12, y + 340, "Clock breaches: 0 response · 1 resolution (#1033, budget-held 2 d). Breaches link to the case.", size=11, fill=MUTE)
    sx = x + half + 16
    s.panel(sx, y + 148, half, 250, "Gate throughput", "who waits on whom")
    yy = y + 192
    for gate, n, med, pct in [("H1 classification (Operator)", "126", "4 min", 90), ("H3 confirm (Approver)", "44", "12 min", 70), ("HW-instance (Approver)", "31", "9 min", 60), ("H5-send (Approver)", "38", "18 min", 50), ("publish (Prompt publisher)", "6", "2 h", 20), ("knowledge confirmed (curator)", "14", "1 d", 10)]:
        s.text(sx + 12, yy, gate, size=11.5); s.text(sx + 250, yy, n, size=11.5, anchor="end"); s.text(sx + 320, yy, med, size=11.5, fill=MUTE, anchor="end"); s.bar(sx + 340, yy - 9, half - 24 - 340, pct, "info"); yy += 26
    s.text(sx + 12, yy + 6, "count · median wait · share of cases touched", size=10.5, fill=MUTE)
    s.panel(x, y + 414, w, h - 40 - 414, "Targets and their basis", "hours only — D111 / D117")
    s.table(x + 12, y + 458, w - 24, ["metric", "now", "target", "basis (why this number)", "trend"],
            [["human minutes per routine case", "38", "≤ 45 → ≤ 30 in phase 2", "measured 38–40 min on 331 routine cases; not the unsupported “45”", ("improving", "ok")],
             ["cases with a DB write", "64 %", "keep visible, no target", "220 / 333 in the baseline — a fact about the work, not a goal", ("flat", "mute")],
             ["prompt version regressions caught before publish", "2 / 2", "100 %", "eval sets are the only gate on publish", ("ok", "ok")],
             ["knowledge proposals promoted within 5 bd", "71 %", "≥ 80 %", "curator duty is part-time; measured since 01.08", ("improving", "ok")],
             ["thin write shapes (single-holder)", "3", "0", "8 at baseline (D115) — trainable gates spread the knowledge", ("improving", "ok")]],
            colw=[290, 70, 170, w - 24 - 640, 110], rowh=26)
    footer(s, "11 · Metrics", "backs: CASES · GATES · GATE_DECISIONS · SESSIONS (human minutes) · PROMPT_VERSIONS (per-version comparison). No money anywhere; hours and counts only.")
    save(s, "wf-metrics.svg")

# ---------------------------------------------------------------- 12 Control · Connectors
def scr_control_connectors():
    s = Svg("12 Control · Connectors — where the platform may reach, in which mode, and whether it is healthy")
    x, y, w, h = shell(s, "Control", "Control  ›  Connectors", "Administrator")
    s.text(x, y + 18, "Control", size=18, weight=700)
    s.chips(x + 90, y + 4, [("connectors", "ink"), ("roles & policies", "mute"), ("exceptions & kill switch", "mute"), ("audit", "mute")])
    s.text(x, y + 44, "A connector scope is the ceiling: env × system × mode. A grant (issued by a gate) can never exceed it. Health and credentials live here too.", size=12, fill=MUTE)
    cols = ["environment", "system", "kind", "allowed modes", "customer-facing", "health", "credential", "last probe", ""]
    colw = [100, 130, 90, 190, 110, 90, 110, 110, w - 930]
    rows = [["PROD", "IPAL", "database", [("read", "ok"), ("write", "gate")], ("yes · H5", "gate"), ("ok", "ok"), "expires 30 d", "09:50 ok", "probe · edit"],
            ["PROD", "INSIS", "database", [("read", "ok"), ("write", "gate"), ("ddl", "warn")], ("yes · H5", "gate"), ("ok", "ok"), "expires 30 d", "09:50 ok", "probe · edit"],
            ["PROD", "ticket system", "api", [("read", "ok"), ("send", "gate")], ("yes · H5", "gate"), ("ok", "ok"), "token · 12 d", "09:48 ok", "probe · edit"],
            ["PROD", "mailbox", "api", [("read", "ok"), ("send", "gate")], ("yes · H5", "gate"), ("degraded", "gate"), "ok", "09:31 slow", "probe · edit"],
            ["TEST", "IPAL", "database", [("read", "ok"), ("write", "gate"), ("ddl", "warn")], ("no", "mute"), ("ok", "ok"), "ok", "09:50 ok", "probe · edit"],
            ["TEST", "INSIS", "database", [("read", "ok"), ("write", "gate")], ("no", "mute"), ("down", "warn"), "ok", "09:12 fail", "probe · edit"],
            ["QA", "IPAL", "database", [("read", "ok"), ("write", "gate")], ("no · H4", "mute"), ("ok", "ok"), "ok", "09:50 ok", "probe · edit"],
            ["DEV", "git", "repo", [("read", "ok"), ("push", "gate")], ("no", "mute"), ("ok", "ok"), "ssh key", "09:50 ok", "probe · edit"],
            ["—", "helpdesk", "api", [("read", "ok")], ("no", "mute"), ("ok", "ok"), "cached · DPAPI", "09:45 ok", "probe · edit"],
            ["—", "model provider", "llm", [("call", "ok")], ("—", "mute"), ("ok", "ok"), "key · 60 d", "09:50 ok", "probe · edit"]]
    yy = s.table(x, y + 62, w, cols, rows, rowh=30, colw=colw)
    yy += 16
    half = (w - 16) / 2
    s.panel(x, yy, half, 170, "Effects of this table", "read by every gate and every tool call")
    ty = yy + 44
    for ln in ["• A tool call outside the allowed modes is refused before it runs and shows as a REFUSED card in the case.", "• “customer-facing = yes” makes H5 mandatory on sends and writes (D114); QA deploys take H4 + write auditor.",
               "• A connector marked down parks its cases as connector-down; they resume when the probe is green.", "• Editing a scope is an Administrator decision, logged with reason."]:
        ty = para(s, x + 12, ty, half - 24, ln, 11.5, "#374151", lh=16) + 5
    sx = x + half + 16
    s.panel(sx, yy, half, 170, "Right now", "")
    ty = yy + 44
    for ln, k in [("TEST · INSIS is down since 09:12 — 1 case parked (#1035 waits for verify)", "warn"), ("PROD · mailbox degraded — sends still allowed, latency 9 s", "gate"), ("kill switch off — writes possible under grants", "ok")]:
        s.circle(sx + 18, ty - 4, 5, {"warn": WARN, "gate": AMBER, "ok": OK}[k]); s.text(sx + 30, ty, ln, size=11.5); ty += 24
    s.buttons(sx + 12, ty + 4, [("Probe all", "mute"), ("Add connector", "primary")])
    footer(s, "12 · Control · Connectors", "backs: CONNECTOR_SCOPES (env × system × mode ceiling) · health probes · credential expiry · environments.<env>.customer_facing. Administrator only.")
    save(s, "wf-control-connectors.svg")

# ---------------------------------------------------------------- 13 Control · Roles & policies
def scr_control_policies():
    s = Svg("13 Control · Roles & policies — who may do what; retention per artefact class; gate training rules; budgets")
    x, y, w, h = shell(s, "Control", "Control  ›  Roles & policies", "Administrator")
    s.text(x, y + 18, "Control", size=18, weight=700)
    s.chips(x + 90, y + 4, [("connectors", "mute"), ("roles & policies", "ink"), ("exceptions & kill switch", "mute"), ("audit", "mute")])
    half = (w - 16) / 2
    s.panel(x, y + 40, half, 300, "Roles → capabilities", "roles, not names — holders are assigned per estate")
    cols = ["capability", "View", "Oper", "Appr", "Publ", "Admin", "Cust"]
    colw = [200] + [(half - 24 - 200) / 6] * 6
    ok_, no, ltd = ("✓", "ok"), ("—", "mute"), ("limited", "gate")
    rows = [["read cases, papers, where", ok_, ok_, ok_, ok_, ok_, ltd],
            ["control a session · answer H1–H3, H6", no, ok_, no, no, no, no],
            ["decide H4 · H5 · H7 · HW-*", no, no, ok_, no, no, no],
            ["publish / rollback prompts & skills", no, no, no, ok_, no, no],
            ["promote knowledge (Hd)", no, ltd, no, no, ok_, no],
            ["edit scopes, policies, grants", no, no, no, no, ok_, no],
            ["start support · request configuration", no, no, no, no, no, ok_],
            ["accept a delivery (CG-11)", no, no, no, no, no, ok_]]
    s.table(x + 12, y + 84, half - 24, cols, rows, colw=colw, rowh=24, size=11.5)
    s.text(x + 12, y + 318, "View = Viewer · Oper = Operator · Appr = Approver · Publ = Prompt publisher · Cust = Customer representative", size=10, fill=MUTE)
    s.text(x + 12, y + 333, "Rule: approval is a permission — the role for the gate and target decides, own case included; four eyes = withhold the role. No holders named yet (D107).", size=10, fill=MUTE)
    sx = x + half + 16
    s.panel(sx, y + 40, half, 300, "Retention policy", "per artefact class · controllable · manual actions are gated (D109)")
    s.table(sx + 12, y + 84, half - 24, ["artefact class", "keep", "then", "manual action", "gate"],
            [["write log (ids, hashes)", "7 y", "archive", "export", ("HW-irrev.", "gate")],
             ["papers (plan, contract, report)", "2 y after close", "archive", "purge case", ("HW-irrev.", "gate")],
             ["session transcripts", "90 d after close", "delete", "delete now", ("Admin", "gate")],
             ["evidence (attachments)", "1 y", "delete", "delete now", ("Admin", "gate")],
             ["knowledge · retired", "3 y", "delete", "restore", ("curator", "gate")],
             ["browser / worktree traces", "end of case", "scrub", "scrub now", ("Operator", "mute")]],
            colw=[176, 96, 56, 82, half - 24 - 410], rowh=26, size=11.5)
    ty = para(s, sx + 12, y + 300, half - 24, "Ticket retention follows the customer’s ticket system; the platform keeps its own case and never the ticket body beyond the brief. Every class row is editable here; every manual action lands in Decisions with a packet.", 10.5, MUTE, lh=14)
    s.panel(x, y + 356, half, h - 40 - 356, "Gate policy — trainable by people", "D112")
    yy = y + 400
    for ln in ["• A write shape starts as draft: every instance is HW-instance (a person approves each write).", "• Three consecutive clean, person-confirmed instances → proposal to lift the shape to HW-approve (batch-level approval).",
               "• A failed matching instance suspends the shape; the target Approver confirms re-approval.", "• Irreversible and DDL shapes stay human unless the exact risk class is accepted.",
               "• Customer-facing environments add H5 on top (D114)."]:
        yy = para(s, x + 12, yy, half - 24, ln, 11.5, "#374151", lh=16) + 5
    s.text(x + 12, yy + 8, "threshold [ 3 ] consecutive · window [ 90 d ] · lift confirmed by [ Administrator ▾ ]", size=11, mono=True)
    s.panel(sx, y + 356, half, h - 40 - 356, "Budgets", "hours, not money (D117)")
    yy = y + 400
    for ln in ["• Per case: agent runtime budget in minutes; human budget shown as elapsed, never capped.", "• Exceeding the runtime budget parks the case (budget) and asks the Operator to extend or stop.",
               "• Per environment: max concurrent grants; per connector: max calls per minute.", "• Nothing on this page is expressed in currency."]:
        yy = para(s, sx + 12, yy, half - 24, ln, 11.5, "#374151", lh=16) + 5
    s.text(sx + 12, yy + 8, "case runtime [ 120 min ] · concurrent grants PROD [ 2 ] · extend by [ Operator ▾ ]", size=11, mono=True)
    footer(s, "13 · Control · Policies", "backs: the role map (Trust and Data § 3) · policy keys of the whitelabel catalogue — retention.* (D109) · gates.* (D112) · budgets.* (D117) — proposed.")
    save(s, "wf-control-policies.svg")

# ---------------------------------------------------------------- 14 Control · Exceptions & kill switch
def scr_control_exceptions():
    s = Svg("14 Control · Exceptions & kill switch — grants in force, held batches, the switch, and the audit tail")
    x, y, w, h = shell(s, "Control", "Control  ›  Exceptions & kill switch", "Administrator · Approver reads")
    s.text(x, y + 18, "Control", size=18, weight=700)
    s.chips(x + 90, y + 4, [("connectors", "mute"), ("roles & policies", "mute"), ("exceptions & kill switch", "ink"), ("audit", "mute")])
    s.rect(x, y + 40, w, 74, fill=OK_L, stroke=OK)
    s.circle(x + 24, y + 77, 10, OK)
    s.text(x + 44, y + 72, "Kill switch is OFF — writes and sends are possible under grants.", size=14, weight=700)
    para(s, x + 44, y + 90, w - 44 - 230, "Turning it ON refuses every write, send, ddl and push immediately, parks running cases, and leaves reads working. It is a decision with a reason; turning it OFF again is a second decision.", 11, "#374151", lh=14)
    s.button(x + w - 200, y + 64, "Turn ON — halt all writes", "warn", w=188)
    half = (w - 16) / 2
    s.panel(x, y + 130, w, 200, "Grants in force", "issued by gates · env × system × target × mode · artefact hash · expiry — the executor checks these, not the agent")
    s.table(x + 12, y + 174, w - 24, ["case", "gate", "env", "system", "target", "mode", "artefact hash", "issued by", "expires", ""],
            [["#1040", "HW-instance", "PROD", "INSIS", "GEN_RISK_COVERED", ("write", "gate"), "9f3c…e1", "Approver", "in 22 min", "revoke"],
             ["#1041", "H3 → HW-approve", "TEST", "IPAL", "nomenclature (2 tables)", ("write", "gate"), "51ab…0c", "Approver", "in 4 h", "revoke"],
             ["#1029", "HW-ddl", "PROD", "IPAL", "index on SRD_CUST.…", ("ddl", "warn"), "c2e0…77", "DBA role", "pending", "—"]],
            colw=[70, 130, 70, 80, 200, 80, 110, 110, 90, w - 24 - 940], rowh=26)
    s.text(x + 12, y + 300, "A grant is single-use per batch, bound to the packet hash: change the statement and the grant no longer matches. Revoking is logged.", size=10.5, fill=MUTE)
    s.panel(x, y + 346, half, h - 40 - 346, "Exceptions", "things a person must look at that are not a gate")
    yy = y + 390
    for ln, k in [("#1035 parked · connector-down · TEST INSIS · 38 min", "gate"), ("#1039 parked · provider-down · model provider retry 3/5", "gate"), ("#1033 parked · budget · runtime 120 min reached — extend?", "gate"),
                  ("#1037 parked · question · customer has not answered for 2 d", "info"), ("capability gap · “print debit note” skill missing → Studio", "info"), ("2 sessions stopped by controller without handover · resume or delete", "mute")]:
        s.circle(x + 18, yy - 4, 5, {"gate": AMBER, "info": INFO, "mute": LINE}[k]); s.text(x + 30, yy, ln, size=11.5); yy += 22
    s.buttons(x + 12, yy + 4, [("Resume", "mute"), ("Extend budget", "gate"), ("Stop & hand over", "mute")])
    sx = x + half + 16
    s.panel(sx, y + 346, half, h - 40 - 346, "Audit tail", "AUDIT_LEDGER · append-only · who · what · where · why")
    yy = y + 390
    for ln in ["09:49  Approver opened packet #1040 · HW-instance", "09:43  Operator asked the agent in #1040 (question in chat)", "09:41  agent requested write · #1040 · PROD INSIS GEN_RISK_COVERED",
               "09:40  Approver gave H3 · #1042 · plan v2", "09:24  Operator answered H1 · #1042 · “IPAL is right”", "09:15  Operator opened case #1042 from brief SD-4471",
               "09:12  platform.intake briefed arrival ticket:SD-4471", "08:10  Operator drafted prompt v13 · Support S2"]:
        s.text(sx + 12, yy, ln, size=11.5, mono=True, fill="#374151"); yy += 20
    s.text(sx + 12, yy + 6, "Export (HW-irreversible for deletion, plain for read) · filter by case, role, env", size=10.5, fill=MUTE)
    footer(s, "14 · Control · Exceptions", "backs: kill-switch state · GRANTS · HELD_BATCHES · CASES (park classes) · AUDIT_LEDGER. Reads never stop; writes stop with one decision.")
    save(s, "wf-control-exceptions.svg")





# ---------------------------------------------------------------- client app
def client_shell(s, crumb, active):
    x, y, w, h = shell(s, "", crumb, "L. Andreev · Customer representative · Bulstrad", client=True)
    # top tabs instead of rail
    tx = 24
    for label in ["My requests", "Start a request", "Help"]:
        on = label == active
        if on: s.rect(tx - 6, 60, len(label) * 7.6 + 12, 26, fill=INFO_L, stroke="none", rx=6)
        s.text(tx, 78, label, size=13, weight=700 if on else 400, fill=INK if on else "#374151")
        tx += len(label) * 7.6 + 28
    s.line(24, 96, W - 24, 96)
    return (24, 112, W - 48, H - 112 - 40)

def scr_client_requests():
    s = Svg("15 Client · My requests — the customer’s list, in customer words")
    x, y, w, h = client_shell(s, "My requests", "My requests")
    s.text(x, y + 18, "My requests", size=18, weight=700)
    s.text(x + 130, y + 18, "everything you asked us for, where it stands, and whether we need something from you", size=12, fill=MUTE)
    s.chips(x, y + 30, [("open 6", "ink"), ("we need something from you 2", "gate"), ("ready for your acceptance 1", "ok"), ("done 14", "mute")])
    cols = ["request", "kind", "where it stands", "last update", "your ticket", "next step is with"]
    colw = [360, 130, 220, 120, 120, w - 950]
    rows = [["Premium differs between the two systems — policy ••••7731", "something is wrong", ("we are looking into it", "info"), "today 09:25", "SD-4471 ↗", "us"],
            ["New vehicle model in pricing — Renault Austral", "configuration", ("we need your confirmation", "gate"), "today 08:55", "—", "you"],
            ["Green-card ranges for agency 18617", "configuration", ("ready for your acceptance", "ok"), "yesterday", "SD-4402 ↗", "you"],
            ["Transfer to INSIS fails for annex 3 — policy ••••0028", "something is wrong", ("being fixed", "info"), "today 09:41", "HD 00101281 ↗", "us"],
            ["Provider report review 2026-08", "question", ("we need an answer from you", "gate"), "2 days ago", "—", "you"],
            ["Print of debit note fails", "something is wrong", ("being fixed · a little delayed", "gate"), "yesterday", "SD-4398 ↗", "us"],
            ["Sync framework 1101 for a new child policy", "configuration", ("done · reply sent", "mute"), "yesterday", "SD-4402 ↗", "—"]]
    yy = s.table(x, y + 60, w, cols, rows, rowh=32, colw=colw)
    yy += 20
    s.text(x, yy, "Words you will see:  received · we are looking into it · we need something from you · being fixed · checking the fix · ready for your acceptance · done", size=11.5, fill=MUTE)
    s.text(x, yy + 20, "You never see internal system names, table names or identifiers — only your policy numbers, your ticket numbers and plain language.", size=11.5, fill=MUTE)
    s.text(x, yy + 40, "Development requests (new features) are agreed with your Ablera contact and are not started here. If your role includes Viewer, a request also offers “Open the case view” — the working thread, read-only.", size=11.5, fill=MUTE)
    footer(s, "15 · Client · My requests", "backs: CASES filtered to this customer · state words mapped from case states · clocks shown as “a little delayed” only. Customer representative role.", show_legend=False)
    save(s, "wf-client-requests.svg")

def scr_client_request():
    s = Svg("16 Client · One request — timeline in plain words; what we need from you; where it stands")
    x, y, w, h = client_shell(s, "My requests  ›  New vehicle model in pricing — Renault Austral", "My requests")
    lw = 700
    s.text(x, y + 18, "New vehicle model in pricing — Renault Austral", size=18, weight=700)
    s.chips(x, y + 30, [("configuration request", "mute"), ("we need your confirmation", "gate"), ("asked on 04.09 08:40 by e-mail", "mute"), ("promised offer within 7 business days", "mute")])
    ty = y + 70
    steps = [("Received", "04.09 08:40", "done"), ("Understood", "04.09 09:02", "done"), ("Your confirmation", "waiting", "now"), ("Being done", "", ""), ("Checking", "", ""), ("Ready for you", "", "")]
    sx = x
    for label, when, st in steps:
        col = OK if st == "done" else (AMBER if st == "now" else LINE)
        s.circle(sx + 10, ty, 8, col if st else "#fff", stroke=col)
        if label != steps[-1][0]: s.line(sx + 18, ty, sx + 110, ty, stroke=LINE)
        s.text(sx, ty + 24, label, size=11.5, weight=700 if st == "now" else 400, fill=INK if st else MUTE)
        s.text(sx, ty + 40, when, size=10.5, fill=MUTE)
        sx += 116
    yy = ty + 70
    s.rect(x, yy, lw, 150, fill=AMBER_L, stroke=AMBER)
    s.text(x + 14, yy + 22, "WE NEED SOMETHING FROM YOU", size=10, fill="#7a4d00", weight=700, ls="1")
    s.text(x + 14, yy + 44, "We found two possible entries for this model in the official vehicle catalogue:", size=12.5)
    s.text(x + 14, yy + 64, "  •  RENAULT AUSTRAL (2022–)      •  RENAULT AUSTRAL E-TECH HYBRID (2022–)", size=12.5)
    s.text(x + 14, yy + 84, "Should we add both, or only the first? Your answer lets us finish today.", size=12.5)
    s.rect(x + 14, yy + 98, lw - 28 - 100, 30, fill="#fff", stroke=LINE, rx=6); s.text(x + 24, yy + 118, "Type your answer…", size=12, fill=MUTE)
    s.button(x + lw - 14 - 90, yy + 100, "Answer", "primary", w=90)
    yy += 166
    s.panel(x, yy, lw, h - (yy - y) - 10, "What happened so far", "in plain words")
    ty = yy + 44
    for when, txt in [("04.09 08:40", "You wrote to the service desk asking for the model to be selectable in pricing."),
                      ("04.09 09:02", "We understood this as a configuration request and promised an offer within 7 business days."),
                      ("04.09 09:30", "We checked the vehicle catalogues and found two candidate entries — see the question above."),
                      ("—", "Next: once you answer, we add the model on our working environment, verify a test quote, then bring it to production and tell you.")]:
        s.text(x + 14, ty, when, size=11, fill=MUTE, mono=True); ty = para(s, x + 110, ty, lw - 124, txt, 12, INK, lh=17) + 5
    rx, rw = x + lw + 16, w - lw - 16
    s.panel(rx, y + 70, rw, 150, "Where it stands")
    yy = y + 114
    for k, v in [("kind", "configuration request"), ("next step is with", "you"), ("your reference", "e-mail of 04.09 08:40"), ("our reference", "request #1041"), ("promised", "offer within 7 business days")]:
        s.text(rx + 12, yy, k, size=10.5, fill=MUTE); s.text(rx + 150, yy, v, size=12); yy += 20
    s.panel(rx, y + 236, rw, 120, "Attachments")
    s.text(rx + 12, y + 280, "catalogue-extract.pdf · from us · 04.09", size=11.5, fill=INFO)
    s.text(rx + 12, y + 300, "your original e-mail · 04.09", size=11.5, fill=INFO)
    s.text(rx + 12, y + 330, "Add a file", size=11.5, fill=INFO)
    s.panel(rx, y + 372, rw, 112, "Need to talk?")
    s.text(rx + 12, y + 414, "Your Ablera contact is Mira D.; write here and the same people see it.", size=11.5, fill="#374151")
    s.rect(rx + 12, y + 430, rw - 24, 30, fill="#fff", stroke=LINE, rx=6); s.text(rx + 22, y + 450, "Write a message…", size=12, fill=MUTE)
    s.panel(rx, y + 500, rw, 128, "Case view", "because your role allows it")
    para(s, rx + 12, y + 544, rw - 24, "Your access includes Viewer: you may open the working thread the Ablera team sees — read-only, with handles instead of identifiers.", 11, "#374151", lh=15)
    s.button(rx + 12, y + 588, "Open the case view ↗", "mute", w=170)
    s.text(rx + 190, y + 605, "read-only · same thread · no composer", size=10, fill=MUTE)
    footer(s, "16 · Client · One request", "backs: the case (state · clocks · question parks addressed to the customer · report paper) in customer words — no handles, no system names; the case view is a door for roles that include Viewer.", show_legend=False)
    save(s, "wf-client-request.svg")

def scr_client_acceptance():
    s = Svg("17 Client · Acceptance — what changed, how we checked it, accept or ask for changes (CG-11)")
    x, y, w, h = client_shell(s, "My requests  ›  Green-card ranges for agency 18617  ›  Acceptance", "My requests")
    lw = 760
    s.text(x, y + 18, "Green-card ranges for agency 18617 — ready for your acceptance", size=18, weight=700)
    s.chips(x, y + 30, [("configuration request", "mute"), ("ready for your acceptance", "ok"), ("delivered 03.09 16:40", "mute"), ("your ticket SD-4402 ↗", "info")])
    yy = y + 66
    s.panel(x, yy, lw, 180, "What changed", "in your words")
    ty = yy + 44
    for ln in ["• Agency 18617 can now issue green cards from the range 000 123 001 – 000 123 500 (500 blanks).", "• The same range is visible in the daily blank report from tomorrow morning.",
               "• Nothing else was changed for this agency or any other."]:
        s.text(x + 14, ty, ln, size=12.5); ty += 22
    s.text(x + 14, ty + 8, "Your original request: “Please add the new green-card blanks for agency 18617 as per the attached delivery note.” (03.09 10:12)", size=11.5, fill=MUTE)
    yy += 196
    s.panel(x, yy, lw, 150, "How we checked", "what a person verified before telling you")
    ty = yy + 44
    for ln, k in [("a test issue with the first blank of the range worked on our working environment", "ok"), ("the range appears once, no overlap with any existing range", "ok"), ("a person at Ablera holding the release role confirmed the change before it went live", "ok"), ("you can check: issue one green card from the range and see it in the report", "info")]:
        s.circle(x + 20, ty - 4, 5, OK if k == "ok" else INFO); s.text(x + 32, ty, ln, size=12); ty += 22
    yy += 166
    s.rect(x, yy, lw, 96, fill=OK_L, stroke=OK)
    s.text(x + 14, yy + 22, "YOUR DECISION", size=10, fill="#1f5c37", weight=700, ls="1")
    s.text(x + 14, yy + 44, "Accept this delivery to close. A preview confirmation keeps a requested release open.", size=12)
    s.buttons(x + 14, yy + 58, [("Accept delivered result", "ok"), ("Ask for changes…", "mute")])
    rx, rw = x + lw + 16, w - lw - 16
    s.panel(rx, y + 66, rw, 200, "Where it stands")
    ty = y + 110
    for k, v in [("kind", "configuration request"), ("next step is with", "you"), ("promised", "resolution within 5 bd"), ("delivered", "03.09 16:40 · within promise"), ("accept by", "10.09 (review reminder)"), ("our reference", "request #1034")]:
        s.text(rx + 12, ty, k, size=10.5, fill=MUTE); s.text(rx + 150, ty, v, size=12); ty += 22
    s.panel(rx, y + 282, rw, 120, "Attachments")
    s.text(rx + 12, y + 326, "your delivery note · 03.09", size=11.5, fill=INFO)
    s.text(rx + 12, y + 346, "confirmation letter (PDF) · from us", size=11.5, fill=INFO)
    s.panel(rx, y + 418, rw, 100, "Not happy?")
    s.text(rx + 12, y + 462, "“Ask for changes” never blames anyone; it tells us what to look at again.", size=11.5, fill="#374151")
    s.panel(rx, y + 534, rw, 96, "See how it was done", "because your role allows it")
    s.button(rx + 12, y + 578, "Open the case view ↗", "mute", w=170)
    s.text(rx + 190, y + 595, "read-only working thread", size=10, fill=MUTE)
    footer(s, "17 · Client · Acceptance", "backs: gate CG-11 (customer accepts) · report paper in customer words · review reminder; no automatic acceptance. Accepting = the customer’s decision recorded like any other gate.", show_legend=False)
    save(s, "wf-client-acceptance.svg")

def scr_client_start():
    s = Svg("18 Client · Start a request — three kinds, plain fields, references we can resolve")
    x, y, w, h = client_shell(s, "Start a request", "Start a request")
    lw = 720
    s.text(x, y + 18, "Start a request", size=18, weight=700)
    s.text(x + 150, y + 18, "you can also just e-mail the service desk or open a ticket as before — it lands in the same place", size=12, fill=MUTE)
    yy = y + 50
    s.text(x, yy, "WHAT IS IT ABOUT?", size=10, fill=MUTE, weight=700, ls="1"); yy += 12
    kinds = [("Something is wrong", "a policy, a premium, a document or a transfer does not behave as it should", True),
             ("I need a configuration change", "a new vehicle model, agency, blank range, product parameter, tariff value", False),
             ("I have a question", "how something works, what a value means, where to find it", False)]
    kx = x
    for title, sub, on in kinds:
        s.rect(kx, yy, 232, 70, fill=INFO_L if on else "#fff", stroke=INFO if on else LINE, sw=1.5 if on else 1)
        s.text(kx + 12, yy + 24, title, size=12.5, weight=700)
        para(s, kx + 12, yy + 42, 208, sub, 10.5, MUTE, lh=14)
        kx += 244
    yy += 90
    s.text(x, yy, "TELL US IN YOUR WORDS", size=10, fill=MUTE, weight=700, ls="1"); yy += 10
    s.rect(x, yy, lw, 90, fill="#fff", stroke=LINE, rx=6)
    para(s, x + 12, yy + 22, lw - 24, "The premium of policy 4704…7731 is 512.40 EUR in one system and 498.10 EUR in the other. Which is right? Please align before the instalment is collected.", 11.5, INK, lh=16)
    yy += 106
    s.text(x, yy, "REFERENCES (we resolve them for you)", size=10, fill=MUTE, weight=700, ls="1"); yy += 10
    s.rect(x, yy, 340, 30, fill="#fff", stroke=LINE, rx=6); s.text(x + 12, yy + 20, "policy number  4704…7731", size=12)
    s.chip(x + 240, yy + 6, "found ✓", "ok")
    s.rect(x + 356, yy, 340, 30, fill="#fff", stroke=LINE, rx=6); s.text(x + 368, yy + 20, "your ticket or e-mail reference (optional)", size=12, fill=MUTE)
    yy += 46
    s.rect(x, yy, lw, 44, fill=FILL, stroke=LINE, rx=6, dash="4 3")
    s.text(x + 12, yy + 27, "Drop screenshots or files here — one screenshot of what you see helps most.", size=12, fill=MUTE)
    yy += 60
    s.text(x, yy, "HOW URGENT IS IT FOR YOUR BUSINESS?", size=10, fill=MUTE, weight=700, ls="1"); yy += 10
    s.chips(x, yy, [("work is stopped", "warn"), ("significant loss of function", "gate"), ("we have a workaround", "ok"), ("no hurry / information", "mute")]); yy += 36
    s.text(x, yy, "We propose the contract severity from what you write; you will see our proposal and can object.", size=11, fill=MUTE); yy += 26
    s.buttons(x, yy, [("Send request", "primary"), ("Save draft", "mute")])
    rx, rw = x + lw + 16, w - lw - 16
    s.panel(rx, y + 50, rw, 200, "What happens next")
    ty = y + 94
    for ln in ["1. We confirm receipt at once and give you a request number.", "2. Your request is classified at once. We show the next step and who can approve it.", "3. You see every step in My requests; we ask you only what only you can answer.",
               "4. When it is done, you accept it — or ask for changes."]:
        ty = para(s, rx + 12, ty, rw - 24, ln, 11.5, "#374151", lh=14) + 4
    s.panel(rx, y + 266, rw, 130, "Not here")
    ty = y + 310
    for ln in ["New features and changes to how the systems work (development) are agreed with your Ablera contact, not started from this form.", "Emergencies outside office hours: use the phone line in your contract."]:
        ty = para(s, rx + 12, ty, rw - 24, ln, 11, MUTE, lh=15) + 6
    footer(s, "18 · Client · Start a request", "backs: an arrival like any other → authorised intake → Support case → classification. Kind maps to case type (incident · request · question); urgency is the customer’s proposal for severity.", show_legend=False)
    save(s, "wf-client-start.svg")

def scr_client_action():
    """Screen 16 variant: an assigned action and returned evidence, not a new screen."""
    s = Svg("16b Client · One request — assigned action and returned evidence")
    x, y, w, h = client_shell(s, "My requests  ›  Complete the requested policy action", "My requests")
    lw = 700
    s.text(x, y + 18, "Complete the requested policy action", size=18, weight=700)
    s.chips(x, y + 32, [("a step for you", "gate"), ("requested 07.09 at 09:15", "mute"), ("request #1042", "mute")])
    yy = y + 78
    s.panel(x, yy, lw, 220, "What we need you to do", "Only the person authorised for this action should perform it")
    para(s, x + 16, yy + 65, lw - 32, "Open the policy linked in the instruction and perform the confirmed transfer action. If the current policy state differs from the instruction, stop and tell us what you see.", 13, INK, lh=20)
    para(s, x + 16, yy + 130, lw - 32, "Then report the outcome and attach the requested result. We will check it before calling the request resolved.", 12, MUTE, lh=18)
    s.button(x + 16, yy + 175, "Report completed", "primary", w=166)
    s.button(x + 190, yy + 175, "Cannot perform", "mute", w=150)
    s.button(x + 348, yy + 175, "Add evidence", "mute", w=132)
    yy += 240
    s.panel(x, yy, lw, 154, "Your response")
    s.rect(x + 16, yy + 45, lw - 32, 50, fill="#fff", stroke=LINE)
    s.text(x + 28, yy + 73, "Describe what happened; include any error you received…", size=12, fill=MUTE)
    s.text(x + 16, yy + 122, "Attached evidence stays with this request. A report is followed by a check.", size=11.5, fill=MUTE)
    yy += 174
    s.panel(x, yy, lw, 145, "What happens next")
    para(s, x + 16, yy + 58, lw - 32, "Your response changes this step to Checking. We verify the result and any remaining work, including restoration of a temporary exception. You then receive the stated result for acceptance.", 12.5, INK, lh=19)
    rx, rw = x + lw + 18, w - lw - 18
    s.panel(rx, y + 78, rw, 172, "Where it stands")
    for j, (k, v) in enumerate([("next step is with", "you"), ("requested", "07.09 09:15"), ("waiting", "35 minutes"), ("after your response", "we check the result")]):
        s.text(rx + 14, y + 132 + j*25, k, size=11, fill=MUTE)
        s.text(rx + 180, y + 132 + j*25, v, size=12)
    s.panel(rx, y + 268, rw, 150, "What counts as complete")
    para(s, rx + 14, y + 322, rw - 28, "The intended policy action works, every required check passes and any temporary exception is restored. Sending the instruction or pressing Report completed does not finish these checks.", 12, INK, lh=19)
    s.panel(rx, y + 436, rw, 160, "Need to change the instruction?")
    para(s, rx + 14, y + 490, rw - 28, "Use Cannot perform or write here. We keep the completed work and review only the affected next step. You do not need to share your password or let another person use your session.", 12, INK, lh=19)
    footer(s, "16b · Client action", "Design fixture: response is bound to the current request and action; it queues verification and is separate from delivery acceptance.", show_legend=False)
    save(s, "wf-client-action.svg")

# ---------------------------------------------------------------- main
ALL = [scr_inbox, scr_cases, scr_case_chat, scr_case_gate, scr_where, scr_decisions, scr_plan_gate,
       scr_studio, scr_studio_version, scr_knowledge, scr_metrics, scr_control_connectors, scr_control_policies, scr_control_exceptions,
       scr_client_requests, scr_client_request, scr_client_acceptance, scr_client_start, scr_client_action]

if __name__ == "__main__":
    os.makedirs(OUT, exist_ok=True)
    for f in ALL:
        f()
    print(len(ALL), "screens")



