#!/usr/bin/env python3
"""Build six exact, editable SUBSTELLAR documentation SVGs for edition 05.

No game code or old artwork is modified. Fonts are measured before export;
the six resulting diagrams carry accessible title/description text.
"""
from __future__ import annotations
import json
from pathlib import Path
from xml.sax.saxutils import escape
from xml.etree import ElementTree
from PIL import ImageFont

HERE = Path(__file__).resolve().parent
W = 1200
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=18, bold=False):
    return font(size, bold).getlength(str(s))

def wrap(s, available, size=18, bold=False):
    lines = []
    for para in str(s).split('\n'):
        words = para.split()
        if not words:
            lines.append('')
            continue
        current = words.pop(0)
        for word in words:
            candidate = current + ' ' + word
            if width(candidate, size, bold) > available:
                lines.append(current)
                current = word
            else:
                current = candidate
        lines.append(current)
    return lines

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.bounds = []
        self.card_checks = []
        self.rect(0, 0, W, height, BG, rx=0)
        self.text(54, 46, 'SUBSTELLAR  /  CURRENT DIRECTION 05', 15, TEAL, True)
        for i, line in enumerate(wrap(title, 1092, 33, True)):
            self.text(54, 101 + 40*i, line, 33, INK, True)
        self.para(54, 145, subtitle, 1080, 18)

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

    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 path(self, d, stroke=TEAL, sw=2.4, fill='none', arrow=False, dash=None):
        s=f'<path d="{d}" stroke="{stroke}" stroke-width="{sw}" fill="{fill}" stroke-linejoin="round" stroke-linecap="round"'
        if arrow: s+=f' marker-end="url(#{self.ident}-{stroke[1:]})"'
        if dash: s+=f' stroke-dasharray="{dash}"'
        self.parts.append(s+'/>')

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

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

    def para(self,x,y,s,w,size=18,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 card(self,x,y,w,h,title,body='',tag=None,accent=TEAL,size=17,fill=PANEL,dash=None):
        self.rect(x,y,w,h,fill,accent if dash else LINE,dash=dash)
        cy=y+28
        if tag:
            self.text(x+18,cy,tag,14,accent,True)
            cy+=29
        cy=self.para(x+18,cy,title,w-36,20,INK,True,26)
        if body:
            cy+=8
            cy=self.para(x+18,cy,body,w-36,size)
        self.card_checks.append((cy-(size*1.4 if body else 26)+size*.25<=y+h-12,title,cy,y+h))

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

    def footer(self,source,note='Editable design diagram · proposed relationships; no measured fit or game implementation claimed.'):
        self.line(54,self.height-84,1146,self.height-84,LINE,sw=1)
        self.para(54,self.height-58,source,1092,14,MUTED,leading=19)
        self.text(54,self.height-19,note,13,MUTED)

    def save(self,filename):
        errors=[]
        for x1,y1,x2,y2,s in self.bounds:
            if x1<35 or x2>1165 or y1<18 or y2>self.height-7:
                errors.append({'text':s,'bounds':[x1,y1,x2,y2]})
        for ok,title,end,bottom in self.card_checks:
            if not ok: errors.append({'card':title,'text_end':end,'bottom':bottom})
        if errors: raise ValueError(f'{filename}: {errors}')
        markers=''.join(f'<marker id="{self.ident}-{c[1:]}" 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>' for c in [TEAL,ORANGE,GOLD,BLUE,MUTED,LINE])
        xml=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>{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(xml)
        (HERE/filename).write_text(xml,encoding='utf-8')
        return {'file':filename,'width':1200,'height':self.height,'text_items':len(self.bounds),'card_checks':len(self.card_checks),'xml_valid':True,'bounds_valid':True}

RECORDS=[]
def finish(s,file,source_ids,caption,notes):
    v=s.save(file)
    RECORDS.append({'id':s.ident,'title':s.title,'kind':'editable_svg','file':file,'caption':caption,'alt':s.alt,'source_ids':source_ids,'coverage_note':'Exact authored relationships and labels; the diagram is an editable design artifact.','proposed_notes':notes,'generation_calls':0,'validation':v})

def d01():
    s=SVG('CD05-D01','How the current direction relates to its sources','A topic can have an approved purpose, a visual reference, and a proposed implementation detail at the same time.',1110,'Source relationship diagram: approved direction and confirmed visual references remain preserved; registered expansions and edition 05 worked examples feed a current-topic review, with unresolved decisions recorded separately and all source material available to the website.')
    # Connectors appear behind cards.
    for x in [224,600,976]: s.line(x,390,x,430,TEAL,True)
    s.path('M 224 622 L 224 667 L 600 667 L 600 707',TEAL,arrow=True)
    s.path('M 976 622 L 976 667 L 600 667',TEAL)
    s.line(600,622,600,707,TEAL,True)
    s.path('M 600 847 L 600 879 L 321 879 L 321 910',TEAL,arrow=True)
    s.path('M 600 879 L 880 879 L 880 910',ORANGE,arrow=True)
    s.card(54,205,340,185,'Approved direction','World identity, player agency, authored construction, deep Earth and eventual space travel.','PURPOSE AND COMMITMENTS')
    s.card(430,205,340,185,'Confirmed references','Appearance and named functions remain intact. Compare example dimensions before final fit review.','VISUAL EVIDENCE')
    s.card(806,205,340,185,'Saved source history','Original prose, later expansions, diagrams and generated concepts remain accessible.','PRESERVED RECORD')
    s.card(54,430,340,192,'Registered expansions','First Playable, industrial functions, Moon/Mars plans and Tier 1 specifications explain the shared direction.','SOURCE IDS AND CROSS-REFERENCES')
    s.card(430,430,340,192,'Worked examples 05','Dependency chains, occupied spaces and the first lunar expedition test the plan on paper.','PROPOSED DETAIL')
    s.card(806,430,340,192,'Artwork roles stay clear','Confirmed references, generated atmosphere and exact diagrams answer different review questions.','READ WITH THEIR CAPTIONS')
    s.card(240,707,720,140,'Current topic account','Bring the relevant sources together. State the current answer, preserved variants, proposed defaults and any consequential unresolved choice.','CONSOLIDATED READING',size=18)
    s.card(54,910,534,110,'Website reading and review','Use current-topic links while retaining the complete source material.',tag=None,size=17)
    s.card(626,910,520,110,'Decision review','Unresolved fit and design choices stay visible until deliberately resolved.',tag=None,accent=ORANGE,size=17)
    s.footer('Sources: original direction; reference catalogue; First Playable, industry, destination and Tier 1 registers; edition 05 review.')
    finish(s,'cd05-d01-source-relationships.svg',['SET01','DST01','T1-DEC01','T1-DEC02','FP-PLAN-03','MO-PLAN-06'],'The current account groups the applicable sources for each topic while preserving artwork roles, older variants and unresolved decisions.',['This is a review structure, not an automatic precedence rule that turns later proposals into approved canon.'])

def d02():
    s=SVG('CD05-D02','From the powered workshop to the first lunar return','The four preparation lanes meet at mission readiness; useful lunar products arrive after a feasible Earth-supplied expedition.',1320,'Dependency diagram from existing workshop and Earth stock to power, four parallel preparation lanes for parts, refuge and suit, propellant and breathing supply, then mission readiness, a first lunar sortie, returned feedstock and useful material products. A dashed revisit loop improves workshop options without becoming a prerequisite for the first launch.')
    s.line(394,282,430,282,TEAL,True)
    s.line(770,282,806,282,TEAL,True)
    s.path('M 600 390 L 600 410 L 1021 410 L 1021 455',TEAL,arrow=True)
    s.line(976,390,976,410,TEAL)
    for x in [178,459,740]: s.path(f'M 600 410 L {x} 410 L {x} 455',TEAL,arrow=True)
    for x in [178,459,740,1021]: s.path(f'M {x} 688 L {x} 731 L 600 731',TEAL)
    s.line(600,731,600,775,TEAL,True)
    s.path('M 600 920 L 600 965 L 224 965 L 224 1005',TEAL,arrow=True)
    s.line(394,1088,430,1088,TEAL,True)
    s.line(770,1088,806,1088,TEAL,True)
    s.path('M 976 1190 L 976 1225 L 26 1225 L 26 288 L 54 288',BLUE,arrow=True,dash='7 6')
    s.card(54,205,340,185,'Workshop foundation','Existing bench, hearth, shaping and useful cargo service; this follows the First Playable opening.','WP05-01 / EXISTING IP01')
    s.card(430,205,340,185,'Reachable Earth stock','Common metals and supported structural, seal, glass and circuit preparation are available before launch.','WP05-02')
    s.card(806,205,340,185,'Modest site power','Dynamo, distribution and charging. Supply and workspace grow with actual installed capability.','WP05-03 / IND-M32, 37–39')
    columns=[(54,'Vehicle assembly','Prepared components enter T1-M01. T1-M02 commissions one Explorer at the separate linked berth.','WP05-04 / T1-M01, M02'),(335,'Suit and refuge','Earth-made seals, equipment and a complete starter cell/airlock kit establish a feasible first foothold.','WP05-05 / TEC04, TEC12'),(616,'Propellant','The fuel route supplies SUPPLY01 from the supported Earth preparation chain. Return stock is reserved.','WP05-06 / T1-M03'),(897,'Breathing supply','The independent SUPPLY02 route serves the craft, field equipment and supported refuge.','WP05-07 / T1-M05')]
    for x,title,body,tag in columns: s.card(x,455,249,233,title,body,tag,accent=GOLD,size=17)
    s.card(240,775,720,145,'Allocate, load and commission','Verify craft, actual supplies, protected return allocation, carried refuge kit, usable cargo, boarding and an accessible landing/return plan.','WP05-08 / T1-M06, T1-M07',size=18)
    s.card(54,1005,340,185,'First lunar sortie','Land, inspect, commission the refuge, reach the first gallery, then return along the checked route.','LE05 / MO-EXP-01')
    s.card(430,1005,340,185,'Return selected samples','Small regolith and glass-bearing loads are enough for the first useful batch. A lunar factory is optional.','WP05-11 / MO-PLAN-05')
    s.card(806,1005,340,185,'Apply the returned batch','PM01 ceramic components and PM02 light-guide stock add useful habitat, lamp and instrument options.','WP05-12 / PM01, PM02')
    s.footer('Sources: WP05-01–08, WP05-11–12; IND-M32/37/38/39; T1-M01–M07; MO-PLAN-05; MO-EXP-01.', 'Dashed loop = future utility and revisits. Lunar products are not required to make the first Earth-supplied trip.')
    finish(s,'cd05-d02-workshop-lunar-loop.svg',['WP05-01','WP05-02','WP05-03','WP05-04','WP05-05','WP05-06','WP05-07','WP05-08','WP05-11','WP05-12','MO-EXP-01','IND-M04','IND-M32','IND-M37','IND-M38','IND-M39','T1-M01','T1-M02','T1-M03','T1-M05','T1-M06','T1-M07','PM01','PM02'],'Four independently prepared capabilities meet at commissioning. Returned lunar material expands useful options after the first complete expedition.',['Functional dependencies only; numerical recipes, pacing and production scale remain proposed.','An installed regional forming press improves capacity; simple shaping remains available earlier.'])

def d03():
    s=SVG('CD05-D03','Mission stock, protected reserves and cargo','Keep propellant, breathing supply, electricity and usable cargo as separate accounts. Symbols below describe planning relationships.',1380,'Symbolic mission accounting diagram. Physical stores transfer actual stock to vehicle, cylinders or refuge; reservation assigns priority without creating more stock. Propellant and breathing allocations protect return and emergency quantities. Electrical priorities protect essential services before optional work. Outbound and return cargo must each fit their own usable capacity.')
    s.card(54,205,1092,118,'Reserve an allocation; transfer actual stock','A source reservoir loses exactly what the receiving vehicle, cylinder or refuge gains. Protected allocations remain part of the same total and ordinary production cannot spend them.',None,size=18)
    s.card(54,360,534,345,'Propellant / SUPPLY01','P_total = P_out + P_return\n                  + P_contingency + P_optional','WP05-06',size=20)
    s.card(626,360,520,345,'Breathing / SUPPLY02','B_total = B_cabin_out + B_field\n                  + B_refuge + B_return\n                  + B_emergency','WP05-07',size=20)
    s.card(74,520,232,148,'Return protected','Reserved for the journey home; not spare factory fuel.',accent=ORANGE,size=17)
    s.card(327,520,241,148,'Optional use','Permitted only beyond outbound, return and contingency needs.',accent=BLUE,size=17)
    s.card(646,548,232,130,'Occupied loads','Field: all sorties.\nRefuge: active +\nretained stock.',accent=BLUE,size=17)
    s.card(898,548,228,130,'Protected stock','Return and emergency\nstock stay protected.',accent=ORANGE,size=17)
    s.label(54,748,'ELECTRICITY IS A SEPARATE SERVICE',GOLD)
    s.line(302,833,335,833,GOLD,True)
    s.line(583,833,616,833,GOLD,True)
    s.line(864,833,897,833,GOLD,True)
    s.card(54,775,248,120,'Available E_total','Actual compatible power available.',accent=GOLD,size=17)
    s.card(335,775,248,120,'Essential first','Craft and occupied refuge services.',accent=GOLD,size=17)
    s.card(616,775,248,120,'Mission tools','Planned charging and field work demand.',accent=GOLD,size=17)
    s.card(897,775,249,120,'Optional work','Use only the remaining budget.',accent=GOLD,size=17)
    s.label(54,948,'CHECK CARGO IN EACH DIRECTION',BLUE)
    s.card(54,975,534,238,'Outbound loading','Refuge kit + carried equipment + packaged supplies + containers must fit usable outbound cargo. Fixed tanks are accounted for in vehicle fit; do not count their contents twice.','WP05-08 / CAPACITY CHECK',accent=BLUE,size=18)
    s.card(626,975,520,238,'Return loading','Required carried stock + retained gear + chosen samples must fit usable return cargo. Taking a smaller sample is a valid successful expedition outcome.','WP05-11 / CAPACITY CHECK',accent=BLUE,size=18)
    s.para(54,1251,'Exact capacity, packed volume, consumption and warning thresholds await fit and playtesting. Category boxes have no quantity scale.',1090,17)
    s.footer('Sources: WP05-06–08, WP05-11; SUPPLY01/02/03; T1 shared ownership/reserve rules; MO-PLAN-04–06.', 'Allocation does not create stock. Parked managed regions preserve recorded reserves without unattended catch-up consumption.')
    finish(s,'cd05-d03-mission-reserves.svg',['WP05-06','WP05-07','WP05-08','WP05-11','SUPPLY01','SUPPLY02','SUPPLY03','T1-V01','T1-S02','MO-PLAN-04','MO-PLAN-05','MO-PLAN-06'],'Independent symbolic accounts protect the return journey while keeping physical transfers and both cargo manifests coherent.',['Symbols are explanatory variables, not new inventory items, units, recipes or finalized capacity values.','Actual craft/refuge volumes and thresholds remain subject to measured fit and playtesting.'])

def d04():
    s=SVG('CD05-D04','Two useful return points, two different boundaries','Conceptual floor-plan relationships for review. Shapes show adjacency and circulation; they are not measured construction footprints.',1340,'Side-by-side conceptual plans. Old Valley repairs an existing guesthouse entrance, retains its bed, separates personal storage from shared storage and restores common access. The lunar refuge has a protected cell, real supply and power services, inner closure, airlock and outer closure. A dashed dust porch outside is an optional later extension. Clear access is maintained in both plans.')
    s.label(54,223,'OLD VALLEY / EXISTING GUESTHOUSE')
    s.label(626,223,'MOON / STARTER REFUGE')
    # Existing guesthouse envelope, with entrance gap.
    s.rect(54,254,534,642,PANEL,LINE)
    s.path('M 84 829 L 84 284 L 558 284 L 558 829 L 365 829 M 276 829 L 84 829',MUTED,sw=6)
    s.rect(110,315,150,105,PANEL2,TEAL,rx=2)
    s.rect(116,321,138,27,INK,rx=2)
    s.rect(116,355,138,59,'#43616c',rx=2)
    s.text(110,451,'Retained bed',19,INK,True)
    s.para(110,479,'Repair the furnishing\nalready at the site.',165,16)
    s.rect(400,315,125,105,'#30404a',TEAL,rx=3)
    s.line(400,350,525,350,TEAL,sw=2)
    s.rect(456,344,13,20,GOLD,rx=2)
    s.text(397,451,'Personal store',17,INK,True)
    s.para(397,479,'Assign or claim\nits use clearly.',135,16)
    s.rect(110,611,155,94,PANEL2,GOLD,rx=2)
    s.rect(395,611,132,94,PANEL2,GOLD,rx=2)
    s.para(110,641,'Ordinary\ncrafting',141,19,INK,True,26)
    s.para(405,641,'Shared\nstorage',111,19,INK,True,26)
    s.text(111,745,'Modest common corner',20,INK,True)
    s.path('M 320 847 L 320 762 M 320 715 L 320 545',TEAL,arrow=True)
    s.text(336,804,'Clear route',16,TEAL)
    s.text(110,874,'Repaired entrance and approach',18,INK,True)
    # Refuge cell and separate vestibule. Exterior extension is dashed.
    s.rect(626,254,520,642,PANEL,LINE)
    s.path('M 656 818 L 656 284 L 1116 284 L 1116 818 L 929 818 M 843 818 L 656 818',MUTED,sw=6)
    s.rect(682,317,147,103,PANEL2,TEAL,rx=2)
    s.rect(688,323,135,25,INK,rx=2)
    s.rect(688,355,135,57,'#5a5244',rx=2)
    s.text(682,450,'Sleeping cell',19,INK,True)
    s.rect(969,318,122,102,PANEL2,TEAL,rx=2)
    s.para(981,350,'Clean\nstorage',103,18,INK,True,25)
    s.para(682,496,'Protected service area',386,21,INK,True)
    s.text(682,526,'Only commissioned volumes are supported.',16,MUTED)
    s.circle(656,563,8,BLUE)
    s.text(679,569,'Breathing input',16,BLUE,True)
    s.circle(1116,563,8,GOLD)
    s.text(1092,569,'Power input',16,GOLD,True,'end')
    s.line(656,609,843,609,ORANGE,sw=5)
    s.line(929,609,1116,609,ORANGE,sw=5)
    s.line(843,609,929,609,ORANGE,sw=7)
    s.text(685,642,'Inner closure',16,ORANGE,True)
    s.text(685,720,'Airlock',23,INK,True)
    s.text(685,748,'Two closures, clear circulation',16,MUTED)
    s.line(843,818,929,818,ORANGE,sw=7)
    s.text(683,859,'Outer closure',16,ORANGE,True)
    s.path('M 887 895 L 887 770 M 887 688 L 887 585',TEAL,arrow=True,dash='6 6')
    s.rect(626,922,520,114,PANEL2,BLUE,dash='7 6')
    s.text(647,953,'Optional later dust porch and stores',20,INK,True)
    s.para(647,983,'Dirty-side equipment service outside the clean cell.\nUseful after commissioning; not a first-descent gate.',477,17)
    s.path('M 887 922 L 887 899',BLUE,arrow=True)
    s.card(54,922,534,114,'Two essential repair stages','1. Enclosure and approach.\n2. Room, storage assignment and common access.',accent=TEAL,size=17)
    s.card(54,1072,534,151,'Old Valley completion','Usable entrance, retained bed, clear storage and common access. Bed/spawn behavior still requires an actual game check.',tag=None,size=18)
    s.card(626,1072,520,151,'Lunar commissioning','Complete airlock and protected sleeping/service cell with real power and breathing stock. Explorer remains refuge during construction.',tag=None,size=18)
    s.footer('Sources: FP-PLAN-03; FP-C01; SET01; T1-S02 / T1-SPEC-15; MO-PLAN-06; REF03.', 'Illustrative adjacency only. Proposed 7 × 9 lunar envelope, precise walls, closure volumes and player fit remain unverified.')
    finish(s,'cd05-d04-return-point-plans.svg',['SET01','FP-PLAN-03','FP-C01','T1-S02','T1-SPEC-15','MO-PLAN-06','REF03'],'The guesthouse repairs an existing room; the lunar refuge commissions a defined supported cell and airlock. Both provide a clear return route, storage and an occupied place to rest.',['Plan shapes are conceptual and have no construction scale.','The optional dust porch/stores are a later convenience stage, not a requirement before the first descent.','The proposed lunar 7 × 9 footprint and all measured access remain pending fit.'])

def d05():
    s=SVG('CD05-D05','Tier 1: the two production-form decisions','Preserve all supplied references. These proposed envelopes support a fit review; they do not settle the final model geometry.',1370,'Comparison diagram for pending Tier 1 decisions. Explorer source sheets carry approximately six-, nine-, and twelve-block scale cues; the current test candidate is nine blocks total with a three-by-three body and five-by-five fin envelope. A separate comparison gives the Part Fabrication Table a proposed two-by-two footprint and Rocket Assembly Station three-by-three footprint, controlling a distinct vehicle berth.')
    s.card(54,205,1092,95,'T1-DEC01 / Explorer scale and usable cabin','Current recommendation: test the nine-block candidate, and enlarge deliberately if the complete occupied fit fails.',accent=ORANGE,size=18)
    s.rect(54,335,534,530,PANEL,LINE)
    s.text(78,372,'Candidate envelope sketch',22,INK,True)
    # 9 equal height intervals; a bounding envelope, not invented final geometry.
    x0,y0,unit=269,410,34
    s.rect(x0-34,y0,170,306,'none',BLUE,rx=0,dash='6 5')
    for i in range(10): s.line(x0-34,y0+i*unit,x0+136,y0+i*unit,LINE,sw=1)
    s.path(f'M {x0+51} {y0} L {x0+51} {y0+34} L {x0+17} {y0+34} L {x0+17} {y0+68} L {x0} {y0+68} L {x0} {y0+255} L {x0-34} {y0+255} L {x0-34} {y0+306} L {x0+136} {y0+306} L {x0+136} {y0+255} L {x0+102} {y0+255} L {x0+102} {y0+68} L {x0+85} {y0+68} L {x0+85} {y0+34} L {x0+51} {y0+34}',TEAL,sw=3,fill=PANEL2)
    s.rect(x0+18,y0+96,66,153,'none',GOLD,rx=0,dash='4 4')
    s.line(202,y0,202,y0+306,BLUE,sw=2)
    s.line(192,y0,212,y0,BLUE,sw=2)
    s.line(192,y0+306,212,y0+306,BLUE,sw=2)
    s.para(78,540,'9 blocks\noverall',113,20,BLUE,True,27)
    s.text(79,761,'Body candidate: 3 × 3',19,INK,True)
    s.text(79,792,'Fin envelope: 5 × 5',19,INK,True)
    s.text(79,832,'Dashed cabin zone is an unverified fit hypothesis.',16,MUTED)
    s.rect(626,335,520,530,PANEL,LINE)
    s.text(650,372,'Reference scale evidence',22,INK,True)
    rows=[('REF02','3 × 3 × 9 example'),('REF08 / REF14','Approximately 9 blocks'),('REF11','Approximately 12 blocks'),('REF12','Approximately 6 blocks')]
    for i,(left,right) in enumerate(rows):
        y=423+i*51
        s.text(650,y,left,18,TEAL,True)
        s.text(1117,y,right,18,INK,False,'end')
        s.line(650,y+15,1120,y+15,LINE,sw=1)
    s.para(650,657,'A complete fit review must include:',472,19,INK,True)
    s.para(650,693,'Suited player and backpack; both closure boundaries; ladder, seat and camera; cargo and tank housings; hatch sweep and service connections.',472,18)
    s.para(650,804,'The reference differences remain visible until the candidate is measured and reviewed.',472,17,ORANGE)
    s.label(54,913,'T1-DEC02 / PREPARED PARTS AND VEHICLE ASSEMBLY')
    s.line(394,1089,430,1089,TEAL,True)
    s.line(770,1089,806,1089,TEAL,True)
    s.card(54,941,340,257,'Part Fabrication Table','Prepares and checks components. Proposed 2 × 2 footprint and 2-block height; a lower work surface with reachable stock access.','T1-M01 / REF10',size=18)
    s.card(430,941,340,257,'Rocket Assembly Station','Receives prepared parts and controls authored vehicle construction. Proposed 3 × 3 footprint and 2-block height.','T1-M02 / REF09',size=18)
    s.card(806,941,340,257,'Separate linked berth','The full Explorer is erected and commissioned at the supported vehicle berth. It does not stand on either station worktop.','ONE VEHICLE / EXISTING ROLE',size=18)
    s.para(54,1241,'Review before final production art: T1-DEC01 follows measured craft/player fit; T1-DEC02 resolves the station appearance while retaining both useful roles.',1088,18,ORANGE)
    s.footer('Sources: T1-DEC01/02; T1-V01; T1-M01/02; REF02/08/09/10/11/12/14.', 'All envelopes are proposed. Source images retain their original numbers; no reference has been rewritten to imply consensus.')
    finish(s,'cd05-d05-tier1-decisions.svg',['T1-DEC01','T1-DEC02','T1-V01','T1-M01','T1-M02','REF02','REF08','REF09','REF10','REF11','REF12','REF14'],'Review the Explorer candidate against preserved scale evidence, then compare the two stations by their distinct jobs and proposed working envelopes.',['Nine-block rocket height, 3 × 3 body, 5 × 5 fins and station sizes are current proposed defaults.','The schematic cabin zone is not a validated usable cabin.','T1-DEC01 and T1-DEC02 remain pending final model review.'])

def d06():
    s=SVG('CD05-D06','The first tube and the view home','A complete first lunar outing has a small objective, a useful return, and a deliberate opportunity to stop while it is still successful.',1510,'Route and branch diagram for MO-EXP-01. Land at Earthrise Rim and check the cabin; make a small surface inspection and return; commission a protected refuge and airlock before the marked descent through Regolith Hollow. Reach the accessible Basalt Gallery, sample glass-bearing material and record Earth through a light shaft. Choose return or an optional short side survey, then use the same verified route back to the refuge and Explorer for the Earth return. Shadow Basin and Glassfall are later journeys.')
    s.line(394,297,430,297,TEAL,True)
    s.line(770,297,806,297,TEAL,True)
    s.line(976,390,976,455,TEAL,True)
    s.line(806,544,770,544,TEAL,True)
    s.line(430,544,394,544,TEAL,True)
    s.path('M 224 640 L 224 684 L 321 684 L 321 735',TEAL,arrow=True)
    s.path('M 224 684 L 886 684 L 886 735',BLUE,arrow=True)
    s.path('M 321 930 L 321 972 L 600 972 L 600 1015',TEAL,arrow=True)
    s.path('M 886 930 L 886 972 L 600 972',BLUE)
    s.line(600,1175,600,1190,TEAL,True)
    s.card(54,205,340,185,'Land and check the cabin','Earthrise Rim arrival. Verify craft, carried stock, protected return allocation and the homeward approach.','01 / EARTHRISE RIM')
    s.card(430,205,340,185,'Inspect a small surface loop','Mark the path, assess the nearby refuge plot and shallow entrance, then return to the craft.','02 / RETURN BEFORE DESCENT')
    s.card(806,205,340,185,'Commission the refuge','Complete the protected cell, both airlock closures and actual power/breathing service.','03 / T1-S02')
    s.card(806,455,340,185,'Enter Regolith Hollow','Use a marked safe route, braced sampling position and supported short tethered descent.','04 / MO-L1')
    s.card(430,455,340,185,'Reach the gallery overlook','Take accessible glass-bearing feedstock and record Earth through the light shaft.','05 / MO-L2')
    s.card(54,455,340,185,'The useful objective is done','The map, small sample and observation already justify a successful journey home.','06 / CHOOSE YOUR NEXT STEP')
    s.card(54,735,534,195,'Return now','Use the verified route while the reserve remains comfortable. Reaching home with the small objective is complete success.','07A / RECOMMENDED FIRST OUTING',accent=TEAL,size=19)
    s.card(626,735,520,195,'Short optional side survey','Continue only inside the checked equipment and reserve envelope. Turn back at the stated limit; record progress without forcing completion.','07B / OPTIONAL EXTENSION',accent=BLUE,size=19,dash='7 6')
    s.card(240,1015,720,160,'Return to the supported refuge','Retrace the same verified route. Service equipment, store and manifest selected samples. An optional later dust porch makes this more convenient.','08 / ROUTE → REFUGE',size=18)
    s.card(240,1190,720,145,'Return to Earth with a useful batch','Check the Explorer and return allocation, bring the selected load home, then make the first ceramic or light-guide products.','09 / REFUGE → EXPLORER → EARTH',size=18)
    s.para(54,1375,'Later journeys: Shadow Basin and A Light Across the Basin; Glassfall Vaults and deeper specimen work. None is required to finish this first trip.',1080,17,BLUE)
    s.footer('Sources: LE05-01–12; MO-EXP-01; MO-PLAN-04/05/06/08; T1-S02; WP05-08/11/12.', 'This is a capability and decision route, not an exact generated map. Turnaround rules and safe return remain explicit.')
    finish(s,'cd05-d06-first-lunar-route.svg',['LE05-01','LE05-02','LE05-03','LE05-04','LE05-05','LE05-06','LE05-07','LE05-08','LE05-09','LE05-10','LE05-11','LE05-12','MO-EXP-01','MO-PLAN-04','MO-PLAN-05','MO-PLAN-06','MO-PLAN-08','T1-S02','WP05-08','WP05-11','WP05-12'],'The first lunar objective is complete at the accessible Basalt Gallery. Returning then is a full success; a short optional survey remains bounded by the checked equipment, route and reserves.',['The route is an authored relationship diagram and does not establish measured terrain or exact map placement.','The commissioned airlock/sleeping cell precedes committed descent. Dust porch/stores are optional later conveniences.','Shadow Basin and Glassfall remain subsequent journeys.'])

if __name__=='__main__':
    for build in [d01,d02,d03,d04,d05,d06]: build()
    (HERE/'diagrams_05.json').write_text(json.dumps({'edition':'05','records':RECORDS,'count':len(RECORDS)},indent=2,ensure_ascii=False)+'\n',encoding='utf-8')
    print(json.dumps({'count':len(RECORDS),'files':[r['file'] for r in RECORDS],'bounds':'pass','XML':'pass'},indent=2))
