#!/usr/bin/env python3
"""OKELIS portfolio · generative technical graphics.
Emits clean vector SVG into assets/gfx/. All geometry is deterministic.
"""
import math, os, random, re

OUT = os.path.join(os.path.dirname(__file__), "..", "assets", "gfx")
os.makedirs(OUT, exist_ok=True)

NAVY   = "#021849"
BLUE   = "#1B5AA8"
BLUE2  = "#3D79C4"
BLUE3  = "#7BA3D6"
SKY    = "#B9CDE8"
RED    = "#E1330F"
REDLT  = "#FE7C60"
LINE   = "#DDE1E8"
MUTED  = "#8590A6"


def write(name, body, w, h, extra="", fscale=1.0):
    body = re.sub(r"&(?!(amp|lt|gt|quot|apos|#)\w*;)", "&amp;", body)
    if fscale != 1.0:
        body = re.sub(r'font-size="([\d.]+)"',
                      lambda m: f'font-size="{float(m.group(1))*fscale:.2f}"', body)
    svg = (f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" '
           f'width="{w}" height="{h}" fill="none">{extra}{body}</svg>')
    with open(os.path.join(OUT, name), "w") as f:
        f.write(svg)
    print("·", name)


# ------------------------------------------------------------------
# 1. COVER · high-performance façade, axonometric mullion grid
# ------------------------------------------------------------------
def cover_facade():
    W, H = 900, 1100
    random.seed(7)
    p = []
    # Two offset façade planes in a shallow two-point perspective.
    def plane(x0, y0, w, h, cols, rows, skew, op, fills):
        """skew: vertical shear per unit x (px of y per px of x)"""
        g = [f'<g opacity="{op}">']
        def pt(cx, cy):
            X = x0 + cx * w / cols
            Y = y0 + cy * h / rows + (cx * w / cols) * skew
            return X, Y
        for c in range(cols + 1):
            a = pt(c, 0); b = pt(c, rows)
            g.append(f'<path d="M{a[0]:.1f} {a[1]:.1f}L{b[0]:.1f} {b[1]:.1f}" stroke="{BLUE3}" stroke-width=".9"/>')
        for r in range(rows + 1):
            a = pt(0, r); b = pt(cols, r)
            g.append(f'<path d="M{a[0]:.1f} {a[1]:.1f}L{b[0]:.1f} {b[1]:.1f}" stroke="{BLUE3}" stroke-width=".9"/>')
        # data-filled panels · reads as a performance overlay on the façade
        for (cc, rr, col, o) in fills:
            q = [pt(cc, rr), pt(cc + 1, rr), pt(cc + 1, rr + 1), pt(cc, rr + 1)]
            d = "M" + "L".join(f"{x:.1f} {y:.1f}" for x, y in q) + "Z"
            g.append(f'<path d="{d}" fill="{col}" opacity="{o}"/>')
        g.append("</g>")
        return "".join(g)

    fills_a = []
    for r in range(18):
        for c in range(9):
            v = random.random()
            if v > .90:   fills_a.append((c, r, RED,   .55))
            elif v > .80: fills_a.append((c, r, BLUE2, .55))
            elif v > .62: fills_a.append((c, r, BLUE,  .30))
    fills_b = []
    for r in range(14):
        for c in range(6):
            v = random.random()
            if v > .86:   fills_b.append((c, r, BLUE2, .40))
            elif v > .70: fills_b.append((c, r, BLUE,  .22))

    p.append(plane(60, 150, 430, 860, 9, 18, .10, .95, fills_a))
    p.append(plane(520, 330, 300, 640, 6, 14, -.09, .55, fills_b))
    # ground datum
    p.append(f'<path d="M0 1035H900" stroke="{BLUE}" stroke-width="1" opacity=".5"/>')
    # daylight rays
    for i in range(7):
        x = 120 + i * 108
        p.append(f'<path d="M{x} 0L{x-150} 1035" stroke="{REDLT}" stroke-width=".7" opacity="{.16 - i*.012:.3f}"/>')
    write("cover-facade.svg", "".join(p), W, H)


# ------------------------------------------------------------------
# 2. DAYLIGHT · floor-plate sDA heat map
# ------------------------------------------------------------------
def daylight_plan():
    W, H = 520, 330
    cols, rows = 34, 21
    cw, ch = 14, 14
    ox, oy = 12, 12
    ramp = ["#0B2E6B", "#1B5AA8", "#3D79C4", "#7BA3D6", "#B9CDE8",
            "#F4C9B6", "#FE9C7E", "#F0603A", "#E1330F"]
    g = []
    # window wall along top + right → daylight falls off from those edges
    for r in range(rows):
        for c in range(cols):
            d_top = r / rows
            d_rgt = (cols - 1 - c) / cols
            d = min(d_top * 1.15, d_rgt * 1.45 + .06)
            # core void (service core) punched out
            if 13 <= c <= 20 and 8 <= r <= 15:
                continue
            v = max(0.0, 1 - d * 1.35)
            v += math.sin(c * .55) * .035 + math.cos(r * .7) * .03
            v = min(.999, max(0.0, v))
            col = ramp[int(v * (len(ramp) - 1))]
            g.append(f'<rect x="{ox+c*cw}" y="{oy+r*ch}" width="{cw-1.1:.1f}" '
                     f'height="{ch-1.1:.1f}" fill="{col}" opacity=".92"/>')
    # core
    g.append(f'<rect x="{ox+13*cw}" y="{oy+8*ch}" width="{8*cw-1.1:.1f}" '
             f'height="{8*ch-1.1:.1f}" fill="#EDF0F4"/>')
    g.append(f'<text x="{ox+17*cw}" y="{oy+12.6*ch}" text-anchor="middle" fill="{MUTED}" '
             f'font-family="SF Pro Text" font-size="9" letter-spacing="1.4">CORE</text>')
    # envelope
    g.append(f'<rect x="{ox-3}" y="{oy-3}" width="{cols*cw+4:.0f}" height="{rows*ch+4:.0f}" '
             f'stroke="{NAVY}" stroke-width="3.2"/>')
    write("daylight-plan.svg", "".join(g), W, H, fscale=1.55)


# ------------------------------------------------------------------
# 3. DAYLIGHT · annual illuminance / façade shading section
# ------------------------------------------------------------------
def daylight_section():
    W, H = 580, 265
    g = []
    fy, cy = 205, 40           # floor, ceiling
    g.append(f'<path d="M40 {cy}H420" stroke="{NAVY}" stroke-width="3.2"/>')
    g.append(f'<path d="M40 {fy}H420" stroke="{NAVY}" stroke-width="3.2"/>')
    g.append(f'<path d="M420 {cy}V{fy}" stroke="{NAVY}" stroke-width="3.2"/>')  # façade
    g.append(f'<path d="M40 {cy}V{fy}" stroke="{NAVY}" stroke-width="3.2" opacity=".35" stroke-dasharray="4 3"/>')
    # glazing
    g.append(f'<path d="M412 {cy+24}V{fy-16}" stroke="{BLUE2}" stroke-width="7"/>')
    # external shade fin
    g.append(f'<path d="M420 {cy+22}L482 {cy+4}" stroke="{NAVY}" stroke-width="5"/>')
    g.append(f'<text x="486" y="{cy+2}" fill="{NAVY}" font-family="SF Pro Text" font-size="9" '
             f'font-weight="600" letter-spacing=".6">SHADE</text>')
    # daylight wedge
    g.append(f'<path d="M420 {cy+24}L60 {fy}L420 {fy-14}Z" fill="{REDLT}" opacity=".16"/>')
    for i in range(9):
        t = i / 8
        y0 = cy + 24 + t * (fy - 14 - cy - 24)
        g.append(f'<path d="M420 {y0:.0f}L{60 + t*250:.0f} {fy}" stroke="{RED}" '
                 f'stroke-width="1.5" opacity="{.34 - t*.22:.2f}"/>')
    # illuminance falloff curve
    pts = []
    for i in range(41):
        x = 60 + i * 9
        t = i / 40
        y = fy - 8 - 92 * math.exp(-(1 - t) * 3.1)
        pts.append(f"{x:.0f} {y:.1f}")
    g.append(f'<path d="M{"L".join(pts)}" stroke="{NAVY}" stroke-width="3"/>')
    g.append(f'<text x="46" y="{fy+20}" fill="{MUTED}" font-family="SF Pro Text" font-size="8.5" '
             f'letter-spacing="1.2">CORE</text>')
    g.append(f'<text x="392" y="{fy+20}" fill="{MUTED}" font-family="SF Pro Text" font-size="8.5" '
             f'letter-spacing="1.2">FAÇADE</text>')
    write("daylight-section.svg", "".join(g), W, H, fscale=2.10)


# ------------------------------------------------------------------
# 4. ENERGY · baseline vs proposed end-use stack
# ------------------------------------------------------------------
def energy_bars():
    W, H = 470, 300
    base = [("HVAC", 132), ("Lighting", 54), ("DHW", 22), ("Equipment", 46), ("Fans & Pumps", 38)]
    prop = [("HVAC", 88), ("Lighting", 28), ("DHW", 15), ("Equipment", 42), ("Fans & Pumps", 24)]
    cols = [NAVY, BLUE, BLUE2, BLUE3, SKY]
    tot_b, tot_p = sum(v for _, v in base), sum(v for _, v in prop)
    y0, hmax = 250, 195
    scale = hmax / tot_b
    g = []
    for i in range(5):
        y = y0 - i * (hmax / 4)
        g.append(f'<path d="M60 {y:.0f}H430" stroke="{LINE}" stroke-width=".7"/>')
        g.append(f'<text x="52" y="{y+3:.0f}" text-anchor="end" fill="{MUTED}" '
                 f'font-family="SF Pro Text" font-size="8">{int(i*tot_b/4)}</text>')

    def stack(x, data, label, total):
        gg = []
        acc = 0
        for (nm, v), c in zip(data, cols):
            h = v * scale
            gg.append(f'<rect x="{x}" y="{y0-acc-h:.1f}" width="96" height="{h-1.2:.1f}" fill="{c}"/>')
            acc += h
        gg.append(f'<text x="{x+48}" y="{y0-acc-11:.1f}" text-anchor="middle" fill="{NAVY}" '
                  f'font-family="SF Pro Display" font-size="16" font-weight="600" '
                  f'letter-spacing="-.5">{total}</text>')
        gg.append(f'<text x="{x+48}" y="{y0+16}" text-anchor="middle" fill="{MUTED}" '
                  f'font-family="SF Pro Text" font-size="8" font-weight="600" '
                  f'letter-spacing="1.5">{label}</text>')
        return "".join(gg)

    g.append(stack(96, base, "BASELINE", tot_b))
    g.append(stack(258, prop, "PROPOSED", tot_p))
    # delta bracket
    yb, yp = y0 - tot_b * scale, y0 - tot_p * scale
    g.append(f'<path d="M198 {yb:.1f}H258" stroke="{RED}" stroke-width=".9" stroke-dasharray="3 2"/>')
    g.append(f'<path d="M370 {yb:.1f}H400M370 {yp:.1f}H400M394 {yb:.1f}V{yp:.1f}" stroke="{RED}" stroke-width="1"/>')
    g.append(f'<text x="404" y="{(yb+yp)/2-1:.1f}" fill="{RED}" font-family="SF Pro Display" '
             f'font-size="15" font-weight="600" letter-spacing="-.4">−32%</text>')
    g.append(f'<text x="404" y="{(yb+yp)/2+11:.1f}" fill="{MUTED}" font-family="SF Pro Text" '
             f'font-size="7" font-weight="600" letter-spacing="1.2">ENERGY</text>')
    g.append(f'<text x="60" y="24" fill="{MUTED}" font-family="SF Pro Text" font-size="7.5" '
             f'font-weight="600" letter-spacing="1.4">ANNUAL ENERGY USE INTENSITY, kWh/m²/yr</text>')
    write("energy-bars.svg", "".join(g), W, H, fscale=1.75)


# ------------------------------------------------------------------
# 5. GHG · Scope 1/2/3 waterfall to net zero
# ------------------------------------------------------------------
def ghg_waterfall():
    W, H = 700, 290
    g = []
    y0 = 232
    S1, S2, S3 = 34, 58, 148
    tot = S1 + S2 + S3
    sc = 172 / tot
    bw = 62
    xs = [40, 128, 216, 322, 430, 538, 632]
    g.append(f'<path d="M20 {y0}H676" stroke="{NAVY}" stroke-width="1"/>')

    def bar(x, base, val, col, lab, sub, w=bw, txtcol=None):
        h = val * sc
        y = y0 - (base + val) * sc
        s = [f'<rect x="{x}" y="{y:.1f}" width="{w}" height="{h:.1f}" fill="{col}"/>']
        s.append(f'<text x="{x+w/2}" y="{y-8:.1f}" text-anchor="middle" fill="{txtcol or NAVY}" '
                 f'font-family="SF Pro Display" font-size="13" font-weight="600" '
                 f'letter-spacing="-.3">{val}</text>')
        s.append(f'<text x="{x+w/2}" y="{y0+15}" text-anchor="middle" fill="{NAVY}" '
                 f'font-family="SF Pro Text" font-size="8" font-weight="600" '
                 f'letter-spacing=".9">{lab}</text>')
        s.append(f'<text x="{x+w/2}" y="{y0+26}" text-anchor="middle" fill="{MUTED}" '
                 f'font-family="SF Pro Text" font-size="6.6" letter-spacing=".6">{sub}</text>')
        return "".join(s)

    g.append(bar(xs[0], 0,        S1, NAVY,  "SCOPE 1", "Direct"))
    g.append(bar(xs[1], S1,       S2, BLUE,  "SCOPE 2", "Purchased energy"))
    g.append(bar(xs[2], S1+S2,    S3, BLUE2, "SCOPE 3", "Value chain"))
    # connectors
    for i, acc in enumerate([S1, S1+S2, tot]):
        y = y0 - acc * sc
        g.append(f'<path d="M{xs[i]+bw} {y:.1f}H{xs[i+1]}" stroke="{MUTED}" '
                 f'stroke-width=".8" stroke-dasharray="3 2.5"/>')
    g.append(bar(xs[3], 0, tot, NAVY, "FOOTPRINT", "tCO₂e baseline", 72))
    g.append(f'<path d="M{xs[3]+72} {y0-tot*sc:.1f}H{xs[4]}" stroke="{MUTED}" stroke-width=".8" stroke-dasharray="3 2.5"/>')
    # reduction
    red = 96
    g.append(f'<rect x="{xs[4]}" y="{y0-tot*sc:.1f}" width="{bw}" height="{red*sc:.1f}" fill="{RED}" opacity=".85"/>')
    g.append(f'<rect x="{xs[4]}" y="{y0-(tot-red)*sc:.1f}" width="{bw}" height="{(tot-red)*sc:.1f}" fill="{SKY}"/>')
    g.append(f'<text x="{xs[4]+bw/2}" y="{y0-tot*sc-8:.1f}" text-anchor="middle" fill="{RED}" '
             f'font-family="SF Pro Display" font-size="13" font-weight="600" letter-spacing="-.3">−{red}</text>')
    g.append(f'<text x="{xs[4]+bw/2}" y="{y0+15}" text-anchor="middle" fill="{NAVY}" '
             f'font-family="SF Pro Text" font-size="8" font-weight="600" letter-spacing=".9">REDUCTION</text>')
    g.append(f'<text x="{xs[4]+bw/2}" y="{y0+26}" text-anchor="middle" fill="{MUTED}" '
             f'font-family="SF Pro Text" font-size="6.6" letter-spacing=".6">Efficiency · Renewables</text>')
    # residual + offset
    resid = tot - red
    g.append(f'<path d="M{xs[4]+bw} {y0-resid*sc:.1f}H{xs[5]}" stroke="{MUTED}" stroke-width=".8" stroke-dasharray="3 2.5"/>')
    g.append(f'<rect x="{xs[5]}" y="{y0-resid*sc:.1f}" width="{bw}" height="{resid*sc:.1f}" fill="{BLUE3}"/>')
    g.append(f'<text x="{xs[5]+bw/2}" y="{y0-resid*sc-8:.1f}" text-anchor="middle" fill="{NAVY}" '
             f'font-family="SF Pro Display" font-size="13" font-weight="600" letter-spacing="-.3">{resid}</text>')
    g.append(f'<text x="{xs[5]+bw/2}" y="{y0+15}" text-anchor="middle" fill="{NAVY}" '
             f'font-family="SF Pro Text" font-size="8" font-weight="600" letter-spacing=".9">RESIDUAL</text>')
    g.append(f'<text x="{xs[5]+bw/2}" y="{y0+26}" text-anchor="middle" fill="{MUTED}" '
             f'font-family="SF Pro Text" font-size="6.6" letter-spacing=".6">Offset / removal</text>')
    # net zero
    g.append(f'<path d="M{xs[5]+bw} {y0:.1f}H{xs[6]+34}" stroke="{RED}" stroke-width="1.4"/>')
    g.append(f'<text x="{xs[6]+2}" y="{y0-26}" fill="{RED}" font-family="SF Pro Display" '
             f'font-size="17" font-weight="600" letter-spacing="-.5">NET</text>')
    g.append(f'<text x="{xs[6]+2}" y="{y0-8}" fill="{RED}" font-family="SF Pro Display" '
             f'font-size="17" font-weight="600" letter-spacing="-.5">ZERO</text>')
    write("ghg-waterfall.svg", "".join(g), W, H, fscale=1.30)


# ------------------------------------------------------------------
# 6. LCC · capital vs whole-life cost crossover
# ------------------------------------------------------------------
def lcc_curves():
    W, H = 470, 285
    g = []
    x0, x1, y0, yt = 56, 430, 230, 34
    g.append(f'<path d="M{x0} {y0}H{x1}" stroke="{NAVY}" stroke-width="1"/>')
    g.append(f'<path d="M{x0} {y0}V{yt}" stroke="{NAVY}" stroke-width="1"/>')
    for i in range(1, 5):
        y = y0 - i * (y0 - yt) / 4
        g.append(f'<path d="M{x0} {y:.0f}H{x1}" stroke="{LINE}" stroke-width=".6"/>')
    for i in range(0, 6):
        x = x0 + i * (x1 - x0) / 5
        g.append(f'<text x="{x:.0f}" y="{y0+15}" text-anchor="middle" fill="{MUTED}" '
                 f'font-family="SF Pro Text" font-size="7.5">{i*5}</text>')
    g.append(f'<text x="{(x0+x1)/2:.0f}" y="{y0+30}" text-anchor="middle" fill="{MUTED}" '
             f'font-family="SF Pro Text" font-size="7" font-weight="600" letter-spacing="1.3">YEARS OF OPERATION</text>')

    def curve(capex, rate, col, dash=""):
        pts = []
        for i in range(0, 101):
            t = i / 100 * 25
            v = capex + rate * t
            x = x0 + (t / 25) * (x1 - x0)
            y = y0 - (v / 300) * (y0 - yt)
            pts.append(f"{x:.1f} {y:.1f}")
        return (f'<path d="M{"L".join(pts)}" stroke="{col}" stroke-width="2" '
                f'stroke-dasharray="{dash}"/>')

    g.append(curve(60, 8.4, MUTED))          # Option A · low capex
    g.append(curve(112, 4.0, NAVY))          # Option B · higher capex
    # crossover
    tx = (112 - 60) / (8.4 - 4.0)
    vx = 60 + 8.4 * tx
    cx = x0 + (tx / 25) * (x1 - x0)
    cy = y0 - (vx / 300) * (y0 - yt)
    g.append(f'<circle cx="{cx:.1f}" cy="{cy:.1f}" r="3.6" fill="{RED}"/>')
    g.append(f'<path d="M{cx:.1f} {cy:.1f}V{y0}" stroke="{RED}" stroke-width=".8" stroke-dasharray="3 2"/>')
    g.append(f'<text x="{cx+7:.1f}" y="{cy-9:.1f}" fill="{RED}" font-family="SF Pro Text" '
             f'font-size="8" font-weight="600" letter-spacing=".7">PAYBACK · YR {tx:.0f}</text>')
    g.append(f'<text x="{x1-4}" y="{y0-(60+8.4*25)/300*(y0-yt)-9:.1f}" text-anchor="end" fill="{MUTED}" '
             f'font-family="SF Pro Text" font-size="8" font-weight="600" letter-spacing=".7">OPTION A · LOW CAPEX</text>')
    g.append(f'<text x="{x1-4}" y="{y0-(112+4*25)/300*(y0-yt)+16:.1f}" text-anchor="end" fill="{NAVY}" '
             f'font-family="SF Pro Text" font-size="8" font-weight="600" letter-spacing=".7">OPTION B · OPTIMIZED</text>')
    g.append(f'<text x="{x0-6}" y="{yt-8}" fill="{MUTED}" font-family="SF Pro Text" font-size="7" '
             f'font-weight="600" letter-spacing="1.3">CUMULATIVE WHOLE-LIFE COST</text>')
    write("lcc-curves.svg", "".join(g), W, H, fscale=1.42)


# ------------------------------------------------------------------
# 7. LCC · whole-life cost composition
# ------------------------------------------------------------------
def lcc_stack():
    W, H = 470, 132
    items = [("CAPEX", 28, NAVY), ("ENERGY", 27, BLUE), ("MAINTENANCE", 18, BLUE2),
             ("REPLACEMENT", 14, BLUE3), ("OPERATION", 9, SKY), ("RESIDUAL", 4, "#E5EBF4")]
    g, x = [], 0.0
    TW = 470
    for i, (nm, v, c) in enumerate(items):
        w = v / 100 * TW
        g.append(f'<rect x="{x:.1f}" y="34" width="{w-1.4:.1f}" height="34" fill="{c}"/>')
        g.append(f'<text x="{x:.1f}" y="26" fill="{NAVY}" font-family="SF Pro Display" '
                 f'font-size="12.5" font-weight="600" letter-spacing="-.3">{v}%</text>')
        ly = 84 if i % 2 == 0 else 95
        last = (i == len(items) - 1)
        anc = 'end' if last else 'start'
        lx = TW if last else x
        g.append(f'<text x="{lx:.1f}" y="{ly}" text-anchor="{anc}" fill="{MUTED}" '
                 f'font-family="SF Pro Text" font-size="6.6" font-weight="600" '
                 f'letter-spacing="1.1">{nm}</text>')
        x += w
    g.append(f'<path d="M0 112H{TW}" stroke="{RED}" stroke-width="1.2"/>')
    g.append(f'<text x="0" y="127" fill="{RED}" font-family="SF Pro Text" font-size="7" '
             f'font-weight="600" letter-spacing="1.3">WHOLE-LIFE COST · TYPICAL COMMERCIAL ASSET · 25-YEAR STUDY PERIOD</text>')
    write("lcc-stack.svg", "".join(g), W, H, fscale=1.06)


# ------------------------------------------------------------------
# 8. ESG · performance dashboard fragment
# ------------------------------------------------------------------
def esg_dashboard():
    W, H = 470, 250
    g = []
    random.seed(3)
    # trend area
    g.append(f'<text x="0" y="10" fill="{MUTED}" font-family="SF Pro Text" font-size="7" '
             f'font-weight="600" letter-spacing="1.3">EMISSIONS INTENSITY, INDEXED TO BASELINE YEAR</text>')
    pts, apts = [], []
    for i in range(9):
        x = i * (470 / 8)
        v = 100 - i * 7.4 + math.sin(i * 1.4) * 4
        y = 26 + (100 - v) / 60 * 96
        pts.append(f"{x:.1f} {y:.1f}")
    g.append(f'<path d="M{"L".join(pts)}L470 122L0 122Z" fill="{SKY}" opacity=".45"/>')
    g.append(f'<path d="M{"L".join(pts)}" stroke="{NAVY}" stroke-width="2"/>')
    # target line
    g.append(f'<path d="M0 104H470" stroke="{RED}" stroke-width="1" stroke-dasharray="4 3"/>')
    g.append(f'<text x="470" y="100" text-anchor="end" fill="{RED}" font-family="SF Pro Text" '
             f'font-size="7" font-weight="600" letter-spacing=".9">2030 TARGET</text>')
    g.append(f'<path d="M0 122H470" stroke="{LINE}" stroke-width=".8"/>')
    for i, yr in enumerate(["2020", "2021", "2022", "2023", "2024", "2025", "2026", "2027", "2028"]):
        x = i * (470 / 8)
        anc = "start" if i == 0 else ("end" if i == 8 else "middle")
        g.append(f'<text x="{x:.0f}" y="135" text-anchor="{anc}" fill="{MUTED}" '
                 f'font-family="SF Pro Text" font-size="6.6">{yr}</text>')
    # KPI bars
    kpis = [("ENERGY", .74), ("WATER", .61), ("WASTE DIVERTED", .83), ("RENEWABLE SHARE", .38)]
    for i, (nm, v) in enumerate(kpis):
        y = 168 + i * 21
        g.append(f'<text x="0" y="{y+3}" fill="{NAVY}" font-family="SF Pro Text" font-size="7.4" '
                 f'font-weight="600" letter-spacing=".5">{nm}</text>')
        g.append(f'<rect x="150" y="{y-5}" width="256" height="7" fill="{"#EDF0F4"}"/>')
        g.append(f'<rect x="150" y="{y-5}" width="{256*v:.0f}" height="7" fill="{NAVY if i<3 else RED}"/>')
        g.append(f'<text x="470" y="{y+3}" text-anchor="end" fill="{MUTED}" '
                 f'font-family="SF Pro Display" font-size="9.5" font-weight="500">{int(v*100)}%</text>')
    write("esg-dashboard.svg", "".join(g), W, H, fscale=1.75)


# ------------------------------------------------------------------
# 9. Embodied carbon by material · horizontal hotspot bars
# ------------------------------------------------------------------
def lca_hotspots():
    W, H = 470, 220
    items = [("Concrete & substructure", .84), ("Structural steel", .58),
             ("Façade & glazing", .41), ("Finishes", .24),
             ("MEP systems", .21), ("Site & external works", .11)]
    g = []
    g.append(f'<text x="0" y="9" fill="{MUTED}" font-family="SF Pro Text" font-size="7" '
             f'font-weight="600" letter-spacing="1.3">EMBODIED CARBON HOTSPOTS · kgCO₂e/m² BY ELEMENT (A1–A5)</text>')
    for i, (nm, v) in enumerate(items):
        y = 40 + i * 29
        c = RED if i == 0 else (NAVY if i < 3 else BLUE2)
        g.append(f'<text x="0" y="{y+4}" fill="{NAVY}" font-family="SF Pro Text" '
                 f'font-size="7.6" letter-spacing=".2">{nm}</text>')
        g.append(f'<rect x="222" y="{y-7}" width="{240*v:.0f}" height="13" fill="{c}"/>')
        g.append(f'<text x="{222+240*v+8:.0f}" y="{y+5}" fill="{MUTED}" '
                 f'font-family="SF Pro Display" font-size="9" font-weight="500">{int(v*430)}</text>')
    write("lca-hotspots.svg", "".join(g), W, H, fscale=1.72)


# ------------------------------------------------------------------
# 10. Energy end-use donut
# ------------------------------------------------------------------
def energy_donut():
    W = H = 210
    data = [("HVAC", 46, NAVY), ("Lighting", 19, BLUE), ("Equipment", 16, BLUE2),
            ("Fans & pumps", 12, BLUE3), ("DHW", 7, SKY)]
    cx = cy = 105
    r, sw = 74, 26
    g, a0 = [], -90.0
    for nm, v, c in data:
        a1 = a0 + v / 100 * 360
        la = 1 if (a1 - a0) > 180 else 0
        x0 = cx + r * math.cos(math.radians(a0)); y0 = cy + r * math.sin(math.radians(a0))
        x1 = cx + r * math.cos(math.radians(a1)); y1 = cy + r * math.sin(math.radians(a1))
        g.append(f'<path d="M{x0:.2f} {y0:.2f}A{r} {r} 0 {la} 1 {x1:.2f} {y1:.2f}" '
                 f'stroke="{c}" stroke-width="{sw}" fill="none"/>')
        a0 = a1 + 1.6
    g.append(f'<text x="{cx}" y="{cy-2}" text-anchor="middle" fill="{NAVY}" '
             f'font-family="SF Pro Display" font-size="26" font-weight="200" letter-spacing="-1.4">118</text>')
    g.append(f'<text x="{cx}" y="{cy+15}" text-anchor="middle" fill="{MUTED}" '
             f'font-family="SF Pro Text" font-size="7" font-weight="600" letter-spacing="1.1">kWh/m²/yr EUI</text>')
    write("energy-donut.svg", "".join(g), W, H, fscale=1.40)


if __name__ == "__main__":
    cover_facade(); daylight_plan(); daylight_section(); energy_bars()
    ghg_waterfall(); lcc_curves(); lcc_stack(); esg_dashboard()
    lca_hotspots(); energy_donut()
    print("done →", os.path.abspath(OUT))
