#!/usr/bin/env python3
"""Build four editable documentation SVGs for SUBSTELLAR UX01–UX12.

These are illustrative interface layouts, not mod UI or implementation code.
Only new output files are written. Text and quantities are drawn from the
approved creative reference and the First Playable / Tier 1 planning registers.
"""
from __future__ import annotations

import argparse
import html
import json
import re
from pathlib import Path
from xml.etree import ElementTree as ET

from PIL import ImageFont

ROOT = Path(__file__).resolve().parent
W, H = 1200, 1736
C = {
    'navy': '#102C40', 'ink': '#183345', 'muted': '#536775',
    'cream': '#F6F2E9', 'white': '#FFFEFA', 'line': '#CDD6D7',
    'teal': '#176F6D', 'tealbg': '#DFEFEB', 'orange': '#A95018',
    'orangebg': '#FCE6D2', 'blue': '#277D9B', 'bluebg': '#E2EFF4',
    'gray': '#DCE2DF', 'pale': '#F0F3F1', 'danger': '#A34835',
}
FONT = '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf'
BOLD = '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf'


def e(s):
    return html.escape(str(s), quote=True)


class Sheet:
    def __init__(self, sheet_id, title, subtitle, desc):
        self.id = sheet_id
        self.parts = [
            f'<svg xmlns="http://www.w3.org/2000/svg" width="{W}" height="{H}" viewBox="0 0 {W} {H}" role="img" aria-labelledby="title desc">',
            f'<title id="title">{e(title)} — illustrative interface layouts</title>',
            f'<desc id="desc">{e(desc)}</desc>',
            '<defs><pattern id="ghost" width="12" height="12" patternUnits="userSpaceOnUse"><rect width="12" height="12" fill="#E2EFF4"/><path d="M-3 3L3-3M0 12L12 0M9 15L15 9" stroke="#9DC1D0" stroke-width="1.5"/></pattern><marker id="arrow" markerWidth="8" markerHeight="8" refX="6.5" refY="4" orient="auto"><path d="M1 1L7 4L1 7" fill="none" stroke="#176F6D" stroke-width="1.6"/></marker></defs>',
            '<style>text{font-family:"DejaVu Sans",Arial,sans-serif} .map-line{fill:none;stroke-linecap:round;stroke-linejoin:round}</style>',
        ]
        self.rect(0, 0, W, H, C['cream'])
        self.rect(0, 0, W, 12, C['navy'])
        self.text(32, 43, f'SUBSTELLAR  /  DOCUMENT INTERFACES  /  {sheet_id}', 14, 700, C['teal'])
        self.text(32, 84, title, 33, 700, maxw=1136)
        self.text(32, 116, subtitle, 17, color=C['muted'], maxw=1136)
        self.rect(32, 135, 1136, 36, C['navy'], 7)
        self.text(48, 159, 'ILLUSTRATIVE INTERFACE LAYOUT  ·  Proposed information hierarchy and example states; not a game screenshot.', 14, 700, C['white'])

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

    def line(self, x1, y1, x2, y2, color=None, sw=1, dash=None, arrow=False):
        a = f' x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{color or C["line"]}" stroke-width="{sw}"'
        if dash: a += f' stroke-dasharray="{dash}"'
        if arrow: a += ' marker-end="url(#arrow)"'
        self.parts.append('<line'+a+'/>')

    def path(self, d, color=None, sw=3, dash=None, fill='none', arrow=False):
        a = f' d="{d}" fill="{fill}" stroke="{color or C["teal"]}" stroke-width="{sw}" class="map-line"'
        if dash: a += f' stroke-dasharray="{dash}"'
        if arrow: a += ' marker-end="url(#arrow)"'
        self.parts.append('<path'+a+'/>')

    def circle(self, cx, cy, r, fill, stroke=None, sw=1, dash=None):
        a = f' cx="{cx}" cy="{cy}" r="{r}" fill="{fill}"'
        if stroke: a += f' stroke="{stroke}" stroke-width="{sw}"'
        if dash: a += f' stroke-dasharray="{dash}"'
        self.parts.append('<circle'+a+'/>')

    def text(self, x, y, text, size=17, weight=400, color=None, maxw=None, leading=None, anchor='start'):
        """Wrap text by measured font width; return the baseline after the block."""
        font = ImageFont.truetype(BOLD if weight >= 600 else FONT, size)
        lines = []
        for para in str(text).split('\n'):
            if not maxw:
                lines.append(para)
                continue
            current = ''
            for word in para.split():
                trial = word if not current else current + ' ' + word
                if current and font.getlength(trial) > maxw:
                    lines.append(current)
                    current = word
                else:
                    current = trial
            lines.append(current)
        lead = leading or round(size * 1.42)
        self.parts.append(f'<text x="{x}" y="{y}" fill="{color or C["ink"]}" font-size="{size}" font-weight="{weight}" text-anchor="{anchor}">')
        for i, line in enumerate(lines):
            self.parts.append(f'<tspan x="{x}" dy="{0 if i == 0 else lead}">{e(line)}</tspan>')
        self.parts.append('</text>')
        return y + lead * len(lines)

    def label(self, x, y, text):
        return self.text(x, y, text.upper(), 12, 700, C['muted'])

    def pill(self, x, y, label, kind='teal', width=None):
        font = ImageFont.truetype(BOLD, 13)
        width = width or int(font.getlength(label)) + 24
        self.rect(x, y, width, 27, C[kind+'bg'], 13)
        self.text(x+12, y+18.5, label, 13, 700, C[kind])

    def button(self, x, y, label, width=None, secondary=False, disabled=False):
        font = ImageFont.truetype(BOLD, 15)
        width = width or int(font.getlength(label)) + 28
        fill = C['pale'] if disabled else C['white'] if secondary else C['teal']
        col = C['muted'] if disabled else C['teal'] if secondary else C['white']
        self.rect(x, y, width, 37, fill, 6, C['line'] if disabled or secondary else None)
        self.text(x+14, y+24, label, 15, 700, col)

    def panel(self, y, ux, purpose, title, state, kind='teal', footer='', split=True):
        self.rect(32, y, 1136, 470, C['white'], 12, C['line'])
        self.text(56, y+25, f'{ux}  /  {purpose.upper()}', 12, 700, C['teal'])
        self.text(56, y+55, title, 23, 700, maxw=785)
        self.pill(945, y+27, state, kind, 199)
        self.line(56, y+74, 1144, y+74)
        if split: self.line(790, y+94, 790, y+390)
        self.rect(33, y+410, 1134, 59, C['pale'], 0)
        self.line(56, y+410, 1144, y+410)
        self.text(56, y+434, footer, 14, color=C['muted'], maxw=1085, leading=19)
        return y+101

    def pair(self, x, y, label, value, width=326):
        self.label(x, y, label)
        return self.text(x, y+25, value, 17, 600, maxw=width)+13

    def check(self, x, y, title, detail, good=True, width=680):
        self.rect(x, y-15, 24, 24, C['tealbg'] if good else C['orangebg'], 5)
        self.text(x+12, y+2, '✓' if good else '!', 16, 700, C['teal'] if good else C['orange'], anchor='middle')
        self.text(x+36, y+2, title, 16, 700, maxw=width-40)
        if detail: self.text(x+36, y+26, detail, 14, color=C['muted'], maxw=width-40)

    def finish(self, source_line):
        self.line(32, 1672, 1168, 1672)
        self.text(32, 1697, source_line, 12, color=C['muted'], maxw=1136, leading=18)
        self.text(1168, 1720, self.id, 12, 700, C['teal'], anchor='end')
        self.parts.append('</svg>')
        document='\n'.join(self.parts)
        # Safe both as an external image and when all four SVGs are inlined.
        for name in ['title','desc','ghost','arrow']:
            document=document.replace(f'id="{name}"',f'id="{self.id}-{name}"')
            document=document.replace(f'url(#{name})',f'url(#{self.id}-{name})')
        document=document.replace('aria-labelledby="title desc"',f'aria-labelledby="{self.id}-title {self.id}-desc"')
        return document


def construction():
    s = Sheet('ILL-U01', 'Construction, without ambiguity',
              'Purpose, placement, material custody, and the useful result.',
              'Three interface studies: a guesthouse controller missing two iron ingots; a crossing preview blocked by a protected chest; and a paused guesthouse stage with exact example material custody. Proposed stage quantities from the First Playable register. No actual game interface is represented.')
    y = 186
    b = s.panel(y, 'UX01', 'Project controller', 'Guesthouse · essential repair', 'AWAITING INPUTS', 'orange',
        'Alternate state: footprint blocked → locate the marked obstruction. Supplied stock stays in project custody.')
    s.text(56, b, 'Make a sleeping place, assigned storage, and common access usable.', 17, maxw=708)
    s.label(56, b+55, 'Current stage')
    s.text(56, b+81, '2 of 2 · Room, storage, and common access', 19, 700)
    ty=b+106
    s.rect(56,ty,710,33,C['navy'],5)
    for x,t in [(70,'Stage input'),(365,'Available in project'),(593,'Still required')]: s.text(x,ty+22,t,13,700,C['white'])
    for i,(mat,avail,need) in enumerate([('Planks · 8 needed','8','0'),('Iron ingots · 2 needed','0','2')]):
        ry=ty+34+i*40
        s.rect(56,ry,710,40,C['pale'] if i%2==0 else C['white'])
        s.text(70,ry+26,mat,16)
        s.text(420,ry+26,avail,16,700)
        s.text(624,ry+26,need,16,700,C['orange'] if need!='0' else C['teal'])
    s.text(56,ty+136,'Stage 1 is complete: 16 planks + 12 cobblestone remain built into the site.',14,color=C['muted'],maxw=710)
    sy=s.pair(814,b,'Next action','Contribute 2 iron ingots')
    s.button(814,sy,'Contribute materials',width=310)
    s.button(814,sy+49,'Start stage · unavailable',width=310,disabled=True)
    s.pair(814,sy+116,'Complete project bill','24 planks · 12 cobblestone · 2 iron ingots')

    y=680
    b=s.panel(y,'UX02','Project footprint preview','Old crossing · deck and approach','BLOCKED','orange',
        'When clear: the next stage can become Ready. Recheck before building; a preview never supports a player.')
    s.label(56,b,'Schematic top view · proposed footprint, not a measured site')
    mx,my=56,b+19
    s.rect(mx,my,710,236,C['pale'],7)
    s.rect(mx+210,my,230,236,C['bluebg'])
    for xx in (mx+260,mx+335,mx+410): s.path(f'M{xx} {my+9}q-20 36 0 65t0 75t0 72','#8CB5C6',2)
    s.rect(mx+14,my+67,180,88,C['gray'],4)
    s.rect(mx+470,my+67,226,88,C['gray'],4)
    s.rect(mx+164,my+67,325,88,'url(#ghost)',4,C['blue'],2)
    for xx in [mx+187,mx+457]: s.rect(xx,my+53,25,116,'#BDC6C0',2,C['muted'])
    s.line(mx+33,my+111,mx+178,my+111,C['teal'],5,arrow=True)
    s.line(mx+506,my+111,mx+657,my+111,C['teal'],5,arrow=True)
    s.rect(mx+316,my+91,40,40,C['orangebg'],4,C['orange'],3)
    s.line(mx+322,my+97,mx+350,my+125,C['orange'],3)
    s.line(mx+350,my+97,mx+322,my+125,C['orange'],3)
    s.line(mx+336,my+90,mx+375,my+33,C['orange'],2)
    s.text(mx+383,my+32,'Protected chest',14,700,C['orange'])
    s.text(mx+16,my+48,'Near-bank access',14,700)
    s.text(mx+505,my+48,'Far-bank access',14,700)
    s.text(mx+222,my+188,'Three-block walking deck proposal',13,700,C['blue'])
    for x,col,t in [(56,C['gray'],'Existing'),(234,C['bluebg'],'Proposed repair'),(459,C['orangebg'],'Conflict'),(620,C['tealbg'],'Access')]:
        s.rect(x,b+271,17,17,col,2,C['line']);s.text(x+25,b+285,t,13)
    sy=s.pair(814,b,'Needs attention','Move the chest at the marked deck position.')
    s.button(814,sy,'Locate obstruction',width=310)
    sy=s.pair(814,sy+72,'Terrain changes','Only the approved repair area. Unsupported terrain blocks work.')
    s.text(814,sy,'Player possessions are not deleted or relocated by construction.',14,700,C['orange'],maxw=310)

    y=1174
    b=s.panel(y,'UX03','Construction stages','Guesthouse · stage history and stock','PAUSED','blue',
        'Completed result: usable sleeping place, assigned storage, and common access. Built stages are not refunded as loose materials.')
    s.text(56,b,'Stage 1 · Complete     →     Stage 2 · Stock ready; not started',17,700)
    s.text(56,b+31,'Example snapshot: no eligible participant is in the managed region.',14,color=C['muted'])
    ty=b+51
    s.rect(56,ty,710,48,C['navy'],5)
    headings=[(70,'Stock'),(250,'Available'),(371,'Committed'),(513,'Built'),(604,'Recoverable')]
    for x,t in headings:s.text(x,ty+28,t,13,700,C['white'])
    for i,row in enumerate([('Planks','8','0','16','8'),('Cobblestone','0','0','12','0'),('Iron ingots','2','0','0','2')]):
        ry=ty+48+i*38;s.rect(56,ry,710,38,C['pale'] if i%2==0 else C['white'])
        for (x,_),t in zip(headings,row):s.text(x,ry+25,t,15,700 if x!=70 else 400)
    s.text(56,ty+190,'Recoverable is part of available stock, not an additional inventory.',14,700,C['teal'],maxw=710)
    s.text(56,ty+216,'No stock is committed to Stage 2 in this example.',14,color=C['muted'])
    sy=s.pair(814,b,'Next action','Return as a participant; an authorized player can start Stage 2.')
    s.button(814,sy,'View region boundary',width=310,secondary=True)
    sy=s.pair(814,sy+75,'If cancelled before commitment','8 planks + 2 iron ingots stay recoverable.')
    s.text(814,sy,'Leaving or reloading must not charge a stage twice.',14,700,C['teal'],maxw=310)
    return s.finish('Sources: UX01–UX03 · SET01–SET02 · FP-C01, FP-C03 · FP-PLAN-03, 04, 13, 14, 16. Quantities are proposed planning examples.')


def community():
    s=Sheet('ILL-U02','A shared place, with a clear memory',
            'Who keeps work active, who can act, and how the journal welcomes a player back.',
            'Three illustrative layouts: Old Valley horizontal and vertical activity boundaries; shared ownership and contribution receipts; a return journal after the first quarry workshop improvement. Bounds are schematic, participant names are roles, and no implementation is claimed.')
    y=186
    b=s.panel(y,'UX04','Regional activity boundary','Old Valley · managed activity','ACTIVE · 1 PLAYER','teal',
        'Only named managed systems follow this pause rule. Ordinary furnaces, mobs, crops, and redstone retain their own rules.')
    s.label(56,b,'Dimension: Earth · horizontal boundary')
    cx,cy=213,b+139
    s.circle(cx,cy,109,C['tealbg'],C['teal'],2)
    s.circle(cx,cy,125,'none',C['teal'],1,'6 6')
    s.text(cx,cy-58,'Old Valley',17,700,anchor='middle')
    s.circle(cx-35,cy+7,10,C['teal']);s.text(cx-17,cy+13,'You · eligible',14,700,C['teal'])
    s.circle(cx+83,cy+102,8,C['gray'],C['muted']);s.text(cx+101,cy+106,'Visitor',12,color=C['muted']);s.text(59,cy+150,'Dashed band: approach indication',12,color=C['muted'])
    s.label(414,b,'Vertical scope · same horizontal position')
    s.rect(432,b+32,307,225,C['pale'],7)
    s.rect(468,b+69,235,86,C['tealbg'],4,C['teal'],1)
    s.line(449,b+69,725,b+69,C['teal'],2,'4 3')
    s.line(449,b+155,725,b+155,C['teal'],2,'4 3')
    s.text(484,b+105,'Active vertical band',15,700,C['teal'])
    s.text(484,b+131,'Lower and upper bounds',13,color=C['muted'])
    s.circle(517,b+206,8,C['gray'],C['muted']);s.text(536,b+212,'Below this region',13)
    s.text(434,b+284,'Radius and Y bounds depend on the authored site.',12,color=C['muted'],maxw=315)
    sy=s.pair(814,b,'Participation','You keep this region active.')
    sy=s.pair(814,sy,'When the last participant leaves','Work pauses at the same saved state. No catch-up output.')
    s.text(814,sy,'A passing nonparticipant does not resume managed work.',14,700,C['teal'],maxw=310)
    s.button(814,y+349,'Inspect participants',width=310,secondary=True)

    y=680
    b=s.panel(y,'UX05','Ownership and contribution','Guesthouse · shared project record','SHARED WORLD','teal',
        'Blocked example: a contributor cannot cancel work or take project stock. Ask the owner; the accepted supplies remain recorded.')
    s.label(56,b,'Who can act · illustrative role labels')
    rows=[('Owner','Manage permissions; start stages; cancel eligible work.'),('Authorized operator','Start and operate within the permissions granted.'),('Contributor / visitor','Accepted deposits / public inspection; no cancellation rights.')]
    for i,(who,what) in enumerate(rows):
        yy=b+17+i*47
        s.rect(56,yy,710,43,C['pale'],4)
        s.text(68,yy+18,who,13,700)
        s.text(68,yy+35,what,13,color=C['muted'])
    s.label(56,b+181,'Stage 1 contribution receipt · already built')
    s.text(56,b+210,'You',16,700);s.text(212,b+210,'16 planks',16);s.pill(527,b+192,'BUILT INTO STAGE','teal',226)
    s.text(56,b+251,'Contributor A',16,700);s.text(212,b+251,'12 cobblestone',16);s.pill(527,b+233,'BUILT INTO STAGE','teal',226)
    s.text(56,b+286,'Each accepted transfer is credited once. Built stock is not loose storage.',13,color=C['muted'])
    sy=s.pair(814,b,'Shared fact','The repaired enclosure remains complete for everyone.')
    sy=s.pair(814,sy,'Personal progress','A newcomer receives a catch-up account; no second guesthouse bill.')
    s.button(814,y+349,'Inspect contributions',width=310,secondary=True)

    y=1174
    b=s.panel(y,'UX06','Return-to-play journal','Welcome back to Old Valley','CHAPTER 4 OPEN','blue',
        'Return links lead to known useful places. Personal discoveries stay recorded even if a carried sample was lost.')
    s.label(56,b,'Last meaningful event')
    s.text(56,b+27,'The quarry cargo cradle was widened.',21,700)
    s.text(56,b+54,'Two occupied cargo slots can now travel together on the fixed route.',15,color=C['muted'])
    s.rect(56,b+76,710,102,C['tealbg'],7)
    s.label(72,b+99,'Main chapter')
    s.text(72,b+126,'A working settlement',22,700)
    s.text(72,b+153,'Chapter 3 is complete. Mill, wider workshop, and market preparations remain.',14,maxw=680)
    s.label(56,b+205,'Optional opportunity')
    s.text(56,b+230,'Revisit the Copperwash survey ledge and the known return route.',15)
    s.label(56,b+265,'Personal discovery')
    s.text(56,b+290,'Tin seam observed · knowledge retained',16,700,C['teal'])
    sy=s.pair(814,b,'Next useful step','Review the next settlement projects.')
    s.button(814,sy,'View chapter projects',width=310)
    sy+=63
    s.label(814,sy,'Known useful destinations')
    s.button(814,sy+18,'Guesthouse · home',width=310,secondary=True)
    s.button(814,sy+65,'Quarry · workshop',width=310,secondary=True)
    s.text(814,sy+116,'Away-state example: quarry work waits while its region is empty.',13,color=C['muted'],maxw=310)
    return s.finish('Sources: UX04–UX06 · SET01–SET04 · FP-PLAN-02, 07, 15, 16, 17 · Regional activity, ownership, contribution, and journal rules.')


def expeditions():
    s=Sheet('ILL-U03','Prepare, find the route, come home',
            'Local orientation and expedition choices, from Copperwash Channels to the first Moon trip.',
            'Three illustrative layouts: a schematic Copperwash route map with height context and an unknown water branch; a preparation warning about missing light; and an Earth to Moon to Earth itinerary. Routes are relationships, not generated maps or undiscovered resource scans.')
    y=186
    b=s.panel(y,'UX07','Underground navigation','Shallows · Copperwash Channels','KNOWN ROUTE','teal',
        'Map distinction: known routes, personal markers, surveyed places, and unexplored possibilities. No ore-through-wall reveal.')
    s.label(56,b,'Route relationship · local elevation lower than quarry entrance')
    s.rect(56,b+15,710,280,C['pale'],7)
    s.path(f'M164 {b+70}L323 {b+135}L558 {b+220}',C['teal'],4)
    s.path(f'M323 {b+135}L539 {b+89}L558 {b+220}',C['teal'],3)
    s.path(f'M323 {b+135}L204 {b+234}',C['blue'],3,'7 6')
    for x,dy in [(164,70),(323,135),(539,89),(558,220)]:s.circle(x,b+dy,8,C['teal'],C['white'],2)
    s.circle(558,b+220,15,'none',C['orange'],3)
    s.text(89,b+47,'Quarry entrance',15,700)
    s.text(91,b+94,'Return to home',12,color=C['muted'])
    s.text(75,b+148,'Recognizable junction',14,700)
    s.text(498,b+57,'Dry maintenance cut',14,700)
    s.text(546,b+255,'Survey ledge · you',14,700,C['orange'])
    s.text(75,b+258,'Unexplored water branch',13,700,C['blue'])
    s.rect(650,b+187,13,13,C['orange'],2)
    s.text(595,b+178,'Your marker',12,color=C['muted'])
    sy=s.pair(814,b,'Known return','Follow the dry route to the quarry entrance, then the guesthouse.')
    sy=s.pair(814,sy,'Current stopping place','Survey ledge: dry ground, not a commissioned refuge.')
    s.text(814,sy,'The submerged continuation is outside this required expedition.',14,700,C['orange'],maxw=310)
    s.button(814,y+349,'Show route to entrance',width=310)

    y=680
    b=s.panel(y,'UX08','Expedition preparation','Copperwash · inspect a useful seam','REVIEW NEEDED','orange',
        'This is a preparation warning for the walking expedition. It adds no new equipment tier or automated travel lock.')
    s.label(56,b,'Carried equipment and useful reserves')
    s.check(56,b+39,'Early pick · carried','Supported early capability; no powered tool is required.')
    s.check(56,b+100,'Food and return provision · stocked','Keep useful food and light available for the walk home.')
    s.check(56,b+161,'Placed-light stock · missing','Bring torches before descending.',False)
    s.check(56,b+222,'Chosen cargo · room for a useful find','Seam knowledge survives even if a sample is lost.')
    s.text(56,b+289,'Markers and ordinary steps support orientation; no breathing or rope system is required.',13,color=C['muted'],maxw=710)
    sy=s.pair(814,b,'Known hazards','Dark passages, route choices, and an optional deeper pool.')
    sy=s.pair(814,sy,'Next action','Stock ordinary torches and review the known dry return route.')
    s.button(814,sy,'View preparation list',width=310)
    s.text(814,sy+68,'Required objective: observe the seam and return with the finding recorded.',14,700,C['teal'],maxw=310)

    y=1174
    b=s.panel(y,'UX11','Destination and route map','First lunar itinerary · Earth → Moon → Earth','ROUTE SELECTED','teal',
        'If an arrival footprint becomes blocked, choose a supported alternative. No first-arrival pad, refinery, or external console is assumed.')
    s.label(56,b,'Surveyed links and intended stops · schematic')
    s.rect(56,b+16,710,190,C['pale'],7)
    s.line(190,b+93,509,b+93,C['teal'],4,arrow=True)
    s.line(507,b+145,190,b+145,C['teal'],3,arrow=True)
    s.circle(171,b+93,25,C['tealbg'],C['teal'],2)
    s.circle(545,b+93,30,C['gray'],C['muted'],2)
    s.text(101,b+49,'Earth return site',17,700)
    s.text(468,b+49,'Moon arrival site',17,700)
    s.text(98,b+190,'Registered pad',13,700,C['teal'])
    s.text(469,b+190,'Surveyed natural ground',13,700)
    s.text(309,b+82,'Outbound',13,700,C['teal'])
    s.text(317,b+135,'Return',13,700,C['teal'])
    s.text(56,b+241,'Planned lunar refuge: carried from Earth; commission before committed descent.',15,700,maxw=710)
    s.text(56,b+280,'Mars is unavailable to this Explorer configuration. Orbital rehearsal is optional.',14,color=C['muted'],maxw=710)
    sy=s.pair(814,b,'Landing services','Natural support and safe access. Local fuel production is unavailable.')
    sy=s.pair(814,sy,'Chosen return plan','Carry mission supplies and protect the Earth return allocation.')
    s.button(814,y+349,'Review mission readiness',width=310)
    return s.finish('Sources: UX07–UX08, UX11 · EAR02 · DST01 · FP-PLAN-06, 16, 17 · T1-OV01–02 · T1-SPEC-07, 12, 15, 18.')


def space_accessibility():
    s=Sheet('ILL-U04','A spacecraft the player can understand',
            'Assembly, the next launch-blocking action, and readable information with reduced effects.',
            'Three illustrative layouts: the six committed Explorer assembly stages and linked vehicle berth; a lunar mission blocked by an open outer hatch with protected return supplies; and accessibility settings with text-and-symbol state previews. Cargo and resource capacities are intentionally not invented.')
    y=186
    b=s.panel(y,'UX09','Spacecraft assembly','Explorer · recognized hull plan','MISSING COMPONENT','orange',
        'If the linked berth is blocked, locate the obstruction before the next stage. The same craft and its committed parts remain recorded.')
    s.label(56,b,'Six committed Explorer stages · T1-SPEC-06')
    stages=[('1','Reserve authorized parts order','Needs engine'),('2','Lower frame and engine cluster','Upcoming'),('3','Tank and main structural modules','Upcoming'),('4','Cabin, window, and inner pressure assembly','Upcoming'),('5','Nose, fins, and exterior services','Upcoming'),('6','Commission the finished craft','Upcoming')]
    for i,(n,label,state) in enumerate(stages):
        yy=b+18+i*42
        s.rect(56,yy,710,37,C['orangebg'] if i==0 else C['pale'],4)
        s.text(70,yy+24,n,14,700,C['orange'] if i==0 else C['teal'])
        s.text(103,yy+24,label,15,700 if i==0 else 400)
        s.text(593,yy+24,state,13,700,C['orange'] if i==0 else C['muted'])
    s.text(56,b+297,'Cabin and cargo capability are pending their stages and commissioning.',14,color=C['muted'])
    sy=s.pair(814,b,'Next action','Supply the Rocket Engine.')
    s.button(814,sy,'View required components',width=310)
    sy=s.pair(814,sy+64,'Assembly relationship','Station → linked pad berth')
    s.text(814,sy,'The rocket is assembled in its vehicle berth, not inside the smaller station.',14,color=C['muted'],maxw=310)
    s.text(814,sy+52,'Fuel tank: in project stock\nCargo space: not commissioned',14,700,C['teal'],maxw=310)

    y=680
    b=s.panel(y,'UX10','Mission readiness','Explorer · first lunar mission','DEPARTURE BLOCKED','orange',
        'When all required conditions pass: Ready → cancellable countdown. Changes to cargo, services, or clearance trigger the relevant recheck.')
    s.label(56,b,'Earth → Moon → Earth · one pilot configuration')
    checks=[('Route','Supported lunar route; navigation data loaded.',True),('Crew','Pilot seated in the supported seat.',True),('Cabin','Outer hatch open; seal and recheck.',False),('Fuel','Outbound allocation + protected return present.',True),('Breathing','Mission allocation + protected contingency present.',True),('Cargo','Starter shelter, service stock, and field power loaded.',True),('Return','Earth return site registered; supplies carried.',True)]
    for i,(label,detail,good) in enumerate(checks):
        yy=b+20+i*38
        s.rect(56,yy,710,33,C['tealbg'] if good else C['orangebg'],4)
        s.text(70,yy+23,'✓' if good else '!',16,700,C['teal'] if good else C['orange'])
        s.text(99,yy+23,label,14,700)
        s.text(209,yy+23,detail,13,700 if not good else 400)
    sy=s.pair(814,b,'Next blocking action','Seal the outer hatch; recheck the cabin.')
    s.button(814,sy,'Locate outer hatch',width=310)
    s.button(814,sy+49,'Start countdown · unavailable',width=310,disabled=True)
    sy=s.pair(814,sy+108,'Protected allocations','Return fuel and breathing contingency stay reserved.')
    s.text(814,sy,'Ordinary work cannot use them. Changing reserves revises the mission.',13,700,C['orange'],maxw=310)

    y=1174
    b=s.panel(y,'UX12','Accessibility settings','Readable states, with reduced effects','EXAMPLE SETTINGS','blue',
        'Static labels and symbols retain the meaning of motion, light, and sound. Final narration support follows the actual game’s available hooks.',split=False)
    s.line(396,b-6,396,b+297)
    s.line(773,b-6,773,b+297)
    s.label(56,b,'Readability')
    for i,(k,v) in enumerate([('Text size','Large'),('Contrast','High'),('State labels','Text + symbol')]):
        yy=b+22+i*65
        s.text(56,yy+15,k,15,700)
        s.rect(56,yy+24,313,29,C['pale'],4,C['line'])
        s.text(68,yy+44,v,14)
    s.label(422,b,'Motion and effects')
    for i,(k,v) in enumerate([('Camera motion','Reduced'),('Particles','Reduced'),('Hazard sound captions','On')]):
        yy=b+22+i*65
        s.text(422,yy+15,k,15,700)
        s.rect(422,yy+24,325,29,C['pale'],4,C['teal'] if i==0 else C['line'],2 if i==0 else 1)
        s.text(434,yy+44,v,14)
    s.label(799,b,'Information preview')
    s.rect(799,b+22,345,86,C['bluebg'],7)
    s.text(815,b+47,'Ⅱ  PAUSED',15,700,C['blue'])
    s.text(815,b+72,'No participating player is in this region.',13,maxw=310,leading=18)
    s.rect(799,b+121,345,86,C['orangebg'],7)
    s.text(815,b+146,'!  BLOCKED',15,700,C['orange'])
    s.text(815,b+171,'Move the chest from the marked deck position.',13,maxw=310,leading=18)
    s.button(56,b+241,'Apply settings',width=214)
    s.button(286,b+241,'Review dialogue',width=220,secondary=True)
    s.label(534,b+226,'Known-route assistance')
    s.rect(534,b+241,610,37,C['pale'],6,C['line'])
    s.text(548,b+265,'Show known route and personal markers',14)
    return s.finish('Sources: UX09–UX10, UX12 · T1-M02, T1-M07, T1-V01 · T1-SPEC-06, 09, 12, 15, 17, 18 · Construction and accessibility direction.')


RECORDS = [
    {'id':'ILL-U01','file':'construction-interfaces.svg','title':'Construction interfaces','alt':'Guesthouse project controller, protected crossing footprint, and construction stock ledger with normal and blocked examples.','caption':'Illustrative layouts for UX01–UX03. The example guesthouse bills and stage stock follow FP-C01; a marked player chest blocks the crossing rather than being overwritten. Available, committed, built, and recoverable stock remain distinct. Values are planning examples.','source_ids':['UX01','UX02','UX03','SET01','SET02','FP-C01','FP-C03','FP-PLAN-03','FP-PLAN-04','FP-PLAN-13','FP-PLAN-14','FP-PLAN-16'],'related_chapters':['Settlements and authored adventures','Residents and settlement systems','First Playable — Old Valley, the Quarry, and the First Workshop'],'slice':2,'kind':'interface'},
    {'id':'ILL-U02','file':'community-interfaces.svg','title':'Community and return interfaces','alt':'Regional activity bounds, ownership and shared material receipts, and a return-to-play journal after the quarry improvement.','caption':'Illustrative layouts for UX04–UX06. Managed activity needs eligible participants within the correct dimension and horizontal and vertical bounds. Contributions and shared construction are distinct from personal discoveries. The journal keeps Chapter 4 open after the limited first workshop service.','source_ids':['UX04','UX05','UX06','SET01','SET04','FP-PLAN-02','FP-PLAN-07','FP-PLAN-15','FP-PLAN-16','FP-PLAN-17'],'related_chapters':['Old Valley','Settlements and authored adventures','Connected adventures and player experience','First Playable — Old Valley, the Quarry, and the First Workshop'],'slice':2,'kind':'interface'},
    {'id':'ILL-U03','file':'expedition-interfaces.svg','title':'Navigation and expedition interfaces','alt':'Copperwash route diagram, a missing-light preparation warning, and an Earth–Moon–Earth route with natural landing ground and protected return supplies.','caption':'Illustrative layouts for UX07–UX08 and UX11. Navigation separates known routes from unexplored branches. The local walking trip uses early tools and ordinary light. The lunar itinerary requires a supported natural arrival and Earth-carried essentials; no lunar pad or refinery is assumed.','source_ids':['UX07','UX08','UX11','EAR02','DST01','FP-PLAN-06','FP-PLAN-16','FP-PLAN-17','T1-OV01','T1-OV02','T1-SPEC-07','T1-SPEC-12','T1-SPEC-15','T1-SPEC-18'],'related_chapters':['Shallows','Connected adventures and player experience','First Playable — Old Valley, the Quarry, and the First Workshop','Tier 1 space systems — complete first expedition'],'slice':2,'kind':'interface'},
    {'id':'ILL-U04','file':'space-accessibility-interfaces.svg','title':'Spacecraft and accessibility interfaces','alt':'Six committed Explorer assembly stages, a mission blocked by an open outer hatch, and accessibility settings with text and symbol state previews.','caption':'Illustrative layouts for UX09–UX10 and UX12. The six committed Explorer stages follow T1-SPEC-06; the station controls an explicitly linked vehicle berth. Mission readiness names the immediate blocker and keeps return fuel and breathing reserves protected. Readable static states survive reduced motion and effects. Numeric fuel, oxygen, and cargo capacities are intentionally not invented.','source_ids':['UX09','UX10','UX12','T1-M02','T1-M07','T1-V01','T1-SPEC-06','T1-SPEC-09','T1-SPEC-12','T1-SPEC-15','T1-SPEC-17','T1-SPEC-18'],'related_chapters':['Spacecraft and orbital places','Tier 1 space systems — complete first expedition','Tier 1 — Model and Implementation Specifications','Connected adventures and player experience'],'slice':2,'kind':'interface'},
]


def main():
    ap=argparse.ArgumentParser(description=__doc__)
    ap.add_argument('--source-dir',type=Path,default=ROOT/'substellar'/'sources')
    ap.add_argument('--output-dir',type=Path,default=ROOT/'illustrated_03_interfaces')
    ap.add_argument('--metadata',type=Path,default=ROOT/'illustrated_03_interfaces.json')
    args=ap.parse_args()
    reg=json.loads((args.source_dir/'first_playable_register.json').read_text())
    guest=next(r for r in reg['construction_orders'] if r['id']=='FP-C01')
    assert guest['inputs']=={'planks':24,'cobblestone':12,'iron_ingots':2}
    assert guest['stages'][0]['inputs']=={'planks':16,'cobblestone':12}
    assert guest['stages'][1]['inputs']=={'planks':8,'iron_ingots':2}
    approved=(args.source_dir/'complete_text_for_typesetting.txt').read_text()
    for n in range(1,13): assert f'UX{n:02}' in approved
    models=(args.source_dir/'tier1_model_specifications.txt').read_text()
    assert 'six committed stages' in models
    args.output_dir.mkdir(parents=True,exist_ok=True)
    builders=[construction,community,expeditions,space_accessibility]
    for record,builder in zip(RECORDS,builders):
        content=builder()
        ET.fromstring(content)
        assert len(re.findall('ILLUSTRATIVE INTERFACE LAYOUT',content))==1
        (args.output_dir/record['file']).write_text(content,encoding='utf-8')
    args.metadata.write_text(json.dumps(RECORDS,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
    print(json.dumps({'svg_sheets':4,'interface_references':12,'output_dir':str(args.output_dir),'metadata':str(args.metadata),'status':'XML valid; visual render review still required'},indent=2))


if __name__=='__main__':main()
