#!/usr/bin/env python3
"""Build eight source-grounded, editable SVGs for SUBSTELLAR edition 03.

This is documentation artwork generation, not Minecraft mod code. The output
uses real SVG text and measured wrapping. Existing source files and artwork are
read only. Run from the delivered sources directory or the task workspace.
"""
from __future__ import annotations

import json
import math
from pathlib import Path
from xml.sax.saxutils import escape
from xml.etree import ElementTree

from PIL import ImageFont

HERE = Path(__file__).resolve().parent
SOURCE = HERE / "substellar" / "sources"
if not SOURCE.exists():
    SOURCE = HERE
OUT = HERE / "illustrated_03_diagrams"
OUT.mkdir(exist_ok=True)

FP = json.loads((SOURCE / "first_playable_register.json").read_text())
T1 = json.loads((SOURCE / "tier1_model_register.json").read_text())
IND = json.loads((SOURCE / "industry_progression_register.json").read_text())
ASSET = {a["id"]: a for a in T1["assets"]}
RECIPE = {a["id"]: a for a in FP["recipes"]}

BG = "#101b25"
PANEL = "#162733"
PANEL2 = "#1b303b"
INK = "#f3eee4"
MUTED = "#afc1cb"
LINE = "#36515f"
TEAL = "#8ccfcd"
ORANGE = "#efad6a"
GOLD = "#e8ce7c"
BLUE = "#88badd"
FONT_NORMAL = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"


def font(size, bold=False):
    return ImageFont.truetype(FONT_BOLD if bold else FONT_NORMAL, int(size))


def width(s, size=19, bold=False):
    return font(size, bold).getlength(s)


def wrap(s, available, size=19, bold=False):
    out = []
    for para in str(s).split("\n"):
        words = para.split()
        if not words:
            out.append("")
            continue
        line = words.pop(0)
        for word in words:
            test = line + " " + word
            if width(test, size, bold) > available:
                out.append(line)
                line = word
            else:
                line = test
        out.append(line)
    return out


class SVG:
    def __init__(self, ident, title, subtitle, height, alt):
        self.ident, self.title, self.height, self.alt = ident, title, height, alt
        self.parts = []
        self.text_bounds = []
        self.checks = []
        self.rect(0, 0, 1200, height, BG, rx=0)
        self.text(54, 47, "SUBSTELLAR  /  ILLUSTRATED OVERVIEW", 15, TEAL, True)
        title_lines = wrap(title, 1092, 34, True)
        for i, line in enumerate(title_lines):
            self.text(54, 101 + i * 42, line, 34, INK, True)
        self.para(54, 142 + (len(title_lines) - 1) * 42, subtitle, 1088, 19)

    def rect(self, x, y, w, h, fill=PANEL, stroke=None, rx=14, dash=None, opacity=None):
        attrs = f'x="{x}" y="{y}" width="{w}" height="{h}" rx="{rx}" fill="{fill}"'
        if stroke:
            attrs += f' stroke="{stroke}" stroke-width="1.5"'
        if dash:
            attrs += f' stroke-dasharray="{dash}"'
        if opacity is not None:
            attrs += f' opacity="{opacity}"'
        self.parts.append(f"<rect {attrs}/>")

    def path(self, d, stroke=TEAL, sw=2.4, fill="none", arrow=False, dash=None, opacity=None):
        attrs = f'd="{d}" fill="{fill}" stroke="{stroke}" stroke-width="{sw}" stroke-linejoin="round" stroke-linecap="round"'
        if arrow:
            attrs += f' marker-end="url(#{self.ident}-arrow-{self.color_key(stroke)})"'
        if dash:
            attrs += f' stroke-dasharray="{dash}"'
        if opacity is not None:
            attrs += f' opacity="{opacity}"'
        self.parts.append(f"<path {attrs}/>")

    @staticmethod
    def color_key(c):
        return c.replace("#", "")

    def line(self, x1, y1, x2, y2, color=TEAL, arrow=False, dash=None, sw=2.4):
        self.path(f"M {x1} {y1} L {x2} {y2}", color, sw, arrow=arrow, dash=dash)

    def circle(self, x, y, r, fill=TEAL, stroke=None, sw=2):
        self.parts.append(f'<circle cx="{x}" cy="{y}" r="{r}" fill="{fill}"' + (f' stroke="{stroke}" stroke-width="{sw}"' if stroke else "") + "/>")

    def text(self, x, y, s, size=19, color=INK, bold=False, anchor="start"):
        self.parts.append(f'<text x="{x}" y="{y}" font-size="{size}" fill="{color}" font-weight="{700 if bold else 400}" text-anchor="{anchor}">{escape(str(s))}</text>')
        w = width(str(s), size, bold)
        left = x - (w / 2 if anchor == "middle" else w if anchor == "end" else 0)
        self.text_bounds.append((left, y - size, left + w, y + size * 0.24, str(s)))

    def para(self, x, y, s, w, size=19, color=MUTED, bold=False, leading=None):
        lead = leading or size * 1.4
        lines = wrap(s, w, size, bold)
        for i, line in enumerate(lines):
            self.text(x, y + i * lead, line, size, color, bold)
        return y + len(lines) * lead

    def label(self, x, y, s, color=TEAL):
        self.text(x, y, s.upper(), 15, color, True)

    def card(self, x, y, w, h, title, body="", tag=None, accent=TEAL, size=19, fill=PANEL, dash=None):
        self.rect(x, y, w, h, fill, LINE if dash is None else accent, dash=dash)
        cy = y + 30
        if tag:
            self.label(x + 20, cy, tag, accent)
            cy += 31
        cy = self.para(x + 20, cy, title, w - 40, 23, INK, True, 29)
        if body:
            cy += 8
            cy = self.para(x + 20, cy, body, w - 40, size)
        self.checks.append((cy - (size * 1.4 if body else 29) + size * .24 <= y + h - 12, title, round(cy, 1), y+h))
        return cy

    def note(self, x, y, w, h, title, body, accent=ORANGE):
        self.card(x, y, w, h, title, body, accent=accent, fill=PANEL2)
        self.rect(x, y + 14, 4, h - 28, accent, rx=2)

    def footer(self, source, note="Editable planning diagram · relationships and proposed defaults; not implementation evidence."):
        y = self.height - 76
        self.line(54, y, 1146, y, LINE, sw=1)
        self.para(54, y + 26, source, 1092, 14, MUTED, leading=19)
        self.text(54, self.height - 20, note, 13, MUTED)

    def save(self, filename):
        errors = []
        for x1, y1, x2, y2, s in self.text_bounds:
            if x1 < 35 or x2 > 1165 or y1 < 20 or y2 > self.height - 7:
                errors.append({"text": s, "bounds": [x1,y1,x2,y2]})
        for ok, title, end, bottom in self.checks:
            if not ok:
                errors.append({"card": title,"text_end":end,"bottom":bottom})
        if errors:
            raise ValueError(f"{filename}: text bounds failed: {errors}")
        markers = []
        for c in [TEAL, ORANGE, GOLD, BLUE, MUTED, LINE]:
            markers.append(f'<marker id="{self.ident}-arrow-{self.color_key(c)}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 Z" fill="{c}"/></marker>')
        svg = f'<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="{self.height}" viewBox="0 0 1200 {self.height}" role="img" aria-labelledby="{self.ident}-title {self.ident}-desc">\n<title id="{self.ident}-title">{escape(self.title)}</title><desc id="{self.ident}-desc">{escape(self.alt)}</desc>\n<defs>' + "".join(markers) + '</defs>\n<style>text{font-family:"DejaVu Sans",Arial,sans-serif}path,rect,circle{vector-effect:non-scaling-stroke}</style>\n' + "\n".join(self.parts) + "\n</svg>\n"
        ElementTree.fromstring(svg)
        (OUT / filename).write_text(svg, encoding="utf-8")
        return {"file": filename, "bytes": len(svg.encode()), "text_items": len(self.text_bounds), "card_checks": len(self.checks), "height": self.height}


RECORDS = []
CHECKS = []


def record(svg, filename, caption, source_ids, related_chapters, slice_number):
    CHECKS.append(svg.save(filename))
    RECORDS.append({"id":svg.ident,"file":filename,"title":svg.title,"alt":svg.alt,"caption":caption,"source_ids":source_ids,"related_chapters":related_chapters,"slice":slice_number,"kind":"diagram"})


def first_playable_route():
    alt = "Old Valley begins with guesthouse restoration, then its authored crossing and quarry service. A dry route connects the quarry to a Copperwash survey ledge and returns with knowledge of tin and copper. The first workshop introduces a useful hoist improvement without completing Chapter 4. A free-built river crossing does not complete the authored project; the optional submerged branch holds no essential item."
    d = SVG("ILL-D01", "Old Valley: a journey that returns to useful work", "A dependency and experience map of the first playable. Locations adapt to supported terrain; this is not a fixed site plan.", 1550, alt)
    d.label(60,218,"Main experience")
    d.label(800,218,"Choices and scope")
    nodes = [
        (245, 170, "A place to stay", "Restore the guesthouse entrance, sleeping place, assigned storage and common access. Establish a dependable return point.", "01 · SET01 / CHAPTER 1"),
        (446, 162, "The old crossing", "Inspect the far approach. Restore supports, deck and transport finishing; make the authored route walkable.", "02 · SET02 / CHAPTER 2"),
        (639, 177, "The quarry", "Restore the one work-bay bench, safe platform, fixed cargo landings and baseline hoist. Ordinary stone preparation becomes useful.", "03 · SET03 / CHAPTER 3"),
        (847, 188, "Copperwash Channels", "Take the dry entrance to a survey ledge. Read the copper-stained face, find accessible tin, learn the return junction and follow a short loop home.", "04 · EAR02 / FIRST DESCENT"),
        (1066, 193, "The first workshop service", "Bring the find into a visible material chain. Make fittings, service the existing hoist and carry two occupied cargo stacks in one trip.", "05 · IP01 / LIMITED SET04 INTRODUCTION"),
    ]
    for y,h,title,body,tag in nodes:
        d.card(60,y,700,h,title,body,tag)
    for i in range(len(nodes)-1):
        y,h,*_ = nodes[i]
        d.line(410,y+h+5,410,nodes[i+1][0]-8,arrow=True)
    d.card(800,245,340,245,"Preparation stays reachable", "Required bridge stock is on the near bank. Quarry completion does not require tin or an advanced machine.", "Fair access", ORANGE,18)
    d.card(800,515,340,255,"Free building remains valid", "Swimming or a personal bridge can help travel. The journal still tracks the authored crossing as its own project.", "Player choice", ORANGE,18)
    d.card(800,847,340,222,"Optional water branch", "A dry observation alcove offers a view or specimen. Its submerged continuation contains no indispensable resource or only-copy reward.", "A clearly optional detour", BLUE,18)
    d.line(767,941,792,941,BLUE,arrow=True,dash="6 5")
    d.path("M 774 1166 L 786 1166 L 786 762 L 768 762",ORANGE,arrow=True,dash="6 5")
    d.note(60,1300,1080,148,"The ending is a changed place", "Prepared stone and improved cargo handling remain useful. Chapter 4's wider workshop, grain mill and market preparation stay open; Chapter 5's first market remains a later payoff.")
    d.footer("Sources: FP-PLAN-02–07, FP-PLAN-11–12 · SET01–04 · EAR02 · IP01.")
    record(d,"first-playable-route.svg","The route preserves the five-chapter Old Valley structure while showing the limited endpoint of the first playable. The orange return link connects the workshop improvement back to the existing quarry service.",["FP-PLAN-02","FP-PLAN-03","FP-PLAN-04","FP-PLAN-05","FP-PLAN-06","FP-PLAN-07","FP-PLAN-11","FP-PLAN-12","SET01","SET02","SET03","SET04","EAR02","IP01"],["First Playable — Old Valley, the Quarry, and the First Workshop"],3)


def project_states():
    alt = "The project controller moves from Discovered to Preview, Claimed or assigned, Awaiting requirements, Ready, Building, Complete and Upgrade available. Paused and Blocked have different causes and preserve custody. Resuming requires a renewed eligibility or clearance check. Cancellation recovers only uncommitted stock. Completed stages remain built, and a completed production batch returns its product rather than both product and ingredients."
    d=SVG("ILL-D02","Project states: progress, interruption and custody","The visible state explains the next useful action. A paused region, a physical obstruction and a material shortage are different conditions.",1860,alt)
    d.label(60,218,"Main project progression")
    d.label(680,218,"Interruption is explicit")
    stages=[
        ("Discovered","Inspect the purpose and find the controller."),
        ("Preview","See the real footprint, stages and access."),
        ("Claimed or assigned","Confirm the project and permitted operators."),
        ("Awaiting requirements","Contribute accepted stock; retain the receipt."),
        ("Ready","Review the next stage and explicitly start."),
        ("Building","Commit one defined stage with clear space."),
        ("Complete","Commission the promised service once."),
        ("Upgrade available","Offer optional work; keep the finished service."),
    ]
    ys=[]
    for i,(title,body) in enumerate(stages):
        y=245+i*126;ys.append(y)
        d.card(60,y,530,104,title,body,size=17)
        if i<7:d.line(325,y+109,325,y+119,arrow=True)
    d.card(680,466,460,208,"Paused", "Manual pause or no eligible participant in the managed region. Save the same stock and progress. Resume only when permitted and active.", "Activity condition", GOLD,18)
    d.card(680,748,460,235,"Blocked", "A player, owned block, chest, full receiver or unsupported terrain prevents the next action. Mark the affected space and keep stock; clear and recheck it.", "Actionable obstruction", ORANGE,18)
    # Conditional branches from the working state; the return arrows require revalidation.
    d.path("M 597 927 L 624 927 L 624 570 L 672 570",GOLD,arrow=True,dash="7 5")
    d.path("M 597 946 L 648 946 L 648 875 L 672 875",ORANGE,arrow=True,dash="7 5")
    d.path("M 911 456 L 911 369 L 610 369 L 610 775 L 597 775",TEAL,arrow=True,dash="5 6")
    d.text(685,350,"Recheck before resuming work",17,TEAL,True)
    d.note(680,1035,460,221,"Cancellation is a custody decision", "Uncommitted recoverable stock goes to a controlled claimable output. Built stages and their costs remain. Completed chapter dismantling is outside the first playable.")
    d.label(60,1317,"The interface keeps these accounts separate")
    ledger=[("Available in project","Accepted, real stock held for the order."),("Committed to this stage","Reserved for one operation and result."),("Built into the structure","Consumed into a persistent completed stage.")]
    for i,(title,body) in enumerate(ledger):
        x=60+i*370;d.card(x,1343,340,165,title,body,size=17)
        if i<2:d.line(x+346,1425,x+363,1425,ORANGE,arrow=True)
    d.note(60,1544,1080,191,"Recoverable and still required remain visible too", "A shortage never erases delivered stock. An interrupted operation cannot produce both a refund and a completed stage. A completed machine batch yields its product, not product plus ingredients. Reopening a screen or reloading must not duplicate the outcome.")
    d.footer("Sources: FP-PLAN-13–16 · Construction, stock custody, regional activity and interface requirements.")
    record(d,"project-states.svg","A conceptual state and custody map. Dashed interruption branches retain the same operation; resumption always depends on the relevant activity and clearance checks. Normal Minecraft simulation is outside this managed-project diagram.",["FP-PLAN-13","FP-PLAN-14","FP-PLAN-15","FP-PLAN-16"],["First Playable — Old Valley, the Quarry, and the First Workshop"],2)


def workshop_chain():
    a=RECIPE['FP-R01'];b=RECIPE['FP-R02'];c=RECIPE['FP-R03']
    assert a['inputs']=={'copper_ingots':3,'tin_ingots':1,'hearth_fuel_items':1}
    assert a['outputs']=={'bronze_fittings':4}
    assert b['inputs']=={'bronze_fittings':4,'planks':4}
    assert c['inputs']=={'iron_ingots':4,'planks':4}
    demo=FP['resource_budget']['two_stack_hoist_demonstration']
    assert demo['occupied_slots']==2 and demo['baseline_trips']==2 and demo['upgraded_trips']==1
    alt = "Proposed first alloy batch: three copper ingots, one tin ingot and one supported hearth fuel item produce four bronze fittings. Those four fittings and four planks service the existing hoist from one cargo slot to two. Four iron ingots and the same four planks are an alternative to that one cradle bill. Six prepared walls and one spare fuel item occupy two real stacks: two baseline trips become one upgraded trip; both goods return to stock."
    d=SVG("ILL-D03","The first workshop: one batch, one useful improvement","Exact proposed quantities from the First Playable register. Basic raw-metal smelting is a separate preparation step; final native recipes remain to be checked.",1690,alt)
    d.note(60,223,1080,115,"Start from a working quarry", "The restored bench and one-slot manual hoist already serve the quarry. Bronze expands a useful service; it does not grant permission to use it.")
    d.label(60,383,"Default alloy route",ORANGE)
    d.label(790,383,"Common-iron alternative",TEAL)
    d.card(60,406,650,128,"3 copper + 1 tin + 1 fuel", "Accepted ingots and one supported hearth fuel item.","FP-R01 · PROPOSED INPUTS",ORANGE,18)
    d.line(385,540,385,562,ORANGE,arrow=True)
    d.card(60,571,650,147,"IND-M07 · Alloy hearth", "One batch yields 4 bronze fittings. Reserve output capacity before starting.","45 ACTIVE SECONDS · PROPOSED",ORANGE,18)
    d.line(385,723,385,733,ORANGE,arrow=True)
    d.card(60,740,650,150,"4 fittings + 4 planks", "IND-M01 prepares and installs the wider cradle at the existing IND-M53 hoist.","FP-R02 = FP-C06 · ONE SHARED BILL",ORANGE,18)
    d.card(790,406,350,191,"4 iron + 4 planks", "Uses the same bench and existing hoist. Replaces the bronze cradle bill; no second plank cost.","FP-R03",TEAL,18)
    d.card(790,635,350,203,"Same cargo benefit", "Available if tin is lost or retained as a sample. It does not count as producing bronze or observing the alloy process.","Distinct learning outcome",TEAL,18)
    d.path("M 385 898 V 919 H 600 V 945",ORANGE,arrow=True)
    d.path("M 965 846 V 919 H 600",TEAL)
    d.card(60,960,1080,156,"One occupied cargo slot → two", "Keep the same fixed route, trip time and native stack limits. Install only with the cradle empty, loading bay clear and authorized start.","IND-M53 · MANUAL CARGO HOIST",TEAL,19)
    d.label(60,1140,"A visible comparison using existing goods")
    d.rect(60,1162,520,176,PANEL,LINE)
    d.rect(620,1162,520,176,PANEL,LINE)
    d.text(80,1195,"Before · two trips",23,INK,True)
    d.text(640,1195,"After · one trip",23,INK,True)
    d.rect(83,1218,218,47,PANEL2,ORANGE,7);d.text(192,1248,"6 prepared walls",18,INK,anchor="middle")
    d.text(318,1248,"then",17,MUTED)
    d.rect(377,1218,177,47,PANEL2,ORANGE,7);d.text(465,1248,"1 spare fuel",18,INK,anchor="middle")
    d.rect(643,1218,218,47,PANEL2,TEAL,7);d.text(752,1248,"6 prepared walls",18,INK,anchor="middle")
    d.rect(879,1218,237,47,PANEL2,TEAL,7);d.text(997,1248,"1 spare fuel",18,INK,anchor="middle")
    d.text(80,1310,"Each is an actual occupied stack.",17,MUTED)
    d.text(640,1310,"Both goods return to stock; no added bill.",17,MUTED)
    budget=FP['resource_budget']['construction_default']
    assert budget=={'planks':84,'cobblestone':68,'iron_ingots':14,'clay_balls':8,'bronze_fittings':4}
    d.note(60,1382,1080,198,"The construction total is a separate shared budget", "84 planks · 68 cobblestone · 14 iron ingots · 8 clay balls · 4 bronze fittings. Expand the fittings into the one alloy batch when planning raw stock. Personal preparation and the 6-cobblestone wall demonstration are separate allowances; do not count the hearth fuel twice.")
    d.footer("Sources: FP-PLAN-07–10 · FP-R01–04 · FP-C06 · First Playable resource budget.")
    record(d,"workshop-chain.svg","Proposed playtest quantities are reproduced from the structured register. The iron route replaces the cradle cost. The diagram does not assign a new material recipe to the six-wall demonstration or imply the preparation allowance is shared by every player.",["FP-PLAN-07","FP-PLAN-08","FP-PLAN-09","FP-PLAN-10","IND-M01","IND-M07","IND-M53","MAT01"],["First Playable — Old Valley, the Quarry, and the First Workshop","Production chains, balance, and machine progression"],3)


def industry_network():
    assert len(IND['machine_functions'])==66
    alt="Two representative production paths use existing machine functions: timber through the Timber mill to boards and recoverable offcuts; iron plus supported carbon through the Controlled steel furnace and Forming press to useful structural parts. A Workshop dynamo supplies a Switchboard with an optional Stationary power bank and selected powered processes. Existing goods pass through Stockroom and Freight dispatch to an active receiving region without starting new remote production. These are relationships, not universal recipes or mandatory long chains."
    d=SVG("ILL-D04","Industry: material, power and freight stay legible","Representative relationships among 66 named machine functions. Each actual recipe declares its own inputs, power, secondary outputs and capacity.",1880,alt)
    d.label(60,221,"Material paths",ORANGE)
    d.card(60,246,245,193,"Accepted timber", "Logs or compatible prepared stock.","INPUT",ORANGE,18)
    d.card(335,246,325,193,"Timber mill", "Prepared boards and structural stock; collect recoverable offcuts separately.","IND-M03",ORANGE,18)
    d.card(690,246,450,193,"Useful workshop stock", "Small orders can still use hand preparation. More throughput earns its larger space and handling needs.","BOARDS / STRUCTURAL STOCK",ORANGE,18)
    d.line(311,324,329,324,ORANGE,arrow=True);d.line(666,324,684,324,ORANGE,arrow=True)
    d.card(60,476,245,228,"Iron + carbon", "Supported stock, suitable refractory construction and accessible fuel.","INPUT",ORANGE,18)
    d.card(335,476,245,228,"Controlled steel furnace", "Structural steel plus bounded slag output.","IND-M08",ORANGE,18)
    d.card(610,476,245,228,"Forming press", "Plates, brackets and beams; regional capacity upgrade.","IND-M04",ORANGE,18)
    d.card(885,476,255,228,"Parts and orders", "Reserve real stock for tools, construction or supported equipment.","USEFUL OUTPUT",ORANGE,18)
    for x in [305,580,855]:d.line(x+6,574,x+24,574,ORANGE,arrow=True)
    d.text(62,742,"Carbon retort IND-M06 is an optional efficiency route; suitable ordinary substitutes remain available.",17,MUTED)
    d.label(60,802,"Power network",GOLD)
    d.card(60,831,310,200,"Workshop dynamo", "Authored water, wind or small fueled source. A compact fueled option avoids a geography-only start.","IND-M32",GOLD,18)
    d.card(425,831,325,200,"Switchboard", "Trace a machine to its supply. Show branch priority and readable overload stops.","IND-M38",GOLD,18)
    d.card(805,831,335,200,"Powered processes", "Only declared consumers draw power. Heat or hand work may have another supported source.","RECIPE-SPECIFIC DEMAND",GOLD,18)
    d.line(376,930,419,930,GOLD,arrow=True);d.line(756,930,799,930,GOLD,arrow=True)
    d.card(425,1080,325,184,"Stationary power bank", "Optional buffer with finite capacity and discharge rate.","IND-M37",GOLD,17)
    d.line(571,1037,571,1072,GOLD,arrow=True);d.line(603,1072,603,1037,GOLD,arrow=True)
    d.note(60,1080,310,184,"Scale when useful", "No nuclear plant or long ore-processing chain is a universal first-flight requirement.",GOLD)
    d.note(805,1080,335,184,"Separate connections", "Material, water, power and control retain their declared roles and feedback.",TEAL)
    d.label(60,1320,"Freight moves already-produced goods",TEAL)
    d.rect(55,1341,696,286,"none",LINE,14,dash="7 6")
    d.label(72,1368,"Source region")
    d.card(70,1387,300,218,"Stockroom and order desk", "Reserve existing authorized goods; retain finite physical storage.","IND-M55",TEAL,17)
    d.card(405,1387,325,218,"Dispatch service", "Use an established route and its actual cargo capacity.","IND-M56",TEAL,17)
    d.line(376,1488,399,1488,TEAL,arrow=True)
    d.rect(796,1341,349,286,"none",TEAL,14,dash="7 6")
    d.label(812,1368,"Active receiving region")
    d.card(812,1387,316,218,"Receive finite cargo", "Show destination, progress and any waiting condition.","IND-M56",TEAL,17)
    d.line(737,1488,804,1488,TEAL,arrow=True)
    d.para(60,1673,"Receiving-region activity may advance delivery while the source is absent. It never starts fresh remote production. Local output backpressure retains goods instead of dropping them into the world.",1080,20)
    d.footer("Sources: industry progression register · IND-M03/04/06/08/32/37/38/54/55/56 · Tier 1 access rules.")
    record(d,"industry-network.svg","These selected paths explain how the catalogue functions fit together. They are not a mandatory sequence through all 66 machines, an exact recipe list, a power-capacity chart or an implementation claim.",["IND-M03","IND-M04","IND-M06","IND-M08","IND-M32","IND-M37","IND-M38","IND-M54","IND-M55","IND-M56","IP01","IP02","IP04"],["Machinery and industrial systems","Industry and progression","Production chains, balance, and machine progression"],7)


def earth_geography():
    alt="A conceptual Earth cross-section shows the familiar Overworld above connected Shallows, with humid Deepwilds and drier Underreach overlapping laterally, Abyss basins and voids below, deep thermal Mantle pockets and a fictional deep-crust Heart called Core. Routes run sideways, descend, loop and return to refuges. The diagram is not to scale and assigns no final realm Y bands. The working envelope is 512 total levels, Y minus 192 to 319, about 255 below a reference surface at Y63."
    d=SVG("ILL-D05","Earth: depth, lateral regions and return routes","Conceptual section through one possible regional arrangement. Colored areas express environmental tendencies; neither their shapes nor their heights are fixed allocations.",1640,alt)
    d.note(60,221,1080,115,"Working world envelope", "512 total Y levels: −192 through 319. From a reference surface near Y 63, the floor is about 255 blocks below. No permanent Y range is assigned to any realm.",TEAL)
    # Irregular, overlapping ecological/geological regions. This is deliberately not a Y-scaled chart.
    d.rect(60,369,730,1005,"#13212c",LINE,15)
    d.path("M 62 444 C 115 452 152 398 228 415 C 306 435 340 390 411 429 C 480 459 514 414 583 442 C 661 473 718 405 788 434 L 788 542 C 697 501 654 556 570 533 C 458 514 395 579 307 548 C 212 517 146 571 62 532 Z", "#49635b",1.5,"#263e3d")
    d.path("M 62 517 C 157 536 210 506 317 539 C 406 567 505 512 588 542 C 692 570 737 520 788 540 L 788 736 C 684 706 643 770 548 708 C 473 661 394 752 289 690 C 174 626 140 709 62 677 Z", "#537b75",1.5,"#274447")
    d.path("M 64 672 C 147 626 226 668 293 637 C 353 609 438 676 477 730 C 449 810 397 888 286 906 C 219 859 143 911 62 861 Z", "#538477",1.5,"#27524b")
    d.path("M 485 644 C 572 673 629 625 698 686 L 788 656 L 788 1000 C 720 1022 660 950 602 968 C 503 929 432 876 457 799 Z", "#756e66",1.5,"#454647")
    d.path("M 62 860 C 181 905 236 852 294 901 C 379 838 453 868 500 940 C 570 1006 690 958 788 1012 L 788 1181 C 652 1161 588 1120 473 1151 C 381 1193 292 1116 208 1173 C 126 1153 99 1086 62 1071 Z", "#4e7a89",1.5,"#203d52")
    d.path("M 62 1118 C 165 1168 235 1110 331 1145 C 414 1180 485 1095 572 1143 C 677 1162 714 1123 788 1179 L 788 1369 L 63 1369 Z", "#916f59",1.5,"#594537")
    d.path("M 171 1287 C 214 1242 292 1239 336 1267 C 392 1229 458 1280 470 1322 C 405 1352 317 1360 252 1336 C 193 1355 164 1316 171 1287 Z", "#b09083",1.7,"#624f50")
    # Region labels and context labels are placed away from the route.
    for x,y,s,color in [(95,475,"Overworld",INK),(99,583,"Shallows",INK),(95,771,"Deepwilds",INK),(538,804,"Underreach",INK),(104,996,"Abyss",INK),(550,1310,"Mantle",INK),(247,1313,"Core · Heart",INK)]:
        d.text(x,y,s,26,color,True)
    d.text(94,506,"Familiar surface and ordinary life",16,MUTED)
    d.text(94,613,"Roots, water channels, dry ledges",16,MUTED)
    d.text(94,801,"Humid living chambers",16,MUTED)
    d.text(524,834,"Drier mineral routes",16,MUTED)
    d.text(95,1026,"Watershed basins, shelves and voids",16,MUTED)
    d.text(516,1340,"Local thermal districts",16,MUTED)
    # Main route and optional lateral loop.
    d.path("M 650 461 C 688 510 665 549 600 588 C 518 642 396 673 344 741 C 331 792 440 799 504 842 C 550 880 549 919 513 960 C 473 1005 639 1070 651 1166 C 642 1224 520 1234 418 1273",ORANGE,3.5)
    d.path("M 344 741 C 330 640 488 605 635 650 C 754 680 759 899 608 908 C 556 905 534 870 504 842",TEAL,2.5,dash="8 7")
    d.path("M 513 960 C 404 973 399 1054 452 1085 C 541 1137 576 1101 599 1081",TEAL,2.5,dash="8 7")
    for x,y in [(650,461),(600,588),(344,741),(504,842),(513,960),(651,1166),(418,1273)]:
        d.circle(x,y,9,BG,ORANGE,2.5)
    for x,y in [(600,588),(344,741),(513,960),(651,1166)]:
        d.rect(x-5,y-5,10,10,INK,rx=1)
    d.label(820,403,"How to read this section")
    d.card(820,431,320,231,"Depth + lateral geography", "Realms can share elevations. Their conditions overlap across readable transitions.","REGIONS",TEAL,18)
    d.card(820,692,320,225,"Routes earn attention", "Follow shelves, shorelines, climbs and loops. An intended descent has a comprehensible ordinary route home.","EXPEDITIONS",ORANGE,18)
    d.card(820,947,320,219,"Refuges have a job", "Place supported anchors on usable landforms. Leave attractive terrain and room for the player's own construction.","AUTHORED PLACES",TEAL,18)
    d.rect(820,1200,320,174,PANEL,LINE)
    d.line(842,1236,892,1236,ORANGE,sw=3.5);d.text(910,1242,"Example route",17,INK)
    d.line(842,1272,892,1272,TEAL,dash="8 7");d.text(910,1278,"Lateral option",17,INK)
    d.circle(866,1310,9,BG,ORANGE,2.5);d.rect(861,1305,10,10,INK,rx=1);d.text(910,1316,"Refuge / junction",17,INK)
    d.note(60,1410,1080,128,"Core is the fictional deep-crust Heart", "It is not the literal planetary center. This section makes the intended relationships visible; it does not demonstrate that every realm already fits the working generation envelope.")
    d.footer("Sources: world-height reconciliation; World generation and the Earth atlas; EAR01–EAR07.","Editable conceptual geography · not to scale · no final biome boundaries or implementation claims.")
    record(d,"earth-geography.svg","The section emphasizes lateral overlap, supported refuges, route choices and transition space. Its graphic proportions are conceptual. The original seven-realm atlas remains the complete named-regional overview.",["EAR01","EAR02","EAR03","EAR04","EAR05","EAR06","EAR07"],["World generation and the Earth atlas","The vertical world specification","Overworld","Shallows","Deepwilds","Underreach","Abyss","Mantle","Core"],4)


def planetary_expeditions():
    alt="Moon and Mars have different expedition rhythms. The Moon begins at a natural supported landing with Earth-made equipment, a working cabin and an EQ25 starter refuge commissioned before a committed descent. Earthrise Rim and Shadow Basin connect with Regolith Hollows, Basalt Galleries and Glassfall Vaults. Regolith Ceramic and Lunar Glass Filament support revisits. Mars starts with imported protection, service stock and a dependable refuge, then follows Windcut Tablelands and Buried Delta into Ochre Galleries, Brine Scarps and Rootstone Chambers. Ferric Lattice and Brine Catalyst improve later field work; neither supplies the first protective equipment."
    d=SVG("ILL-D06","Moon and Mars: prepared arrival, useful return","Two destination rhythms. Surface and underground names describe connected regional families, not fixed geological slabs or identical mission routes.",1840,alt)
    d.label(60,219,"Moon · a foothold carried from Earth",TEAL)
    d.label(620,219,"Mars · a region learned through travel",ORANGE)
    rows=[
        (246,218,("Arrive with the complete first loadout","Working airlocked Explorer, Earth-made EQ25 shelter, suit, breathing supply, field cells, light, tools and protected return reserves.","MO-PLAN-01"),("Arrive with independent capability","Imported protection, shelter, repair and sample stock, weather reader, and a rover or supported small-rover assembly materials.","MA-PLAN-01")),
        (502,230,("Commission the first refuge","Park on surveyed natural ground. The cabin remains a retreat while the starter shelter and marked entrance become usable before committed descent.","CRAFT + EQ25"),("Inspect, shelter and read the weather","Verify the refuge and service area. Observe the first weather change from safety. Walking-scale surveying remains useful before rover assembly.","LANDING / SERVICE SITE")),
        (770,280,("Connect the first underground route","Earthrise Rim ↔ Shadow Basin\nRegolith Hollows → Basalt Galleries → selected Glassfall Vault routes\nRoof openings and distant exits can form loops.","MO-S1/S2 · MO-L1/L2/L3"),("Connect terrain and travel modes","Windcut Tablelands ↔ Buried Delta\nOchre Galleries → Brine Scarps → Rootstone Chambers\nWalking routes, drivable gullies and caches serve different loads.","MA-S1/S2 · MA-L1/L2/L3")),
        (1088,224,("Bring home a useful discovery","Regolith Ceramic and Lunar Glass Filament improve habitat parts, instruments or lamps. The first useful batch can return to Earth; no lunar factory is required.","PM01 / PM02"),("Bring home a useful discovery","Ferric Lattice supports repairable frames; Brine Catalyst supports selected process and habitat improvements. Early samples work with imported equipment.","PM03 / PM04")),
        (1350,223,("Choose a stronger second expedition","Optional gallery workshop, enlarged refuge and linked observations make routes dependable. A Light Across the Basin gives the network a purpose.","LOCAL IMPROVEMENT"),("Choose a stronger regional base","The Frontier Yard adds service, storage, lab and selected habitat work. The River That Isn't There turns regional observations into a connected mission.","LOCAL IMPROVEMENT")),
    ]
    for y,h,moon,mars in rows:
        for x,item,color in [(60,moon,TEAL),(620,mars,ORANGE)]:
            title,body,tag=item;d.card(x,y,520,h,title,body,tag,color,18)
    for i in range(len(rows)-1):
        y,h,*_=rows[i]
        for x,c in [(320,TEAL),(880,ORANGE)]:d.line(x,y+h+6,x,rows[i+1][0]-9,c,arrow=True)
    d.note(60,1611,1080,119,"The first safe mission never depends on its own undiscovered reward", "Protect the homeward route and supplies. Local materials improve later capability; completing every optional outpost stage is not the price of going home.")
    d.footer("Sources: MO-PLAN-01–09 · MA-PLAN-01–09 · T1-SPEC-07/15/18 · PM01–PM04.")
    record(d,"planetary-expeditions.svg","Each lane is an experience sequence, not a requirement to visit every listed region in one outing. Lunar first arrival and return need no Moon-built pad, local refinery or external console. Mars preserves useful nearby exploration for a player who delays rover assembly.",["MO-PLAN-01","MO-PLAN-02","MO-PLAN-05","MO-PLAN-06","MO-PLAN-07","MO-PLAN-08","MO-PLAN-09","MA-PLAN-01","MA-PLAN-02","MA-PLAN-05","MA-PLAN-06","MA-PLAN-07","MA-PLAN-08","MA-PLAN-09","T1-SPEC-07","T1-SPEC-15","PM01","PM02","PM03","PM04"],["Moon — complete destination planning expansion","Mars — complete destination planning expansion"],5)


def tier1_assembly():
    alt="Earth-accessible prepared stock passes through the Part Fabrication Table into reusable rocket components. The Rocket Assembly Station receives prepared parts and controls stages in a separately linked vacant Launch Pad berth. The stages are reserve order and berth, lower frame and engine, tanks and main structure, cabin and pressure assembly, nose and fins and services, then commission one craft. A named mission manifest and readiness checks follow commissioning; the assembly worktop never contains the full-size rocket."
    d=SVG("ILL-D07","Tier 1 assembly: parts become one persistent craft","The fabrication table makes components. The assembly station controls the vehicle's construction at a separate, explicitly linked berth.",1770,alt)
    d.label(60,220,"Prepared stock → component work",ORANGE)
    d.card(60,246,270,205,"Earth stock", "Accessible early materials; no Core, Heartglass or lunar input required.","INPUT",ORANGE,18)
    d.card(373,246,337,205,"Part Fabrication Table", "Produces the manageable structural, engine, tank and related component families.","T1-M01 / IND-M02",ORANGE,18)
    d.card(753,246,387,205,"Prepared components", "Keep real quantities and reserved output capacity. No required inventory item for every decorative mesh.","T1-I01–I04",ORANGE,18)
    d.line(336,338,366,338,ORANGE,arrow=True);d.line(716,338,746,338,ORANGE,arrow=True)
    d.label(60,485,"Two different physical places")
    d.card(60,510,470,215,"Rocket Assembly Station", "Receives authorized parts and controls the stage order. Proposed 3 × 3 footprint, 2 high, with a 5 × 5 work area.","T1-M02 / IND-M46",TEAL,19)
    d.card(670,510,470,215,"Vacant Launch Pad berth", "The full Explorer is erected here. Keep the work volume and approach clear; show the selected berth in the preview.","T1-M06",TEAL,19)
    d.path("M 946 458 V 470 H 580 V 560 H 538",ORANGE,arrow=True)
    d.line(538,615,662,615,TEAL,arrow=True,dash="8 6")
    d.text(600,757,"Named same-region association · proposed maximum 12 horizontal blocks between origins",17,MUTED,anchor="middle")
    d.label(60,816,"Authored stage results · counts belong to the recipe record")
    stages=[
        ("1 · Reserve","Accept an authorized parts order and vacant berth."),
        ("2 · Frame + engine","Build the lower support and one engine-cluster role."),
        ("3 · Tanks + structure","Install the defined vessels and main structural modules."),
        ("4 · Cabin + pressure","Fit window, usable cabin and inner pressure boundary."),
        ("5 · Exterior services","Attach nose, four fixed fins and required service points."),
        ("6 · Commission","Retain one finished craft, its modules and its location."),
    ]
    for i,(title,body) in enumerate(stages):
        row=i//3;col=i%3
        if row==1:col=2-col
        x=60+370*col;y=844+210*row
        d.card(x,y,340,172,title,body,size=19)
    d.line(406,930,423,930,TEAL,arrow=True);d.line(776,930,793,930,TEAL,arrow=True)
    d.line(970,1024,970,1046,TEAL,arrow=True)
    d.line(793,1140,777,1140,TEAL,arrow=True);d.line(423,1140,407,1140,TEAL,arrow=True)
    d.note(60,1267,1080,119,"Commit each stage once", "A blocked berth retains the stock and current craft. Cancellation exposes the policy for uncommitted stock and built stages. Refit and reload never create another vehicle.")
    d.card(60,1430,510,218,"Load a named mission", "Assign crew, real cargo, fuel, breathing supply, power and protected reserves. First lunar descent also needs the Earth-carried EQ25 starter refuge.","AFTER COMMISSIONING",ORANGE,19)
    d.card(620,1430,520,218,"Ready means the essentials fit", "Read the next blocker: seat, suit, supplies, reserved return, service clearance and departure path. The Launch Key never bypasses those checks.","T1-M07 / T1-I05",TEAL,19)
    d.line(578,1539,612,1539,TEAL,arrow=True)
    d.footer("Sources: T1-SPEC-06/08/09/12/13/15/18 · T1 model register · IND-M02/46.")
    record(d,"tier1-assembly.svg","Assembly is a documented product workflow. All stated workstation dimensions and the 12-block association distance are proposed defaults. Hardware roles are kept distinct from decorative meshes and from recipe quantities.",["T1-SPEC-06","T1-SPEC-08","T1-SPEC-09","T1-SPEC-12","T1-SPEC-13","T1-SPEC-15","T1-SPEC-18","T1-M01","T1-M02","T1-M06","T1-M07","T1-I01","T1-I02","T1-I03","T1-I04","T1-I05","IND-M02","IND-M46"],["Tier 1 — Model and Implementation Specifications","Tier 1 space systems — complete first expedition"],10)


def tier1_services():
    rocket=ASSET['T1-V01']['proposed_dimensions'];pad=ASSET['T1-M06']['proposed_dimensions']
    assert rocket['height_blocks']==9 and rocket['main_body_footprint_blocks']==[3,3] and rocket['maximum_fin_footprint_blocks']==[5,5]
    assert pad['footprint_blocks']==[7,7] and pad['deck_height_blocks']==0.5
    alt="Proposed Explorer envelopes are shown separately: a 3-by-3 body, 5-by-5 fin silhouette and 9-block total height. The proposed pad has a 7-by-7 deck, 0.5 high, within a 9-by-9 controlled site. The boarding strip lies at the front between diagonal fins. A candidate lower airlock vestibule and upper cabin require real fit testing. Fuel, breathing supply, power and control use separate typed connections; control transfers no stock. Ground couplers retract before departure and reserves remain protected."
    d=SVG("ILL-D08","Tier 1 services: separate envelopes, separate supplies","A planning schematic, not a verified blueprint. Proposed scale follows T1-DEC01; cabin fit, collisions, doors and boarding still require a measured model review.",2070,alt)
    d.label(60,220,"Top view · proposed relative envelopes",TEAL)
    d.label(650,220,"Candidate occupied volumes",ORANGE)
    # A proportional top view in the X/Z plane. 45 px per proposed block.
    sx,sy,unit=100,270,45
    c=sx+4.5*unit;cy=sy+4.5*unit
    d.rect(sx,sy,405,405,"none",TEAL,0,dash="8 6")
    d.rect(sx+unit,sy+unit,315,315,"#20313d",LINE,4)
    # Closed fin envelope is indicated independently of the actual fin pieces.
    d.rect(c-2.5*unit,cy-2.5*unit,225,225,"none",ORANGE,0,dash="5 5")
    d.rect(c-1.5*unit,cy-1.5*unit,135,135,"#3d4d57",INK,5)
    for dx,dz in [(-1,-1),(1,-1),(-1,1),(1,1)]:
        pts=[(c+dx*1.0*unit,cy+dz*1.5*unit),(c+dx*2.5*unit,cy+dz*2.5*unit),(c+dx*1.5*unit,cy+dz*1.0*unit)]
        d.path("M "+" L ".join(f"{x} {y}" for x,y in pts)+" Z",ORANGE,1.5,"#705749")
    d.rect(c-.5*unit,cy+1.5*unit,unit,3*unit,"#244a47",TEAL,0)
    d.line(c-.4*unit,cy+1.5*unit,c+.4*unit,cy+1.5*unit,INK,sw=5)
    d.text(c,cy-8,"3 × 3",24,INK,True,anchor="middle")
    d.text(c,cy+22,"body",19,INK,anchor="middle")
    d.line(81,270,81,675,TEAL,sw=1.5);d.line(73,270,89,270,TEAL,sw=1.5);d.line(73,675,89,675,TEAL,sw=1.5)
    d.text(100,251,"9 × 9 controlled site",19,TEAL,True)
    d.text(120,704,"7 × 7 deck · 0.5 high",19,INK,True)
    d.text(120,734,"5 × 5 closed fin envelope",19,ORANGE,True)
    d.text(120,764,"+Z front · clear boarding strip",19,TEAL)
    d.line(c,637,c,663,TEAL,arrow=True)
    # Functional vertical stack, drawn as relationships; dimensions are labeled explicitly.
    d.rect(650,249,490,544,PANEL,LINE)
    d.label(672,282,"Rocket-local height · candidate only",ORANGE)
    d.rect(690,310,160,88,"#3c4a55",LINE,10)
    d.rect(690,410,160,108,"#2b4d58",TEAL,8)
    d.rect(690,522,160,14,ORANGE,rx=2)
    d.rect(690,540,160,108,"#28504b",TEAL,8)
    d.rect(690,660,160,64,"#4e4339",ORANGE,8)
    d.para(876,338,"Nose, equipment and antenna",242,18,INK,True)
    d.para(876,438,"Upper cabin: seat, view, controls and ladder",242,18,INK,True)
    d.para(876,558,"Lower vestibule: outer hatch and inner pressure hatch",242,18,INK,True)
    d.para(876,686,"Propulsion / support",242,18,INK,True)
    d.text(710,460,"Upper cabin",17,INK,True)
    d.text(710,590,"Vestibule",17,INK,True)
    d.text(672,761,"9 high overall; 9.5 above pad origin when mounted",16,MUTED)
    d.note(60,831,1080,145,"A real cabin remains the requirement", "Candidate interior: 2.25 × 2.25 blocks; at least 2.125 clear standing height on each occupied level. Test suit/backpack, ladder, seat, tank housings, cargo, camera and both closures. If they do not fit, revisit the hull scale instead of hiding an expanded interior.")
    d.label(60,1028,"Four typed relationships · color is supplemented by labels")
    lanes=[
        (1054,ORANGE,"FUEL · SUPPLY01","Fuel Refinery","T1-M03 / IND-M45","Fuel Pipe + adapters","T1-M04","Named pad / craft fuel inlet","Real fuel transfer with a stop target."),
        (1240,TEAL,"BREATHING · SUPPLY02","Oxygen Generator","T1-M05 / IND-M40","Compatible store / refill","T1-S01","Breathing tank / cabin / refuge","The supported process needs actual inputs."),
        (1426,GOLD,"POWER / CHARGING","Declared local supply","SOURCE + CAPACITY","Power connection","TYPED ELECTRICAL","Process, service and field-cell loads","Available energy and reserves remain visible."),
        (1612,BLUE,"CONTROL · ORDERS + STATUS","Control Console","T1-M07","Named pad / craft link","LOGICAL ASSOCIATION","Selected operation and next blocker","Control transfers no fuel, air or items."),
    ]
    for y,color,label,t1,g1,t2,g2,t3,body in lanes:
        d.label(60,y,label,color)
        d.card(60,y+20,295,136,t1,"",g1,color)
        d.card(402,y+20,328,136,t2,"",g2,color)
        d.card(777,y+20,363,136,t3,body,size=17,accent=color)
        d.line(362,y+90,395,y+90,color,arrow=True);d.line(737,y+90,770,y+90,color,arrow=True)
    d.note(60,1813,1080,143,"Before departure: protect reserves and clear the craft", "Retract pad couplers; keep the hatch sweep, front approach and departure column clear. Ground fuel and breathing connections reject one another. First lunar arrival and return use carried supplies and onboard controls; a prebuilt lunar pad or refinery is not required.")
    d.footer("Sources: T1-SPEC-05/07/10/11/12/15/16/18 · T1 model register · T1-DEC01.","Editable service schematic · proposed dimensions · top-view ratios only; cabin stack is not a measured floor plan.")
    record(d,"tier1-services.svg","The proportional top view separates nominal body, fin, deck and site extents. The cabin stack is functional, not a measured floor plan. The pad's proposed 3-block maximum service height belongs to raised couplers; its deck remains 0.5 high. Fuel Pipe terminal adapters resolve the recorded grid/socket offsets within the existing pipe family.",["T1-SPEC-05","T1-SPEC-07","T1-SPEC-10","T1-SPEC-11","T1-SPEC-12","T1-SPEC-15","T1-SPEC-16","T1-SPEC-18","T1-DEC01","T1-V01","T1-M03","T1-M04","T1-M05","T1-M06","T1-M07","T1-S01","IND-M40","IND-M45"],["Tier 1 — Model and Implementation Specifications"],10)


def main():
    for build in [first_playable_route,project_states,workshop_chain,industry_network,earth_geography,planetary_expeditions,tier1_assembly,tier1_services]:
        build()
    (HERE / "illustrated_03_diagrams.json").write_text(json.dumps(RECORDS,ensure_ascii=False,indent=2)+"\n")
    (HERE / "illustrated_03_diagram_validation.json").write_text(json.dumps({"xml_valid":True,"text_measured_with":"DejaVu Sans via Pillow","all_text_inside_viewbox":True,"all_card_text_within_bounds":True,"no_external_resources":True,"figures":CHECKS},ensure_ascii=False,indent=2)+"\n")
    print(json.dumps({"figures":len(RECORDS),"output":str(OUT),"svg_bytes":sum(c['bytes'] for c in CHECKS)},indent=2))


if __name__ == "__main__":
    main()
