//! Experimental cyberia console: fleets & flats.
//!
//! soft3 compliance (experimental surface):
//! - intent is first-class (assign fleet ร— action ร— flat)
//! - open map data (public Gesing KML), no closed gatekeeper
//! - client-local queue only โ€” no proprietary backend API
//! - vocabulary: fleet (work), flat (hold/space), intent (write)

use crate::land::FLAG_SVG;
use crate::nav::CyberiaNav;
use crate::robots::{load_owned, save_owned, OwnedRobot};
use crate::wallet::{
    debit_cx, load_intents, load_leases, save_intents, save_leases, IntentRec, Lease,
};
use leptos::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use wasm_bindgen::JsCast;

const MAP_JSON: &str = include_str!("cyberia_map.json");

#[derive(Clone, Debug, Deserialize)]
struct MapData {
    site: String,
    center: [f64; 2],
    bbox: BBox,
    #[serde(default)]
    stats: MapStats,
    /// All interactive land plots (from KML `plots` folder).
    phase0: Vec<Flat>,
    #[serde(default)]
    districts: Vec<Flat>,
    places: Vec<Flat>,
    /// 21 cybics domains as citadel shill points (no core/bridge).
    #[serde(default)]
    domains: Vec<DomainMark>,
    source: String,
}

#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
struct DomainMark {
    id: String,
    name: String,
    #[serde(default)]
    triad: String,
    #[serde(default)]
    question: String,
    #[serde(default)]
    zone: String,
    #[serde(default)]
    district: String,
    #[serde(default)]
    shill: String,
    #[serde(default)]
    href: String,
    coords: Vec<[f64; 2]>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LeftBoard {
    Fleets,
    Domains,
}

const DOMAIN_POS_KEY: &str = "cyberia.domain_positions.v1";

fn triad_color(triad: &str) -> &'static str {
    match triad {
        "form" => "#00b4ff",  // cyan โ€” rules
        "mass" => "#ff6501",  // orange โ€” matter
        "space" => "#1700fe", // blue โ€” where
        "life" => "#00ff01",  // green โ€” alive
        "word" => "#6b00fe",  // violet โ€” meaning
        "work" => "#ffc501",  // yellow โ€” making
        "play" => "#fe0000",  // red โ€” coordinate
        _ => "#00ff01",
    }
}

/// Canonical cybics triad order for domain board grouping.
const TRIAD_ORDER: &[&str] = &["form", "mass", "space", "life", "word", "work", "play"];

fn triad_question(triad: &str) -> &'static str {
    match triad {
        "form" => "what are the rules?",
        "mass" => "what is it made of?",
        "space" => "where does it happen?",
        "life" => "who is alive?",
        "word" => "what does it mean?",
        "work" => "how is it made?",
        "play" => "how do we coordinate?",
        _ => "",
    }
}

#[derive(Clone, Debug, Default, Deserialize)]
struct MapStats {
    #[serde(default)]
    plot_count: u32,
    #[serde(default)]
    plot_ha: f64,
    #[serde(default)]
    district_ha: f64,
}

#[derive(Clone, Debug, Deserialize)]
struct BBox {
    min_lon: f64,
    max_lon: f64,
    min_lat: f64,
    max_lat: f64,
}

fn load_domain_positions() -> HashMap<String, [f64; 2]> {
    web_sys::window()
        .and_then(|w| w.local_storage().ok().flatten())
        .and_then(|ls| ls.get_item(DOMAIN_POS_KEY).ok().flatten())
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default()
}

fn save_domain_positions(domains: &[DomainMark]) {
    let mut m = HashMap::new();
    for d in domains {
        if let Some(c) = d.coords.first() {
            m.insert(d.id.clone(), [c[0], c[1]]);
            m.insert(d.name.clone(), [c[0], c[1]]);
        }
    }
    if let Some(ls) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
        if let Ok(s) = serde_json::to_string(&m) {
            let _ = ls.set_item(DOMAIN_POS_KEY, &s);
        }
    }
}

fn clear_domain_positions() {
    if let Some(ls) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
        let _ = ls.remove_item(DOMAIN_POS_KEY);
    }
}

fn apply_domain_positions(mut domains: Vec<DomainMark>) -> Vec<DomainMark> {
    let pos = load_domain_positions();
    for d in &mut domains {
        if let Some(c) = pos.get(&d.id).or_else(|| pos.get(&d.name)) {
            d.coords = vec![[c[0], c[1]]];
        }
    }
    domains
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
struct Flat {
    id: String,
    name: String,
    kind: String,
    phase: u32,
    geom: String,
    coords: Vec<[f64; 2]>,
    #[serde(default)]
    zone: String,
}

#[derive(Clone, Copy, Debug, PartialEq)]
struct FleetUnit {
    id: &'static str,
    name: &'static str,
    kind: &'static str, // worker | machine
    role: &'static str,
    status: &'static str, // idle | busy | offline
    phase: u32,
}

/// Existing Gesing hard-force workers (ops roster) โ€” 15 people on site.
/// Source: cve/ops/hard force.md crews (repair ยท cube ยท base ยท pruning ยท delivery).
const WORKERS: &[FleetUnit] = &[
    // repair
    FleetUnit {
        id: "w-sutar",
        name: "SUTAR",
        kind: "worker",
        role: "repair lead ยท energy/water",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-witaya",
        name: "WITAYA",
        kind: "worker",
        role: "repair ยท electronics",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-lupus",
        name: "LUPUS",
        kind: "worker",
        role: "repair ยท mechanical",
        status: "idle",
        phase: 0,
    },
    // cube
    FleetUnit {
        id: "w-sudi",
        name: "SUDI",
        kind: "worker",
        role: "cube lead ยท build",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-budi",
        name: "BUDI",
        kind: "worker",
        role: "cube ยท build",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-tika",
        name: "TIKA",
        kind: "worker",
        role: "cube ยท stove",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-sastra",
        name: "SASTRA",
        kind: "worker",
        role: "cube ยท build",
        status: "idle",
        phase: 0,
    },
    // base
    FleetUnit {
        id: "w-angga",
        name: "ANGGA",
        kind: "worker",
        role: "base lead ยท road/trail",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-darma",
        name: "DARMA",
        kind: "worker",
        role: "base ยท mason",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-darsana",
        name: "DARSANA",
        kind: "worker",
        role: "base ยท terrace",
        status: "idle",
        phase: 0,
    },
    // pruning
    FleetUnit {
        id: "w-arima",
        name: "ARIMA",
        kind: "worker",
        role: "pruning lead ยท land",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-doplang",
        name: "DOPLANG",
        kind: "worker",
        role: "pruning ยท firewood",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-surya",
        name: "SURYA",
        kind: "worker",
        role: "pruning ยท fodder",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "w-suardita",
        name: "SUARDITA",
        kind: "worker",
        role: "pruning ยท compost",
        status: "idle",
        phase: 0,
    },
    // delivery
    FleetUnit {
        id: "w-pande",
        name: "PANDE",
        kind: "worker",
        role: "delivery lead ยท haul",
        status: "idle",
        phase: 0,
    },
];

/// Phase-0 machines (hardware fleet).
const MACHINES: &[FleetUnit] = &[
    FleetUnit {
        id: "f-eye",
        name: "EYE-01",
        kind: "machine",
        role: "survey drone",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "f-haul",
        name: "HAUL-01",
        kind: "machine",
        role: "ground rover",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "f-cut",
        name: "CUT-01",
        kind: "machine",
        role: "clearing arm",
        status: "offline",
        phase: 0,
    },
    FleetUnit {
        id: "f-build",
        name: "CUBE-01",
        kind: "machine",
        role: "build stack",
        status: "offline",
        phase: 1,
    },
];

fn all_stock_fleets() -> impl Iterator<Item = &'static FleetUnit> {
    WORKERS.iter().chain(MACHINES.iter())
}

fn stock_fleet(id: &str) -> Option<&'static FleetUnit> {
    all_stock_fleets().find(|f| f.id == id)
}

/// Open ring (no duplicate close point).
fn open_ring(coords: &span>; 2) -> Vec<[f64; 2]> {
    let mut c = coords.to_vec();
    if c.len() > 1 && c.first() == c.last() {
        c.pop();
    }
    c
}

fn close_ring(mut c: Vec<[f64; 2]>) -> Vec<[f64; 2]> {
    if c.len() >= 2 && c.first() != c.last() {
        let first = c[0];
        c.push(first);
    }
    c
}

fn poly_bbox(coords: &span>; 2) -> (f64, f64, f64, f64) {
    let ring = open_ring(coords);
    let mut min_lon = f64::INFINITY;
    let mut max_lon = f64::NEG_INFINITY;
    let mut min_lat = f64::INFINITY;
    let mut max_lat = f64::NEG_INFINITY;
    for p in &ring {
        min_lon = min_lon.min(p[0]);
        max_lon = max_lon.max(p[0]);
        min_lat = min_lat.min(p[1]);
        max_lat = max_lat.max(p[1]);
    }
    (min_lon, max_lon, min_lat, max_lat)
}

/// Planar area in mยฒ via equirectangular projection around centroid (WGS84).
fn area_m2(coords: &span>; 2) -> f64 {
    let ring = open_ring(coords);
    if ring.len() < 3 {
        return 0.0;
    }
    let lat0 = ring.iter().map(|c| c[1]).sum::<f64>() / ring.len() as f64;
    let lon0 = ring.iter().map(|c| c[0]).sum::<f64>() / ring.len() as f64;
    const R: f64 = 6_378_137.0;
    let cos_lat = lat0.to_radians().cos();
    let to_xy = |lon: f64, lat: f64| -> (f64, f64) {
        let x = (lon - lon0).to_radians() * R * cos_lat;
        let y = (lat - lat0).to_radians() * R;
        (x, y)
    };
    let mut a = 0.0;
    for i in 0..ring.len() {
        let (x1, y1) = to_xy(ring[i][0], ring[i][1]);
        let j = (i + 1) % ring.len();
        let (x2, y2) = to_xy(ring[j][0], ring[j][1]);
        a += x1 * y2 - x2 * y1;
    }
    (a.abs()) * 0.5
}

fn fmt_area_m2(m2: f64) -> String {
    if m2 >= 10_000.0 {
        format!("{:.2} ha ({:.0} mยฒ)", m2 / 10_000.0, m2)
    } else if m2 >= 100.0 {
        format!("{:.0} mยฒ", m2)
    } else {
        format!("{:.1} mยฒ", m2)
    }
}

/// Bbox span along axis in meters (axis 0 = lon width, 1 = lat height).
fn bbox_span_m(coords: &span>; 2, axis: usize) -> f64 {
    let (min_lon, max_lon, min_lat, max_lat) = poly_bbox(coords);
    const R: f64 = 6_378_137.0;
    let mid_lat = (min_lat + max_lat) * 0.5;
    if axis == 0 {
        (max_lon - min_lon).to_radians() * R * mid_lat.to_radians().cos()
    } else {
        (max_lat - min_lat).to_radians() * R
    }
}

/// Sutherlandโ€“Hodgman clip against half-plane on lon (axis=0) or lat (axis=1).
/// `keep_le`: keep points with value โ‰ค t (west / south side).
fn clip_half_plane(poly: &span>; 2, axis: usize, t: f64, keep_le: bool) -> Vec<[f64; 2]> {
    let ring = open_ring(poly);
    if ring.is_empty() {
        return vec![];
    }
    let inside = |p: [f64; 2]| -> bool {
        let v = p[axis];
        if keep_le {
            v <= t + 1e-12
        } else {
            v >= t - 1e-12
        }
    };
    let intersect = |a: [f64; 2], b: [f64; 2]| -> [f64; 2] {
        let da = a[axis] - t;
        let db = b[axis] - t;
        let denom = da - db;
        let u = if denom.abs() < 1e-15 { 0.5 } else { da / denom };
        [a[0] + u * (b[0] - a[0]), a[1] + u * (b[1] - a[1])]
    };
    let mut out: Vec<[f64; 2]> = Vec::new();
    let n = ring.len();
    for i in 0..n {
        let cur = ring[i];
        let prev = ring[(i + n - 1) % n];
        let cur_in = inside(cur);
        let prev_in = inside(prev);
        if cur_in {
            if !prev_in {
                out.push(intersect(prev, cur));
            }
            out.push(cur);
        } else if prev_in {
            out.push(intersect(prev, cur));
        }
    }
    // dedupe consecutive near-equal points
    let mut cleaned: Vec<[f64; 2]> = Vec::new();
    for p in out {
        if cleaned
            .last()
            .map(|q| (q[0] - p[0]).abs() > 1e-10 || (q[1] - p[1]).abs() > 1e-10)
            .unwrap_or(true)
        {
            cleaned.push(p);
        }
    }
    close_ring(cleaned)
}

/// User-controlled split: axis 0 = vertical cut (E/W), 1 = horizontal (N/S).
/// `ratio` in (0,1) is cut position across the polygon bbox (0 = west/south).
fn split_flat_at(flat: &Flat, axis: usize, ratio: f64) -> Option<(Flat, Flat, f64)> {
    let r = ratio.clamp(0.05, 0.95);
    let (min_lon, max_lon, min_lat, max_lat) = poly_bbox(&flat.coords);
    let t = if axis == 0 {
        min_lon + r * (max_lon - min_lon)
    } else {
        min_lat + r * (max_lat - min_lat)
    };
    let a_coords = clip_half_plane(&flat.coords, axis, t, true);
    let b_coords = clip_half_plane(&flat.coords, axis, t, false);
    // closed ring needs โ‰ฅ4 points (triangle + close)
    if a_coords.len() < 4 || b_coords.len() < 4 {
        return None;
    }
    if area_m2(&a_coords) < 1.0 || area_m2(&b_coords) < 1.0 {
        return None;
    }
    let a = Flat {
        id: format!("{}-a", flat.id),
        name: format!("{}-a", flat.name),
        kind: flat.kind.clone(),
        phase: flat.phase,
        geom: "polygon".into(),
        coords: a_coords,
        zone: flat.zone.clone(),
    };
    let b = Flat {
        id: format!("{}-b", flat.id),
        name: format!("{}-b", flat.name),
        kind: flat.kind.clone(),
        phase: flat.phase,
        geom: "polygon".into(),
        coords: b_coords,
        zone: flat.zone.clone(),
    };
    Some((a, b, t))
}

fn merge_flats(a: &Flat, b: &Flat) -> Flat {
    let mut coords = a.coords.clone();
    if coords.len() > 1 && coords.first() == coords.last() {
        coords.pop();
    }
    for c in &b.coords {
        if coords.last() != Some(c) {
            coords.push(*c);
        }
    }
    if coords.len() >= 2 && coords.first() != coords.last() {
        coords.push(coords[0]);
    }
    Flat {
        id: format!("{}+{}", a.id, b.id),
        name: format!("{}+{}", a.name, b.name),
        kind: a.kind.clone(),
        phase: a.phase.min(b.phase),
        geom: "polygon".into(),
        coords,
        zone: if a.zone == b.zone {
            a.zone.clone()
        } else {
            "mixed".into()
        },
    }
}

fn zone_key(flat: &Flat) -> &str {
    if !flat.zone.is_empty() {
        return flat.zone.as_str();
    }
    // fallback: prefix of id/name
    let s = if flat.id.is_empty() {
        flat.name.as_str()
    } else {
        flat.id.as_str()
    };
    s
}

fn flat_fill(zone_or_id: &str, selected: bool) -> &'static str {
    let z = zone_or_id.to_lowercase();
    // Soft glass tiles โ€” readable color, not neon slabs (edges carry definition)
    let (hi, lo) = if z.contains("avalon") {
        ("rgba(255,120,20,0.48)", "rgba(255,102,0,0.22)")
    } else if z.contains("sinwood") {
        ("rgba(20,255,80,0.46)", "rgba(0,230,50,0.20)")
    } else if z.contains("bridge") {
        ("rgba(40,235,255,0.44)", "rgba(0,210,240,0.18)")
    } else if z.contains("core") {
        ("rgba(255,220,40,0.46)", "rgba(255,210,0,0.20)")
    } else if z.contains("ether") {
        ("rgba(180,100,255,0.46)", "rgba(150,70,255,0.20)")
    } else if z.contains("asgard") {
        ("rgba(255,50,90,0.44)", "rgba(255,20,70,0.18)")
    } else if z.contains("edem") || z.contains("canyon") {
        ("rgba(0,255,210,0.44)", "rgba(0,240,190,0.18)")
    } else if z.contains("front") || z.contains("avatar") {
        ("rgba(200,200,210,0.40)", "rgba(160,160,170,0.16)")
    } else {
        ("rgba(180,180,180,0.40)", "rgba(140,140,140,0.16)")
    };
    if selected {
        hi
    } else {
        lo
    }
}

/// Idle borders stay dim so hover/select glow actually reads.
/// Hot = hover or selected โ€” zone neon / white.
fn flat_stroke_col(zone_or_id: &str, selected: bool, hover: bool) -> &'static str {
    let z = zone_or_id.to_lowercase();
    if selected {
        return "#ffffff";
    }
    if hover {
        // bright edge โ€” the only "glowing" state
        return if z.contains("avalon") {
            "#ffb070"
        } else if z.contains("sinwood") {
            "#7dff9a"
        } else if z.contains("bridge") {
            "#9af6ff"
        } else if z.contains("core") {
            "#ffe98a"
        } else if z.contains("ether") {
            "#e0a8ff"
        } else if z.contains("asgard") {
            "#ff8aa0"
        } else if z.contains("edem") || z.contains("canyon") {
            "#8affe8"
        } else {
            "#e8e8e8"
        };
    }
    // idle: deep, low-contrast seams (tile grid, not neon wire)
    if z.contains("avalon") {
        "rgba(255,120,40,0.18)"
    } else if z.contains("sinwood") {
        "rgba(40,220,80,0.16)"
    } else if z.contains("bridge") {
        "rgba(40,210,230,0.16)"
    } else if z.contains("core") {
        "rgba(230,200,40,0.16)"
    } else if z.contains("ether") {
        "rgba(160,90,255,0.16)"
    } else if z.contains("asgard") {
        "rgba(255,50,90,0.16)"
    } else if z.contains("edem") || z.contains("canyon") {
        "rgba(0,230,190,0.16)"
    } else {
        "rgba(180,180,180,0.14)"
    }
}

fn zone_hover_fill(zone_or_id: &str) -> &'static str {
    // fill lift on hover โ€” still secondary to the glowing stroke
    let z = zone_or_id.to_lowercase();
    if z.contains("avalon") {
        "rgba(255,130,30,0.42)"
    } else if z.contains("sinwood") {
        "rgba(40,255,90,0.40)"
    } else if z.contains("bridge") {
        "rgba(50,240,255,0.38)"
    } else if z.contains("core") {
        "rgba(255,225,50,0.40)"
    } else if z.contains("ether") {
        "rgba(170,90,255,0.40)"
    } else if z.contains("asgard") {
        "rgba(255,40,80,0.38)"
    } else if z.contains("edem") || z.contains("canyon") {
        "rgba(20,255,200,0.38)"
    } else {
        "rgba(200,200,200,0.32)"
    }
}

fn render_fleet_card(
    f: &'static FleetUnit,
    selected_fleet: RwSignal<Option<String>>,
) -> impl IntoView {
    let id = f.id.to_string();
    let id2 = id.clone();
    let offline = f.status == "offline" || f.phase > 0;
    let kind_worker = f.kind == "worker";
    let name = f.name;
    let role = f.role;
    let kind = f.kind;
    let status = f.status;
    let phase = f.phase;
    view! {
        <button
            class=move || {
                let sel = selected_fleet.get().as_deref() == Some(id.as_str());
                format!(
                    "fleet-card{}{}{}",
                    if sel { " sel" } else { "" },
                    if offline { " offline" } else { "" },
                    if kind_worker { " worker" } else { " machine" },
                )
            }
            disabled=offline
            on:click=move |_| {
                if !offline {
                    selected_fleet.set(Some(id2.clone()));
                }
            }
        >
            <div class="fleet-top">
                <span class="fleet-name">{name}</span>
                <span class="fleet-status" style:color=status_color(status)>
                    {status.to_uppercase()}
                </span>
            </div>
            <div class="fleet-role">{role}</div>
            <div class="fleet-meta">
                <span>{kind.to_uppercase()}</span>
                <span>{format!("P{phase}")}</span>
            </div>
        </button>
    }
}

const ACTIONS: &[(&str, &str)] = &[
    ("survey", "map + sense the flat"),
    ("clear", "remove undergrowth / debris"),
    ("plant", "set regeneratives"),
    ("haul", "move material on-site"),
    ("watch", "hold a perimeter watch"),
    ("build", "raise a cyberCube (phase 1+)"),
];

/// Robots available for purchase (catalog, not yet owned).
const ROBOT_CATALOG: &[FleetUnit] = &[
    FleetUnit {
        id: "cat-eye",
        name: "EYE",
        kind: "machine",
        role: "survey drone",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "cat-hand",
        name: "HAND",
        kind: "worker",
        role: "field hand",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "cat-haul",
        name: "HAUL",
        kind: "machine",
        role: "ground rover",
        status: "idle",
        phase: 0,
    },
    FleetUnit {
        id: "cat-cut",
        name: "CUT",
        kind: "machine",
        role: "clearing arm",
        status: "idle",
        phase: 0,
    },
];

#[derive(Clone, Copy, Debug, PartialEq)]
enum Sheet {
    None,
    BuyRobot,
    LeaseLand,
    SplitLand,
    MergeLand,
}

fn load_map() -> MapData {
    serde_json::from_str(MAP_JSON).expect("cyberia_map.json")
}

/// Project lon/lat into a viewBox-local SVG plane (y flipped).
fn project(lon: f64, lat: f64, bbox: &BBox, w: f64, h: f64, pad: f64) -> (f64, f64) {
    let dx = (bbox.max_lon - bbox.min_lon).max(1e-9);
    let dy = (bbox.max_lat - bbox.min_lat).max(1e-9);
    // square-ish fit with padding
    let x = pad + (lon - bbox.min_lon) / dx * (w - 2.0 * pad);
    let y = pad + (1.0 - (lat - bbox.min_lat) / dy) * (h - 2.0 * pad);
    (x, y)
}

/// Inverse of `project` โ€” SVG/world โ†’ lon/lat (for domain drag).
fn unproject(x: f64, y: f64, bbox: &BBox, w: f64, h: f64, pad: f64) -> (f64, f64) {
    let dx = (bbox.max_lon - bbox.min_lon).max(1e-9);
    let dy = (bbox.max_lat - bbox.min_lat).max(1e-9);
    let uw = (w - 2.0 * pad).max(1e-9);
    let uh = (h - 2.0 * pad).max(1e-9);
    let lon = bbox.min_lon + (x - pad) / uw * dx;
    let lat = bbox.min_lat + (1.0 - (y - pad) / uh) * dy;
    (lon, lat)
}

fn poly_path(coords: &span>; 2, bbox: &BBox, w: f64, h: f64, pad: f64) -> String {
    if coords.is_empty() {
        return String::new();
    }
    let mut s = String::new();
    for (i, c) in coords.iter().enumerate() {
        let (x, y) = project(c[0], c[1], bbox, w, h, pad);
        if i == 0 {
            s.push_str(&format!("M{x:.2},{y:.2}"));
        } else {
            s.push_str(&format!(" L{x:.2},{y:.2}"));
        }
    }
    s.push('Z');
    s
}

/// Game camera zoom range.
/// Max ~1000 โ†’ ~1.5 m view width on Gesing site โ†’ 10 cm tile โ‰ˆ 60โ€“70 screen px
/// (enough to layout tiny hex/tiles as a planning game).
const MAP_ZOOM_MIN: f64 = 0.5;
const MAP_ZOOM_MAX: f64 = 1000.0;

/// Keep HUD-sized marks (labels, dots) constant on screen while viewBox zooms.
/// Anchor at (x,y) in SVG/world units.
fn screen_stable_tf(x: f64, y: f64, zoom: f64) -> String {
    let s = 1.0 / zoom.max(0.25);
    format!("translate({x:.2},{y:.2}) scale({s:.4})")
}

/// SVG camera: viewBox centered at `center` with size (W/z)ร—(H/z).
/// Pure vector zoom โ€” no CSS scale raster blur.
fn map_view_box(center: (f64, f64), zoom: f64, w: f64, h: f64) -> String {
    let z = zoom.max(0.25);
    let vw = w / z;
    let vh = h / z;
    format!(
        "{:.4} {:.4} {:.4} {:.4}",
        center.0 - vw * 0.5,
        center.1 - vh * 0.5,
        vw,
        vh
    )
}

/// Site envelope size in meters from lon/lat bbox (approx WGS84).
fn site_span_m(bbox: &BBox) -> (f64, f64) {
    let lat0 = (bbox.min_lat + bbox.max_lat) * 0.5;
    let m_lat = 111_320.0;
    let m_lon = 111_320.0 * lat0.to_radians().cos();
    let ew = (bbox.max_lon - bbox.min_lon) * m_lon;
    let ns = (bbox.max_lat - bbox.min_lat) * m_lat;
    (ew, ns)
}

/// Visible world width (meters) at current zoom โ€” for HUD scale readout.
fn view_width_m(zoom: f64, map_w: f64, pad: f64, site_ew_m: f64) -> f64 {
    let z = zoom.max(0.25);
    let usable = (map_w - 2.0 * pad).max(1.0);
    let m_per_svg = site_ew_m / usable;
    (map_w / z) * m_per_svg
}

fn format_zoom_hud(zoom: f64, view_m: f64) -> String {
    let z = zoom;
    let z_s = if z >= 100.0 {
        format!("ร—{z:.0}")
    } else if z >= 10.0 {
        format!("ร—{z:.1}")
    } else {
        format!("ร—{z:.2}")
    };
    let scale = if view_m >= 100.0 {
        format!("~{view_m:.0} m")
    } else if view_m >= 10.0 {
        format!("~{view_m:.1} m")
    } else if view_m >= 1.0 {
        format!("~{view_m:.2} m")
    } else {
        format!("~{:.0} cm", view_m * 100.0)
    };
    format!("{z_s} ยท {scale}")
}

/// Map client point (relative to wrap) โ†’ world/SVG coords under current camera.
fn client_to_world(
    cx: f64,
    cy: f64,
    wrap_w: f64,
    wrap_h: f64,
    center: (f64, f64),
    zoom: f64,
    w: f64,
    h: f64,
) -> (f64, f64) {
    let z = zoom.max(0.25);
    let vw = w / z;
    let vh = h / z;
    let left = center.0 - vw * 0.5;
    let top = center.1 - vh * 0.5;
    // meet: uniform scale of viewBox into wrap
    let fit = (wrap_w / vw).min(wrap_h / vh);
    let ox = (wrap_w - vw * fit) * 0.5;
    let oy = (wrap_h - vh * fit) * 0.5;
    let wx = left + (cx - ox) / fit;
    let wy = top + (cy - oy) / fit;
    (wx, wy)
}

fn centroid(coords: &span>; 2) -> (f64, f64) {
    if coords.is_empty() {
        return (0.0, 0.0);
    }
    let n = coords.len() as f64;
    let lon = coords.iter().map(|c| c[0]).sum::<f64>() / n;
    let lat = coords.iter().map(|c| c[1]).sum::<f64>() / n;
    (lon, lat)
}

fn status_color(s: &str) -> &'static str {
    match s {
        "idle" => "var(--cyber-green)",
        "busy" => "var(--cyber-yellow)",
        _ => "#555",
    }
}

/// Ops map โ€” fleets rail + flats.
#[component]
pub fn ValleyConsole() -> impl IntoView {
    view! { <MapConsole domains_board=false /> }
}

/// Domains board โ€” same camera, left rail = 21 cybics (draggable shill points).
#[component]
pub fn DomainsBoard() -> impl IntoView {
    view! { <MapConsole domains_board=true /> }
}

#[component]
fn MapConsole(domains_board: bool) -> impl IntoView {
    let map = load_map();
    let map = std::sync::Arc::new(map);
    let map_bbox = map.bbox.clone();
    let base_domains = map.domains.clone();

    let selected_flat = RwSignal::new({
        let from_q = web_sys::window()
            .and_then(|w| w.location().search().ok())
            .and_then(|s| {
                let s = s.trim_start_matches('?');
                s.split('&').find_map(|pair| {
                    let mut it = pair.splitn(2, '=');
                    let k = it.next()?;
                    let v = it.next().unwrap_or("");
                    if k == "plot" && !v.is_empty() {
                        // plot ids are ascii (core-0, sinwood-1-laba, โ€ฆ)
                        Some(v.replace('+', " "))
                    } else {
                        None
                    }
                })
            });
        from_q
            .filter(|id| map.phase0.iter().any(|f| f.id == *id))
            .or_else(|| map.phase0.first().map(|f| f.id.clone()))
            .or_else(|| Some("sinwood".into()))
    });
    let selected_fleet = RwSignal::new(Some("w-sutar".to_string()));
    let selected_action = RwSignal::new("survey".to_string());
    let intents = RwSignal::new(load_intents());
    let next_id = RwSignal::new(
        load_intents()
            .iter()
            .map(|i| i.id)
            .max()
            .unwrap_or(0)
            .saturating_add(1),
    );
    let owned = RwSignal::new(load_owned());
    let leased = RwSignal::new(
        load_leases()
            .into_iter()
            .map(|l| l.flat_id)
            .collect::<Vec<String>>(),
    );
    let flats = RwSignal::new(map.phase0.clone()); // live land geometry (split/merge)
    let sheet = RwSignal::new(Sheet::None);
    let buy_pick = RwSignal::new("cat-eye".to_string());
    let robot_serial = RwSignal::new({
        let max = load_owned()
            .iter()
            .filter_map(|r| {
                r.name
                    .rsplit('-')
                    .next()
                    .and_then(|s| s.parse::<u32>().ok())
            })
            .max()
            .unwrap_or(0);
        max + 1
    });
    let merge_pick = RwSignal::new(None::<String>); // second flat for merge
                                                    // split controls: axis 0 = E/W (vertical line), 1 = N/S (horizontal line)
    let split_axis = RwSignal::new(0usize);
    let split_ratio = RwSignal::new(0.50_f64); // 0..1 across bbox

    // left rail: fleets (ops) or domains (shill points)
    let left_board = RwSignal::new(if domains_board {
        LeftBoard::Domains
    } else {
        LeftBoard::Fleets
    });
    let domains = RwSignal::new(apply_domain_positions(base_domains.clone()));
    let selected_domain = RwSignal::new(
        domains
            .get_untracked()
            .first()
            .map(|d| d.id.clone())
            .or_else(|| Some("domain-ai".into())),
    );
    // live domain drag (id) โ€” positions persist to localStorage
    let domain_drag = RwSignal::new(None::<String>);
    let domain_did_drag = RwSignal::new(false);

    // game map camera โ€” SVG viewBox zoom/pan (vector-sharp; never CSS scale)
    // max zoom high enough for ~10 cm tile/hex planning on the ground
    const MAP_W: f64 = 960.0;
    const MAP_H: f64 = 720.0;
    const MAP_PAD: f64 = 20.0;
    let site_ew_m = site_span_m(&map.bbox).0;
    let map_zoom = RwSignal::new(1.0_f64);
    let map_center = RwSignal::new((MAP_W * 0.5, MAP_H * 0.5)); // world/SVG units
    let map_hover = RwSignal::new(None::<(String, String, f64)>); // id, label, m2
    let map_dragging = RwSignal::new(false);
    // true once pointer moved past threshold โ€” suppress plot-click select after pan
    let map_did_drag = RwSignal::new(false);
    let map_drag_last = RwSignal::new((0.0_f64, 0.0_f64));
    let map_wrap_ref = NodeRef::<leptos::html::Div>::new();

    // focus camera on a domain id (used from list cards)

    let zoom_by = move |factor: f64| {
        map_zoom.update(|z| *z = (*z * factor).clamp(MAP_ZOOM_MIN, MAP_ZOOM_MAX));
    };
    let zoom_at = move |factor: f64, cx: f64, cy: f64| {
        // keep world point under cursor stable while viewBox zooms
        let z0 = map_zoom.get_untracked();
        let z1 = (z0 * factor).clamp(MAP_ZOOM_MIN, MAP_ZOOM_MAX);
        if (z1 - z0).abs() < 1e-9 {
            return;
        }
        let c0 = map_center.get_untracked();
        let (ww, wh) = if let Some(el) = map_wrap_ref.get_untracked() {
            let r = el.get_bounding_client_rect();
            (r.width(), r.height())
        } else {
            (MAP_W, MAP_H)
        };
        let (wx, wy) = client_to_world(cx, cy, ww, wh, c0, z0, MAP_W, MAP_H);
        let vw1 = MAP_W / z1;
        let vh1 = MAP_H / z1;
        let fit1 = (ww / vw1).min(wh / vh1);
        let ox1 = (ww - vw1 * fit1) * 0.5;
        let oy1 = (wh - vh1 * fit1) * 0.5;
        let left1 = wx - (cx - ox1) / fit1;
        let top1 = wy - (cy - oy1) / fit1;
        let c1 = (left1 + vw1 * 0.5, top1 + vh1 * 0.5);
        map_zoom.set(z1);
        map_center.set(c1);
    };
    let pan_by = move |dx_screen: f64, dy_screen: f64| {
        let z = map_zoom.get_untracked().max(0.25);
        let (ww, wh) = if let Some(el) = map_wrap_ref.get_untracked() {
            let r = el.get_bounding_client_rect();
            (r.width(), r.height())
        } else {
            (MAP_W, MAP_H)
        };
        let vw = MAP_W / z;
        let vh = MAP_H / z;
        let fit = (ww / vw).min(wh / vh).max(1e-9);
        map_center.update(|(cx, cy)| {
            *cx -= dx_screen / fit;
            *cy -= dy_screen / fit;
        });
    };
    let reset_cam = move || {
        map_zoom.set(1.0);
        map_center.set((MAP_W * 0.5, MAP_H * 0.5));
    };

    Effect::new(move |_| {
        let title = if left_board.get() == LeftBoard::Domains {
            "Cyberia โ€” domains ยท Gesing, Bali"
        } else {
            "Cyberia โ€” map ยท Gesing, Bali"
        };
        document().set_title(title);
    });

    let map_for_svg = map.clone();
    let nav_active = if domains_board { "domains" } else { "map" };
    // split clones so left-rail closure and map markers don't fight over ownership
    let base_domains_rail = base_domains.clone();
    let base_domains_map = base_domains.clone();
    let map_bbox_rail = map_bbox.clone();
    let map_bbox_drag = map_bbox.clone();

    view! {
        <div class="page-shell cyberia-shell">
            <div class="site-chrome cyberia-chrome">
            <div class="chrome-inner">
            <div class="header-row1">
                <div class="logo-zone">
                    <h1 class="logo">
                        <a href="/cities" class="brand-flag" title="home" inner_html=FLAG_SVG></a>
                        <span style="color: var(--cyber-green);">"cyber"</span>
                        <span style="color: var(--cyber-green); margin: 0 1px;">"โ€ข"</span>
                        <span style="color: #fff;">"ia"</span>
                    </h1>
                </div>
                <div class="cyberia-phase-pill">
                    <span class="phase-dot"></span>
                    {move || {
                        if left_board.get() == LeftBoard::Domains {
                            let n = domains.get().len();
                            format!("21 CYBICS ยท {n} DOMAINS ยท DRAG")
                        } else {
                            let n = flats.get().len();
                            format!("PHASE 0 ยท {n} PLOTS ยท GESING")
                        }
                    }}
                </div>
                <CyberiaNav active=nav_active />
            </div>
            </div>
            </div>

            <div class="cyberia-stage">
                // LEFT โ€” FLEETS or DOMAINS
                <section class="cyberia-panel cyberia-fleets">
                    <div class="cyberia-panel-h">
                        <div class="board-tabs">
                            <a
                                class=move || if left_board.get() == LeftBoard::Fleets { "board-tab on" } else { "board-tab" }
                                href="/map"
                                on:click=move |ev| {
                                    if !domains_board {
                                        ev.prevent_default();
                                        left_board.set(LeftBoard::Fleets);
                                    }
                                }
                            >"FLEETS"</a>
                            <a
                                class=move || if left_board.get() == LeftBoard::Domains { "board-tab on" } else { "board-tab" }
                                href="/domains"
                                on:click=move |ev| {
                                    if domains_board {
                                        ev.prevent_default();
                                        left_board.set(LeftBoard::Domains);
                                    }
                                }
                            >"DOMAINS"</a>
                        </div>
                        <span class="panel-sub">
                            {move || match left_board.get() {
                                LeftBoard::Fleets => format!("{} workers ยท {} machines", WORKERS.len(), MACHINES.len()),
                                LeftBoard::Domains => {
                                    let n = domains.get().len();
                                    format!("{n} cybics ยท drag on map")
                                }
                            }}
                        </span>
                    </div>
                    <div class="fleet-list">
                    {move || match left_board.get() {
                        LeftBoard::Domains => {
                            let list = domains.get();
                            let baked_reset = base_domains_rail.clone();
                            let bbox_list = map_bbox_rail.clone();
                            // group by triad โ€” question once per group, not on every card
                            let mut by_triad: HashMap<String, Vec<DomainMark>> = HashMap::new();
                            for d in list {
                                by_triad.entry(d.triad.clone()).or_default().push(d);
                            }
                            // stable order inside group: name alpha
                            for v in by_triad.values_mut() {
                                v.sort_by(|a, b| a.name.cmp(&b.name));
                            }
                            let mut groups: Vec<(String, Vec<DomainMark>)> = TRIAD_ORDER
                                .iter()
                                .filter_map(|t| {
                                    by_triad
                                        .remove(*t)
                                        .map(|v| ((*t).to_string(), v))
                                })
                                .collect();
                            // any unknown triad tail
                            let mut rest: Vec<_> = by_triad.into_iter().collect();
                            rest.sort_by(|a, b| a.0.cmp(&b.0));
                            groups.extend(rest);

                            view! {
                                <div class="fleet-section">"CYBICS ยท BY TRIAD"</div>
                                <div class="domain-board-actions">
                                    <button
                                        class="act-pill"
                                        type="button"
                                        title="reset domain positions to baked map"
                                        on:click=move |_| {
                                            clear_domain_positions();
                                            domains.set(baked_reset.clone());
                                        }
                                    >"RESET"</button>
                                    <button
                                        class="act-pill"
                                        type="button"
                                        title="log lon/lat JSON overrides to browser console"
                                        on:click=move |_| {
                                            let list = domains.get_untracked();
                                            let mut pos = HashMap::new();
                                            for d in &list {
                                                if let Some(c) = d.coords.first() {
                                                    pos.insert(d.name.clone(), [c[0], c[1]]);
                                                }
                                            }
                                            let s = serde_json::to_string_pretty(&pos).unwrap_or_else(|_| "{}".into());
                                            web_sys::console::log_1(&s.into());
                                            if let Some(w) = web_sys::window() {
                                                let _ = w.alert_with_message(
                                                    "Domain positions logged to console (F12).\nAlso saved in localStorage.",
                                                );
                                            }
                                        }
                                    >"EXPORT"</button>
                                </div>
                                {groups.into_iter().map(|(triad, doms)| {
                                    let color = triad_color(&triad);
                                    let q = triad_question(&triad);
                                    let triad_up = triad.to_uppercase();
                                    let n = doms.len();
                                    let bbox_group = bbox_list.clone();
                                    view! {
                                        <div class="domain-triad-group" style=format!("--dom-color: {color}")>
                                            <div class="domain-triad-h">
                                                <span class="domain-triad-name" style:color=color>{triad_up}</span>
                                                <span class="domain-triad-q">{q}</span>
                                                <span class="domain-triad-n">{format!("{n}")}</span>
                                            </div>
                                            <div class="domain-triad-cards">
                                                {doms.into_iter().map(|dom| {
                                                    let id = dom.id.clone();
                                                    let id2 = id.clone();
                                                    let name = dom.name.clone();
                                                    let district = if dom.district.is_empty() {
                                                        dom.zone.clone()
                                                    } else {
                                                        dom.district.clone()
                                                    };
                                                    let color = triad_color(&dom.triad);
                                                    let bbox_focus = bbox_group.clone();
                                                    view! {
                                                        <button
                                                            class=move || {
                                                                let sel = selected_domain.get().as_deref() == Some(id.as_str());
                                                                format!("fleet-card domain-card domain-card-compact{}", if sel { " sel" } else { "" })
                                                            }
                                                            style=format!("--dom-color: {color}")
                                                            on:click=move |_| {
                                                                selected_domain.set(Some(id2.clone()));
                                                                if let Some(dom) = domains
                                                                    .get_untracked()
                                                                    .into_iter()
                                                                    .find(|d| d.id == id2)
                                                                {
                                                                    if let Some(c) = dom.coords.first() {
                                                                        let (x, y) = project(
                                                                            c[0],
                                                                            c[1],
                                                                            &bbox_focus,
                                                                            MAP_W,
                                                                            MAP_H,
                                                                            MAP_PAD,
                                                                        );
                                                                        map_center.set((x, y));
                                                                        if map_zoom.get_untracked() < 3.0 {
                                                                            map_zoom.set(4.0);
                                                                        }
                                                                    }
                                                                }
                                                            }
                                                        >
                                                            <div class="fleet-top">
                                                                <span class="fleet-name" style:color=color>{name.to_uppercase()}</span>
                                                                <span class="fleet-status">{district.to_uppercase()}</span>
                                                            </div>
                                                        </button>
                                                    }
                                                }).collect_view()}
                                            </div>
                                        </div>
                                    }
                                }).collect_view()}
                            }.into_any()
                        }
                        LeftBoard::Fleets => view! {
                        <div class="fleet-section">"WORKERS ยท HARD FORCE"</div>
                        {WORKERS.iter().map(|f| render_fleet_card(f, selected_fleet)).collect_view()}
                        <div class="fleet-section">"MACHINES"</div>
                        {MACHINES.iter().map(|f| render_fleet_card(f, selected_fleet)).collect_view()}
                        // robots you bought this session
                        {move || owned.get().into_iter().map(|r| {
                            let id = r.id.clone();
                            let id2 = id.clone();
                            let kind_worker = r.kind == "worker";
                            let name = r.name.clone();
                            let role = r.role.clone();
                            let kind = r.kind.clone();
                            view! {
                                <button
                                    class=move || {
                                        let sel = selected_fleet.get().as_deref() == Some(id.as_str());
                                        format!(
                                            "fleet-card owned{}{}",
                                            if sel { " sel" } else { "" },
                                            if kind_worker { " worker" } else { " machine" },
                                        )
                                    }
                                    on:click=move |_| selected_fleet.set(Some(id2.clone()))
                                >
                                    <div class="fleet-top">
                                        <span class="fleet-name">{name}</span>
                                        <span class="fleet-status" style:color=status_color("idle")>"OWNED"</span>
                                    </div>
                                    <div class="fleet-role">{role}</div>
                                    <div class="fleet-meta">
                                        <span>{kind.to_uppercase()}</span>
                                        <span>"BOUGHT"</span>
                                    </div>
                                </button>
                            }
                        }).collect_view()}
                        }.into_any()
                    }}
                    </div>
                    <div class="cyberia-hint">
                        {move || match left_board.get() {
                            LeftBoard::Domains => "Select a domain ยท drag its glow on the map ยท positions save in this browser",
                            LeftBoard::Fleets => "Pick a fleet unit, a flat on the map, an action โ€” then commit intent. Or buy a robot / lease land.",
                        }}
                    </div>
                </section>

                // CENTER โ€” FLATS MAP (game camera: static viewport, wheel/keys zoom, drag pan)
                <section class="cyberia-panel cyberia-flats">
                    <div class="cyberia-panel-h">
                        <span class="panel-kicker">"FLATS"</span>
                        <span class="panel-sub">
                            {format!(
                                "{} plots ยท {:.1} ha plots ยท {:.0} ha site ยท scroll zoom",
                                map.stats.plot_count.max(map.phase0.len() as u32),
                                map.stats.plot_ha,
                                map.stats.district_ha,
                            )}
                        </span>
                        <span class="map-zoom-readout" title="zoom ยท visible width (game scale)">
                            {move || {
                                let z = map_zoom.get();
                                let m = view_width_m(z, MAP_W, MAP_PAD, site_ew_m);
                                format_zoom_hud(z, m)
                            }}
                        </span>
                    </div>
                    <div
                        class="flat-map-wrap game-map"
                        node_ref=map_wrap_ref
                        tabindex="0"
                        on:wheel=move |ev| {
                            ev.prevent_default();
                            ev.stop_propagation();
                            let dy = ev.delta_y();
                            if dy == 0.0 { return; }
                            // slightly faster step so deep game zoom (ร—1000) is reachable
                            let factor = if dy < 0.0 { 1.18 } else { 1.0 / 1.18 };
                            if let Some(el) = map_wrap_ref.get_untracked() {
                                let rect = el.get_bounding_client_rect();
                                let cx = ev.client_x() as f64 - rect.left();
                                let cy = ev.client_y() as f64 - rect.top();
                                zoom_at(factor, cx, cy);
                            } else {
                                zoom_by(factor);
                            }
                        }
                        on:keydown=move |ev| {
                            let key = ev.key();
                            let step = 36.0;
                            match key.as_str() {
                                "ArrowLeft" | "a" | "A" => { ev.prevent_default(); pan_by(step, 0.0); }
                                "ArrowRight" | "d" | "D" => { ev.prevent_default(); pan_by(-step, 0.0); }
                                "ArrowUp" | "w" | "W" => { ev.prevent_default(); pan_by(0.0, step); }
                                "ArrowDown" | "s" | "S" => { ev.prevent_default(); pan_by(0.0, -step); }
                                "+" | "=" => { ev.prevent_default(); zoom_by(1.25); }
                                "-" | "_" => { ev.prevent_default(); zoom_by(1.0 / 1.25); }
                                "0" => { ev.prevent_default(); reset_cam(); }
                                _ => {}
                            }
                        }
                        on:pointerdown=move |ev| {
                            // pan from anywhere on the map (plots, empty, places) โ€”
                            // filled flats used to block drag; click still selects if no pan
                            // domain-dot starts its own drag via stop_propagation
                            if ev.button() != 0 { return; }
                            if domain_drag.get_untracked().is_some() { return; }
                            let target = ev.target().and_then(|t| t.dyn_into::<web_sys::Element>().ok());
                            let on_ui = target
                                .as_ref()
                                .and_then(|el| el.closest(".map-zoom-rail, .map-cam-btn, .domain-dot").ok().flatten())
                                .is_some();
                            if on_ui {
                                return;
                            }
                            map_dragging.set(true);
                            map_did_drag.set(false);
                            map_drag_last.set((ev.client_x() as f64, ev.client_y() as f64));
                            if let Some(el) = map_wrap_ref.get_untracked() {
                                let _ = el.set_pointer_capture(ev.pointer_id());
                            }
                        }
                        on:pointermove=move |ev| {
                            // โ€”โ€” domain drag (shill points) โ€”โ€”
                            if let Some(dom_id) = domain_drag.get_untracked() {
                                let (ww, wh) = if let Some(el) = map_wrap_ref.get_untracked() {
                                    let r = el.get_bounding_client_rect();
                                    (r.width(), r.height())
                                } else {
                                    (MAP_W, MAP_H)
                                };
                                let rect = map_wrap_ref
                                    .get_untracked()
                                    .map(|el| el.get_bounding_client_rect());
                                let (cx, cy) = if let Some(r) = rect {
                                    (
                                        ev.client_x() as f64 - r.left(),
                                        ev.client_y() as f64 - r.top(),
                                    )
                                } else {
                                    (ev.client_x() as f64, ev.client_y() as f64)
                                };
                                let (lx, ly) = map_drag_last.get_untracked();
                                let dx = (ev.client_x() as f64) - lx;
                                let dy = (ev.client_y() as f64) - ly;
                                if !domain_did_drag.get_untracked() {
                                    if dx * dx + dy * dy < 16.0 {
                                        return;
                                    }
                                    domain_did_drag.set(true);
                                }
                                let (wx, wy) = client_to_world(
                                    cx,
                                    cy,
                                    ww,
                                    wh,
                                    map_center.get_untracked(),
                                    map_zoom.get_untracked(),
                                    MAP_W,
                                    MAP_H,
                                );
                                let (lon, lat) = unproject(wx, wy, &map_bbox_drag, MAP_W, MAP_H, MAP_PAD);
                                domains.update(|list| {
                                    if let Some(d) = list.iter_mut().find(|d| d.id == dom_id) {
                                        d.coords = vec![[lon, lat]];
                                    }
                                });
                                map_drag_last.set((ev.client_x() as f64, ev.client_y() as f64));
                                return;
                            }

                            if !map_dragging.get_untracked() { return; }
                            let (lx, ly) = map_drag_last.get_untracked();
                            let cx = ev.client_x() as f64;
                            let cy = ev.client_y() as f64;
                            let dx = cx - lx;
                            let dy = cy - ly;
                            // ~5px threshold so a tap still counts as plot select
                            if !map_did_drag.get_untracked() {
                                if dx * dx + dy * dy < 25.0 {
                                    return;
                                }
                                map_did_drag.set(true);
                            }
                            pan_by(dx, dy);
                            map_drag_last.set((cx, cy));
                        }
                        on:pointerup=move |_| {
                            if domain_drag.get_untracked().is_some() {
                                if domain_did_drag.get_untracked() {
                                    save_domain_positions(&domains.get_untracked());
                                }
                                domain_drag.set(None);
                                domain_did_drag.set(false);
                            }
                            map_dragging.set(false);
                        }
                        on:pointercancel=move |_| {
                            domain_drag.set(None);
                            domain_did_drag.set(false);
                            map_dragging.set(false);
                            map_did_drag.set(false);
                        }
                        on:pointerleave=move |_| {
                            if !map_dragging.get_untracked() {
                                map_hover.set(None);
                            }
                        }
                    >
                        // left zoom rail (game-style) โ€” deep zoom for cm-scale tile planning
                        <div class="map-zoom-rail" aria-label="map zoom">
                            <button class="map-cam-btn" type="button" title="zoom in (to ~10cm tile scale)"
                                on:click=move |ev| { ev.stop_propagation(); zoom_by(1.35); }
                            >"+"</button>
                            <button class="map-cam-btn" type="button" title="zoom out"
                                on:click=move |ev| { ev.stop_propagation(); zoom_by(1.0 / 1.35); }
                            >"โˆ’"</button>
                            <button class="map-cam-btn" type="button" title="reset view"
                                on:click=move |ev| { ev.stop_propagation(); reset_cam(); }
                            >"โŒ‚"</button>
                            <div class="map-cam-sep"></div>
                            <button class="map-cam-btn" type="button" title="pan left"
                                on:click=move |ev| { ev.stop_propagation(); pan_by(48.0, 0.0); }
                            >"โ†"</button>
                            <button class="map-cam-btn" type="button" title="pan right"
                                on:click=move |ev| { ev.stop_propagation(); pan_by(-48.0, 0.0); }
                            >"โ†’"</button>
                            <button class="map-cam-btn" type="button" title="pan up"
                                on:click=move |ev| { ev.stop_propagation(); pan_by(0.0, 48.0); }
                            >"โ†‘"</button>
                            <button class="map-cam-btn" type="button" title="pan down"
                                on:click=move |ev| { ev.stop_propagation(); pan_by(0.0, -48.0); }
                            >"โ†“"</button>
                        </div>

                        {
                            let m = map_for_svg.clone();
                            let m_places = map_for_svg.clone();
                            let m_cap = map_for_svg.clone();
                            let m_districts = map_for_svg.clone();
                            const W: f64 = 960.0;
                            const H: f64 = 720.0;
                            const PAD: f64 = 20.0;
                            view! {
                                <div class="map-world">
                                <svg
                                    class="flat-map"
                                    viewBox=move || map_view_box(map_center.get(), map_zoom.get(), W, H)
                                    preserveAspectRatio="xMidYMid meet"
                                >
                                    // full world backdrop (fixed world bounds)
                                    <rect x="0" y="0" width=W height=H fill="#070707" />
                                    {(0..12).map(|i| {
                                        let x = PAD + i as f64 * (W - 2.0*PAD) / 11.0;
                                        let y2 = H - PAD;
                                        view! {
                                            <line x1=x y1=PAD x2=x y2=y2 stroke="#121212" stroke-width="1" />
                                        }
                                    }).collect_view()}
                                    {(0..9).map(|i| {
                                        let y = PAD + i as f64 * (H - 2.0*PAD) / 8.0;
                                        let x2 = W - PAD;
                                        view! {
                                            <line x1=PAD y1=y x2=x2 y2=y stroke="#121212" stroke-width="1" />
                                        }
                                    }).collect_view()}

                                    // district outlines โ€” full site ~37 ha envelope
                                    {m_districts.districts.iter().map(|d| {
                                        let path = poly_path(&d.coords, &m_districts.bbox, W, H, PAD);
                                        let (clon, clat) = centroid(&d.coords);
                                        let (lx, ly) = project(clon, clat, &m_districts.bbox, W, H, PAD);
                                        let label = d.name.to_uppercase();
                                        view! {
                                            <g class="district-poly" pointer-events="none">
                                                <path
                                                    d=path
                                                    fill="rgba(255,255,255,0.015)"
                                                    stroke="rgba(255,255,255,0.12)"
                                                    stroke-width="1.25"
                                                    stroke-dasharray="4 3"
                                                    vector-effect="non-scaling-stroke"
                                                />
                                                <text
                                                    class="district-label"
                                                    text-anchor="middle"
                                                    dominant-baseline="middle"
                                                    transform=move || screen_stable_tf(lx, ly, map_zoom.get())
                                                >{label.clone()}</text>
                                            </g>
                                        }
                                    }).collect_view()}

                                    // ALL plots (interactive)
                                    {move || {
                                        let list = flats.get();
                                        list.into_iter().map(|flat| {
                                            let id_click = flat.id.clone();
                                            let id_fill = flat.id.clone();
                                            let id_stroke = flat.id.clone();
                                            let id_sw = flat.id.clone();
                                            let id_sw2 = flat.id.clone();
                                            let id_cls = flat.id.clone();
                                            let id_lab = flat.id.clone();
                                            let id_hov = flat.id.clone();
                                            let id_hov2 = flat.id.clone();
                                            let zkey = zone_key(&flat).to_string();
                                            let z_fill = zkey.clone();
                                            let z_stroke = zkey.clone();
                                            let z_hov = zkey.clone();
                                            let label_hov = flat.name.to_uppercase();
                                            let area = area_m2(&flat.coords);
                                            let d = poly_path(&flat.coords, &m.bbox, W, H, PAD);
                                            let (clon, clat) = centroid(&flat.coords);
                                            let (lx, ly) = project(clon, clat, &m.bbox, W, H, PAD);
                                            // labels only on selected plot (click) โ€” hover uses HUD tooltip
                                            let base_label = flat.name.to_uppercase();
                                            let d_under = d.clone();
                                            view! {
                                                <g class="flat-poly"
                                                    on:click=move |ev| {
                                                        // after a pan, browser still fires click โ€” ignore it
                                                        if map_did_drag.get_untracked() {
                                                            map_did_drag.set(false);
                                                            ev.stop_propagation();
                                                            return;
                                                        }
                                                        ev.stop_propagation();
                                                        selected_flat.set(Some(id_click.clone()));
                                                    }
                                                    on:pointerenter=move |_| {
                                                        map_hover.set(Some((id_hov.clone(), label_hov.clone(), area)));
                                                    }
                                                    on:pointerleave=move |_| {
                                                        map_hover.update(|h| {
                                                            if h.as_ref().map(|t| t.0.as_str()) == Some(id_hov2.as_str()) {
                                                                *h = None;
                                                            }
                                                        });
                                                    }
                                                >
                                                    // dark under-stroke: thin seam idle, wide halo on hover/select
                                                    <path
                                                        d=d_under
                                                        fill="none"
                                                        stroke="#000000"
                                                        stroke-width=move || {
                                                            let sel = selected_flat.get().as_deref() == Some(id_sw2.as_str());
                                                            let hov = map_hover.get().as_ref().map(|t| t.0.as_str()) == Some(id_sw2.as_str());
                                                            // non-scaling-stroke: screen px, constant under viewBox zoom
                                                            if sel { "4.0" } else if hov { "3.4" } else { "1.35" }
                                                        }
                                                        stroke-linejoin="round"
                                                        stroke-linecap="round"
                                                        vector-effect="non-scaling-stroke"
                                                        class="flat-path-under"
                                                        pointer-events="none"
                                                    />
                                                    <path
                                                        d=d
                                                        fill=move || {
                                                            let sel = selected_flat.get().as_deref() == Some(id_fill.as_str());
                                                            let hov = map_hover.get().as_ref().map(|t| t.0.as_str()) == Some(id_fill.as_str());
                                                            if hov && !sel {
                                                                zone_hover_fill(&z_hov).to_string()
                                                            } else {
                                                                flat_fill(&z_fill, sel).to_string()
                                                            }
                                                        }
                                                        stroke=move || {
                                                            let sel = selected_flat.get().as_deref() == Some(id_stroke.as_str());
                                                            let hov = map_hover.get().as_ref().map(|t| t.0.as_str()) == Some(id_stroke.as_str());
                                                            flat_stroke_col(&z_stroke, sel, hov).to_string()
                                                        }
                                                        stroke-width=move || {
                                                            let sel = selected_flat.get().as_deref() == Some(id_sw.as_str());
                                                            let hov = map_hover.get().as_ref().map(|t| t.0.as_str()) == Some(id_sw.as_str());
                                                            // glow only when hot โ€” idle is a hairline seam
                                                            if sel { "2.2" } else if hov { "1.9" } else { "0.75" }
                                                        }
                                                        stroke-linejoin="round"
                                                        stroke-linecap="round"
                                                        paint-order="stroke fill"
                                                        vector-effect="non-scaling-stroke"
                                                        class=move || {
                                                            let sel = selected_flat.get().as_deref() == Some(id_cls.as_str());
                                                            let hov = map_hover.get().as_ref().map(|t| t.0.as_str()) == Some(id_cls.as_str());
                                                            format!(
                                                                "flat-path{}{}",
                                                                if sel { " is-sel" } else { "" },
                                                                if hov { " is-hov" } else { "" },
                                                            )
                                                        }
                                                    />
                                                    {move || {
                                                        let sel = selected_flat.get().as_deref() == Some(id_lab.as_str());
                                                        if !sel {
                                                            return view! { <g></g> }.into_any();
                                                        }
                                                        let text = if leased.get().iter().any(|x| x == &id_lab) {
                                                            format!("{base_label} ยท LEASED")
                                                        } else {
                                                            base_label.clone()
                                                        };
                                                        view! {
                                                            <text
                                                                class="flat-label"
                                                                text-anchor="middle"
                                                                dominant-baseline="middle"
                                                                transform=move || screen_stable_tf(lx, ly, map_zoom.get())
                                                            >{text}</text>
                                                        }.into_any()
                                                    }}
                                                </g>
                                            }
                                        }).collect_view()
                                    }}

                                    {m_places.places.iter().map(|p| {
                                        if p.coords.is_empty() { return view! { <g></g> }.into_any(); }
                                        let (x, y) = project(p.coords[0][0], p.coords[0][1], &m_places.bbox, W, H, PAD);
                                        let name = p.name.clone();
                                        view! {
                                            // dots + names stay screen-stable under camera zoom
                                            <g
                                                class="place-dot"
                                                transform=move || screen_stable_tf(x, y, map_zoom.get())
                                            >
                                                <circle cx="0" cy="0" r="3" fill="#3a3a3a" stroke="#777" stroke-width="0.9" />
                                                <text x="0" y="-7" text-anchor="middle" class="place-label">{name}</text>
                                            </g>
                                        }.into_any()
                                    }).collect_view()}

                                    // 21 cybics domain shill points โ€” draggable; node graph is stable (pos from signal)
                                    {
                                        let bbox = m_places.bbox.clone();
                                        // meta from baked list (ids/names fixed); coords live in `domains` signal
                                        let domain_meta = base_domains_map.clone();
                                        domain_meta.iter().map(|dom| {
                                        let id = dom.id.clone();
                                        let id_cls = id.clone();
                                        let id_tf = id.clone();
                                        let id_down = id.clone();
                                        let id_r = id.clone();
                                        let id_sw = id.clone();
                                        let name = dom.name.clone();
                                        let triad = dom.triad.clone();
                                        let q = dom.question.clone();
                                        let district = if dom.district.is_empty() {
                                            dom.zone.clone()
                                        } else {
                                            dom.district.clone()
                                        };
                                        let color = triad_color(&dom.triad);
                                        let title = format!(
                                            "{} ยท {} ยท {}\n{} ยท drag to move",
                                            triad.to_uppercase(),
                                            name,
                                            district.to_uppercase(),
                                            q
                                        );
                                        let bbox2 = bbox.clone();
                                        view! {
                                            <g
                                                class=move || {
                                                    let sel = selected_domain.get().as_deref() == Some(id_cls.as_str());
                                                    let dragging = domain_drag.get().as_deref() == Some(id_cls.as_str());
                                                    format!(
                                                        "domain-dot{}{}",
                                                        if sel { " is-sel" } else { "" },
                                                        if dragging { " is-drag" } else { "" },
                                                    )
                                                }
                                                transform=move || {
                                                    let (x, y) = domains
                                                        .get()
                                                        .into_iter()
                                                        .find(|d| d.id == id_tf)
                                                        .and_then(|d| d.coords.first().copied())
                                                        .map(|c| project(c[0], c[1], &bbox2, W, H, PAD))
                                                        .unwrap_or((0.0, 0.0));
                                                    screen_stable_tf(x, y, map_zoom.get())
                                                }
                                                on:pointerdown=move |ev| {
                                                    ev.stop_propagation();
                                                    ev.prevent_default();
                                                    if ev.button() != 0 { return; }
                                                    selected_domain.set(Some(id_down.clone()));
                                                    domain_drag.set(Some(id_down.clone()));
                                                    domain_did_drag.set(false);
                                                    map_dragging.set(false);
                                                    map_drag_last.set((ev.client_x() as f64, ev.client_y() as f64));
                                                    if let Some(el) = map_wrap_ref.get_untracked() {
                                                        let _ = el.set_pointer_capture(ev.pointer_id());
                                                    }
                                                }
                                            >
                                                <title>{title.clone()}</title>
                                                <circle
                                                    cx="0"
                                                    cy="0"
                                                    r="10"
                                                    fill="transparent"
                                                    class="domain-hit"
                                                />
                                                <circle
                                                    cx="0"
                                                    cy="0"
                                                    r=move || {
                                                        let sel = selected_domain.get().as_deref() == Some(id_r.as_str());
                                                        if sel { "7.5" } else { "5.5" }
                                                    }
                                                    fill=color
                                                    fill-opacity="0.22"
                                                    stroke=color
                                                    stroke-width=move || {
                                                        let sel = selected_domain.get().as_deref() == Some(id_sw.as_str());
                                                        if sel { "2.0" } else { "1.4" }
                                                    }
                                                    class="domain-ring"
                                                />
                                                <circle
                                                    cx="0"
                                                    cy="0"
                                                    r="2.4"
                                                    fill=color
                                                    class="domain-core"
                                                />
                                                <text
                                                    x="0"
                                                    y="-9"
                                                    text-anchor="middle"
                                                    class="domain-label"
                                                    fill=color
                                                >{name.clone()}</text>
                                            </g>
                                        }
                                        .into_any()
                                    }).collect_view()
                                    }

                                    <text
                                        class="map-caption"
                                        text-anchor="start"
                                        transform=move || screen_stable_tf(PAD, H - 10.0, map_zoom.get())
                                    >
                                        {format!(
                                            "N โ†‘  ยท  {} plots  ยท  {:.1} ha plots / {:.0} ha site  ยท  21 domains",
                                            m_cap.stats.plot_count,
                                            m_cap.stats.plot_ha,
                                            m_cap.stats.district_ha,
                                        )}
                                    </text>
                                </svg>
                                </div>
                            }
                        }

                        // hover tooltip (game HUD)
                        {move || map_hover.get().map(|(id, label, m2)| {
                            let leased_tag = if leased.get().iter().any(|x| x == &id) { " ยท LEASED" } else { "" };
                            let sel = selected_flat.get().as_deref() == Some(id.as_str());
                            view! {
                                <div class="map-tooltip">
                                    <div class="mt-name">{format!("{label}{leased_tag}")}</div>
                                    <div class="mt-area">{fmt_area_m2(m2)}</div>
                                    <div class="mt-hint">{if sel { "SELECTED" } else { "click to select" }}</div>
                                </div>
                            }
                        })}

                        <div class="map-hud-hint">
                            {move || if left_board.get() == LeftBoard::Domains {
                                "drag domain glows to place ยท pan empty space ยท scroll zoom ยท saved in browser"
                            } else {
                                "drag pan ยท scroll zoom ยท drag domain glows ยท /domains for domain board"
                            }}
                        </div>
                    </div>
                    <div class="flat-legend">
                        <span class="leg sin">"SINWOOD"</span>
                        <span class="leg avalon">"AVALON"</span>
                        <span class="leg ether">"ROCKETS"</span>
                        <span class="leg asgard">"ASGARD"</span>
                        <span class="leg edem">"EDEM"</span>
                        <span class="leg dim">"21 domains ยท drag to place ยท /domains board"</span>
                    </div>
                </section>

                // RIGHT โ€” 3D + INTENT
                <section class="cyberia-panel cyberia-right">
                    <div class="cyberia-panel-h">
                        <span class="panel-kicker">"RENDER"</span>
                        <span class="panel-sub">"flat ยท robot ยท 3d"</span>
                    </div>
                    <div class="render-3d">
                        {move || {
                            // โ€”โ€” flat โ€”โ€”
                            let fid = selected_flat.get().unwrap_or_else(|| "sinwood".into());
                            let list = flats.get();
                            let flat = list.iter().find(|f| f.id == fid);
                            let flat_name = flat
                                .map(|f| f.name.to_uppercase())
                                .unwrap_or_else(|| fid.to_uppercase());
                            let flat_kind = flat.map(|f| f.kind.clone()).unwrap_or_default();
                            let n = flat.map(|f| f.coords.len()).unwrap_or(0);
                            let land_hue = if fid.contains("avalon") {
                                "var(--cyber-orange)"
                            } else if fid.contains("sinwood") {
                                "var(--cyber-green)"
                            } else {
                                "var(--cyber-cyan)"
                            };
                            let leased_tag = if leased.get().iter().any(|x| x == &fid) {
                                " ยท LEASED"
                            } else {
                                ""
                            };

                            // โ€”โ€” robot / worker โ€”โ€”
                            let rid = selected_fleet.get();
                            let (bot_name, bot_kind, bot_role, bot_status, bot_owned) = rid
                                .as_ref()
                                .and_then(|id| {
                                    stock_fleet(id).map(|f| {
                                        (
                                            f.name.to_string(),
                                            f.kind.to_string(),
                                            f.role.to_string(),
                                            f.status.to_string(),
                                            false,
                                        )
                                    }).or_else(|| {
                                        owned.get().into_iter().find(|r| &r.id == id).map(|r| {
                                            (r.name, r.kind, r.role, "idle".into(), true)
                                        })
                                    })
                                })
                                .unwrap_or_else(|| {
                                    ("โ€”".into(), "none".into(), "no unit selected".into(), "โ€”".into(), false)
                                });
                            let is_worker = bot_kind == "worker";
                            let bot_hue = if bot_kind == "none" {
                                "#444"
                            } else if is_worker {
                                "var(--cyber-cyan)"
                            } else {
                                "var(--cyber-orange)"
                            };
                            let bot_cls = if bot_kind == "none" {
                                "bot-figure empty"
                            } else if is_worker {
                                "bot-figure worker"
                            } else {
                                "bot-figure machine"
                            };

                            view! {
                                <div class="render-pair">
                                    // LAND
                                    <div class="prism-scene">
                                        <div class="render-tag">"FLAT"</div>
                                        <div class="prism" style:--prism-color=land_hue>
                                            <div class="prism-face prism-top"></div>
                                            <div class="prism-face prism-front"></div>
                                            <div class="prism-face prism-side"></div>
                                        </div>
                                        <div class="prism-meta">
                                            <div class="prism-name">{format!("{flat_name}{leased_tag}")}</div>
                                            <div class="prism-sub">{format!("{flat_kind} ยท {n} verts")}</div>
                                            <div class="prism-sub">"Gesing hold"</div>
                                        </div>
                                    </div>
                                    // ROBOT
                                    <div class="prism-scene bot-scene">
                                        <div class="render-tag">"ROBOT"</div>
                                        <div class=bot_cls style:--bot-color=bot_hue>
                                            <div class="bot-head"></div>
                                            <div class="bot-body">
                                                <div class="bot-chest"></div>
                                                <div class="bot-arm bot-arm-l"></div>
                                                <div class="bot-arm bot-arm-r"></div>
                                            </div>
                                            <div class="bot-legs">
                                                <div class="bot-leg"></div>
                                                <div class="bot-leg"></div>
                                            </div>
                                            <div class="bot-glow"></div>
                                        </div>
                                        <div class="prism-meta">
                                            <div class="prism-name" style:color=bot_hue>{bot_name}</div>
                                            <div class="prism-sub">{bot_role}</div>
                                            <div class="prism-sub">
                                                {
                                                    let status_lbl = if bot_owned {
                                                        "OWNED".to_string()
                                                    } else {
                                                        bot_status.to_uppercase()
                                                    };
                                                    format!("{} ยท {}", bot_kind.to_uppercase(), status_lbl)
                                                }
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            }
                        }}
                    </div>

                    <div class="cyberia-panel-h" style="margin-top: 12px;">
                        <span class="panel-kicker">"INTENT"</span>
                        <span class="panel-sub">"soft3 write"</span>
                    </div>
                    <div class="intent-form">
                        <div class="intent-row">
                            <span class="intent-k">"fleet"</span>
                            <span class="intent-v">
                                {move || selected_fleet.get()
                                    .and_then(|id| {
                                        stock_fleet(&id)
                                            .map(|f| f.name.to_string())
                                            .or_else(|| {
                                                owned
                                                    .get_untracked()
                                                    .into_iter()
                                                    .find(|r| r.id == id)
                                                    .map(|r| r.name)
                                            })
                                    })
                                    .unwrap_or_else(|| "โ€”".into())}
                            </span>
                        </div>
                        <div class="intent-row">
                            <span class="intent-k">"flat"</span>
                            <span class="intent-v">
                                {move || selected_flat.get().unwrap_or_else(|| "โ€”".into()).to_uppercase()}
                            </span>
                        </div>
                        <div class="intent-actions">
                            {ACTIONS.iter().map(|(a, _)| {
                                let act = (*a).to_string();
                                let act2 = act.clone();
                                let locked = *a == "build";
                                view! {
                                    <button
                                        class=move || {
                                            let sel = selected_action.get() == act;
                                            format!("act-pill{}{}", if sel { " sel" } else { "" }, if locked { " locked" } else { "" })
                                        }
                                        disabled=locked
                                        on:click=move |_| {
                                            if !locked {
                                                selected_action.set(act2.clone());
                                            }
                                        }
                                    >{a.to_uppercase()}</button>
                                }
                            }).collect_view()}
                        </div>
                        <button
                            class="intent-commit"
                            on:click=move |_| {
                                let Some(fleet) = selected_fleet.get() else { return };
                                let Some(flat) = selected_flat.get() else { return };
                                let action = selected_action.get();
                                // soft3: offline stock fleets cannot take intent
                                if stock_fleet(&fleet)
                                    .is_some_and(|f| f.status == "offline" || f.phase > 0)
                                {
                                    return;
                                }
                                let fleet_name = stock_fleet(&fleet)
                                    .map(|f| f.name.to_string())
                                    .or_else(|| {
                                        owned
                                            .get_untracked()
                                            .into_iter()
                                            .find(|r| r.id == fleet)
                                            .map(|r| r.name)
                                    })
                                    .unwrap_or(fleet);
                                let id = next_id.get();
                                next_id.set(id + 1);
                                intents.update(|q| {
                                    q.insert(
                                        0,
                                        IntentRec {
                                            id,
                                            fleet: fleet_name,
                                            action,
                                            flat,
                                        },
                                    );
                                    save_intents(q);
                                });
                            }
                        >
                            "COMMIT INTENT"
                        </button>
                        <div class="intent-queue">
                            {move || {
                                let q = intents.get();
                                if q.is_empty() {
                                    return view! {
                                        <div class="intent-empty">"no intents yet โ€” buy, lease, or assign a fleet"</div>
                                    }.into_any();
                                }
                                view! {
                                    <div class="intent-list">
                                        {q.into_iter().take(10).map(|it| {
                                            let act_cls = match it.action.as_str() {
                                                "buy" => "ii-act buy",
                                                "lease" => "ii-act lease",
                                                "split" => "ii-act split",
                                                "merge" => "ii-act merge",
                                                _ => "ii-act",
                                            };
                                            view! {
                                                <div class="intent-item">
                                                    <span class="ii-id">{format!("#{:03}", it.id)}</span>
                                                    <span class="ii-fleet">{it.fleet}</span>
                                                    <span class=act_cls>{it.action.to_uppercase()}</span>
                                                    <span class="ii-flat">{it.flat.to_uppercase()}</span>
                                                </div>
                                            }
                                        }).collect_view()}
                                    </div>
                                }.into_any()
                            }}
                        </div>
                    </div>
                </section>
            </div>

            // โ”€โ”€ sheets: BUY ROBOT / LEASE LAND โ”€โ”€
            {move || match sheet.get() {
                Sheet::None => view! { <div></div> }.into_any(),
                Sheet::BuyRobot => view! {
                    <div class="cyberia-sheet-backdrop" on:click=move |_| sheet.set(Sheet::None)>
                        <div class="cyberia-sheet" on:click=move |ev| ev.stop_propagation()>
                            <div class="sheet-h">
                                <span class="panel-kicker">"BUY ROBOT"</span>
                                <button class="sheet-x" on:click=move |_| sheet.set(Sheet::None)>"โœ•"</button>
                            </div>
                            <p class="sheet-note">
                                "Phase 0 catalog โ€” soft3 local intent. No payment rail yet; purchase queues an intent and adds the unit to your fleet."
                            </p>
                            <div class="sheet-catalog">
                                {ROBOT_CATALOG.iter().map(|r| {
                                    let id = r.id.to_string();
                                    let id2 = id.clone();
                                    let kind_worker = r.kind == "worker";
                                    view! {
                                        <button
                                            class=move || {
                                                let sel = buy_pick.get() == id;
                                                format!(
                                                    "fleet-card catalog{}{}",
                                                    if sel { " sel" } else { "" },
                                                    if kind_worker { " worker" } else { " machine" },
                                                )
                                            }
                                            on:click=move |_| buy_pick.set(id2.clone())
                                        >
                                            <div class="fleet-top">
                                                <span class="fleet-name">{r.name}</span>
                                                <span class="fleet-status" style:color="var(--cyber-yellow)">"FOR SALE"</span>
                                            </div>
                                            <div class="fleet-role">{r.role}</div>
                                            <div class="fleet-meta">
                                                <span>{r.kind.to_uppercase()}</span>
                                                <span>"P0"</span>
                                            </div>
                                        </button>
                                    }
                                }).collect_view()}
                            </div>
                            <button
                                class="intent-commit"
                                on:click=move |_| {
                                    let pick = buy_pick.get();
                                    let Some(cat) = ROBOT_CATALOG.iter().find(|r| r.id == pick) else { return };
                                    let n = robot_serial.get();
                                    robot_serial.set(n + 1);
                                    let unit_id = format!("own-{}-{}", cat.name.to_lowercase(), n);
                                    let unit_name = format!("{}-{:02}", cat.name, n);
                                    owned.update(|v| {
                                        v.push(OwnedRobot {
                                            id: unit_id.clone(),
                                            name: unit_name.clone(),
                                            kind: cat.kind.to_string(),
                                            role: cat.role.to_string(),
                                        });
                                        save_owned(v);
                                    });
                                    selected_fleet.set(Some(unit_id));
                                    let id = next_id.get();
                                    next_id.set(id + 1);
                                    intents.update(|q| {
                                        q.insert(
                                            0,
                                            IntentRec {
                                                id,
                                                fleet: unit_name,
                                                action: "buy".into(),
                                                flat: "market".into(),
                                            },
                                        );
                                        save_intents(q);
                                    });
                                    debit_cx(15.0, "robots", "unit buy ยท map");
                                    sheet.set(Sheet::None);
                                }
                            >
                                "CONFIRM BUY"
                            </button>
                        </div>
                    </div>
                }.into_any(),
                Sheet::LeaseLand => view! {
                    <div class="cyberia-sheet-backdrop" on:click=move |_| sheet.set(Sheet::None)>
                        <div class="cyberia-sheet" on:click=move |ev| ev.stop_propagation()>
                            <div class="sheet-h">
                                <span class="panel-kicker">"LEASE LAND"</span>
                                <button class="sheet-x" on:click=move |_| sheet.set(Sheet::None)>"โœ•"</button>
                            </div>
                            <p class="sheet-note">
                                "Phase 0 flats โ€” century-index denominated later. Soft3 local intent only; no closed payment backend."
                            </p>
                            <div class="sheet-catalog">
                                {move || flats.get().into_iter().map(|f| {
                                    let id_cls = f.id.clone();
                                    let id_dis = f.id.clone();
                                    let id_click = f.id.clone();
                                    let id_col = f.id.clone();
                                    let id_lab = f.id.clone();
                                    let name = f.name.to_uppercase();
                                    let kind = f.kind.clone();
                                    view! {
                                        <button
                                            class=move || {
                                                let sel = selected_flat.get().as_deref() == Some(id_cls.as_str());
                                                let taken = leased.get().iter().any(|x| x == &id_cls);
                                                format!(
                                                    "fleet-card catalog flat-pick{}{}",
                                                    if sel { " sel" } else { "" },
                                                    if taken { " offline" } else { "" },
                                                )
                                            }
                                            disabled=move || leased.get().iter().any(|x| x == &id_dis)
                                            on:click=move |_| selected_flat.set(Some(id_click.clone()))
                                        >
                                            <div class="fleet-top">
                                                <span class="fleet-name">{name.clone()}</span>
                                                <span class="fleet-status" style:color=move || {
                                                    if leased.get().iter().any(|x| x == &id_col) {
                                                        "var(--cyber-yellow)"
                                                    } else {
                                                        "var(--cyber-green)"
                                                    }
                                                }>
                                                    {move || {
                                                        if leased.get().iter().any(|x| x == &id_lab) {
                                                            "LEASED"
                                                        } else {
                                                            "OPEN"
                                                        }
                                                    }}
                                                </span>
                                            </div>
                                            <div class="fleet-role">{format!("{kind} ยท Gesing ยท phase 0")}</div>
                                            <div class="fleet-meta">
                                                <span>"FLAT"</span>
                                                <span>"CX later"</span>
                                            </div>
                                        </button>
                                    }
                                }).collect_view()}
                            </div>
                            <button
                                class="intent-commit lease"
                                on:click=move |_| {
                                    let Some(flat) = selected_flat.get() else { return };
                                    if leased.get_untracked().iter().any(|x| x == &flat) {
                                        return;
                                    }
                                    leased.update(|v| v.push(flat.clone()));
                                    let (name, zone) = flats
                                        .get_untracked()
                                        .into_iter()
                                        .find(|f| f.id == flat)
                                        .map(|f| (f.name, f.zone))
                                        .unwrap_or_else(|| (flat.clone(), String::new()));
                                    let mut full = load_leases();
                                    if !full.iter().any(|l| l.flat_id == flat) {
                                        full.push(Lease {
                                            flat_id: flat.clone(),
                                            flat_name: name,
                                            zone,
                                        });
                                        save_leases(&full);
                                    }
                                    let id = next_id.get();
                                    next_id.set(id + 1);
                                    intents.update(|q| {
                                        q.insert(
                                            0,
                                            IntentRec {
                                                id,
                                                fleet: "YOU".into(),
                                                action: "lease".into(),
                                                flat: flat.clone(),
                                            },
                                        );
                                        save_intents(q);
                                    });
                                    debit_cx(10.0, "land", "lease ยท map");
                                    sheet.set(Sheet::None);
                                }
                            >
                                "CONFIRM LEASE"
                            </button>
                        </div>
                    </div>
                }.into_any(),
                Sheet::SplitLand => view! {
                    <div class="cyberia-sheet-backdrop" on:click=move |_| sheet.set(Sheet::None)>
                        <div class="cyberia-sheet sheet-wide" on:click=move |ev| ev.stop_propagation()>
                            <div class="sheet-h">
                                <span class="panel-kicker">"SPLIT LAND"</span>
                                <button class="sheet-x" on:click=move |_| sheet.set(Sheet::None)>"โœ•"</button>
                            </div>
                            <p class="sheet-note">
                                "Pick a flat, set cut direction and position. Areas are geodesic-approx mยฒ (equirectangular). Soft3 local intent."
                            </p>

                            <div class="split-pick-label">"1 ยท SELECT FLAT"</div>
                            <div class="sheet-catalog sheet-catalog-compact">
                                {move || flats.get().into_iter().map(|f| {
                                    let id = f.id.clone();
                                    let id2 = id.clone();
                                    let name = f.name.to_uppercase();
                                    let a = area_m2(&f.coords);
                                    view! {
                                        <button
                                            class=move || {
                                                let sel = selected_flat.get().as_deref() == Some(id.as_str());
                                                format!("fleet-card catalog flat-pick{}", if sel { " sel" } else { "" })
                                            }
                                            on:click=move |_| {
                                                selected_flat.set(Some(id2.clone()));
                                                split_ratio.set(0.50);
                                            }
                                        >
                                            <div class="fleet-top">
                                                <span class="fleet-name">{name}</span>
                                                <span class="fleet-status" style:color="var(--cyber-cyan)">{fmt_area_m2(a)}</span>
                                            </div>
                                        </button>
                                    }
                                }).collect_view()}
                            </div>

                            <div class="split-pick-label">"2 ยท CUT DIRECTION"</div>
                            <div class="split-dir-row">
                                <button
                                    class=move || if split_axis.get() == 0 { "act-pill sel" } else { "act-pill" }
                                    on:click=move |_| split_axis.set(0)
                                >"โ†• E / W  ยท  vertical cut"</button>
                                <button
                                    class=move || if split_axis.get() == 1 { "act-pill sel" } else { "act-pill" }
                                    on:click=move |_| split_axis.set(1)
                                >"โ†” N / S  ยท  horizontal cut"</button>
                            </div>

                            <div class="split-pick-label">
                                {move || {
                                    if split_axis.get() == 0 {
                                        "3 ยท POSITION  (west โ† โ†’ east)"
                                    } else {
                                        "3 ยท POSITION  (south โ† โ†’ north)"
                                    }
                                }}
                            </div>
                            <div class="split-slider-row">
                                <input
                                    type="range"
                                    min="5"
                                    max="95"
                                    step="1"
                                    class="split-range"
                                    prop:value=move || format!("{:.0}", split_ratio.get() * 100.0)
                                    on:input=move |ev| {
                                        if let Ok(v) = event_target_value(&ev).parse::<f64>() {
                                            split_ratio.set((v / 100.0).clamp(0.05, 0.95));
                                        }
                                    }
                                />
                                <span class="split-ratio-lbl">
                                    {move || format!("{:.0}%", split_ratio.get() * 100.0)}
                                </span>
                            </div>

                            // live preview metrics + mini map
                            {move || {
                                let fid = selected_flat.get();
                                let axis = split_axis.get();
                                let ratio = split_ratio.get();
                                let list = flats.get();
                                let Some(src) = fid.as_ref().and_then(|id| list.iter().find(|f| &f.id == id).cloned()) else {
                                    return view! {
                                        <div class="split-metrics empty">"select a flat to preview the cut"</div>
                                    }.into_any();
                                };
                                let total = area_m2(&src.coords);
                                let span = bbox_span_m(&src.coords, axis);
                                let offset = span * ratio;
                                let preview = split_flat_at(&src, axis, ratio);
                                let (ok, a_m2, b_m2, a_coords, b_coords) = match &preview {
                                    Some((a, b, _)) => (
                                        true,
                                        area_m2(&a.coords),
                                        area_m2(&b.coords),
                                        a.coords.clone(),
                                        b.coords.clone(),
                                    ),
                                    None => (false, 0.0, 0.0, vec![], vec![]),
                                };
                                let pct_a = if total > 0.0 { a_m2 / total * 100.0 } else { 0.0 };
                                let pct_b = if total > 0.0 { b_m2 / total * 100.0 } else { 0.0 };
                                let side_a = if axis == 0 { "WEST (A)" } else { "SOUTH (A)" };
                                let side_b = if axis == 0 { "EAST (B)" } else { "NORTH (B)" };
                                let (min_lon, max_lon, min_lat, max_lat) = poly_bbox(&src.coords);
                                // mini viewBox
                                const MW: f64 = 320.0;
                                const MH: f64 = 180.0;
                                const MP: f64 = 12.0;
                                let bbox = BBox { min_lon, max_lon, min_lat, max_lat };
                                let path_src = poly_path(&src.coords, &bbox, MW, MH, MP);
                                let path_a = poly_path(&a_coords, &bbox, MW, MH, MP);
                                let path_b = poly_path(&b_coords, &bbox, MW, MH, MP);
                                let (cut_x1, cut_y1, cut_x2, cut_y2) = if axis == 0 {
                                    let t = min_lon + ratio * (max_lon - min_lon);
                                    let (x, y1) = project(t, max_lat, &bbox, MW, MH, MP);
                                    let (_, y2) = project(t, min_lat, &bbox, MW, MH, MP);
                                    (x, y1, x, y2)
                                } else {
                                    let t = min_lat + ratio * (max_lat - min_lat);
                                    let (x1, y) = project(min_lon, t, &bbox, MW, MH, MP);
                                    let (x2, _) = project(max_lon, t, &bbox, MW, MH, MP);
                                    (x1, y, x2, y)
                                };
                                view! {
                                    <div class="split-preview">
                                        <svg class="split-mini" viewBox=format!("0 0 {MW} {MH}")>
                                            <path d=path_src fill="rgba(255,255,255,0.03)" stroke="#333" stroke-width="1" />
                                            {ok.then(|| view! {
                                                <path d=path_a fill="rgba(0,229,255,0.28)" stroke="var(--cyber-cyan)" stroke-width="1.5" />
                                                <path d=path_b fill="rgba(255,0,255,0.22)" stroke="var(--cyber-magenta)" stroke-width="1.5" />
                                                <line x1=cut_x1 y1=cut_y1 x2=cut_x2 y2=cut_y2
                                                    stroke="var(--cyber-yellow)" stroke-width="2"
                                                    stroke-dasharray="4 3" />
                                            })}
                                        </svg>
                                        <div class="split-metrics">
                                            <div class="sm-row total">
                                                <span>"TOTAL"</span>
                                                <span>{fmt_area_m2(total)}</span>
                                            </div>
                                            <div class="sm-row a">
                                                <span>{side_a}</span>
                                                <span>{if ok { format!("{} ยท {:.1}%", fmt_area_m2(a_m2), pct_a) } else { "โ€”".into() }}</span>
                                            </div>
                                            <div class="sm-row b">
                                                <span>{side_b}</span>
                                                <span>{if ok { format!("{} ยท {:.1}%", fmt_area_m2(b_m2), pct_b) } else { "โ€”".into() }}</span>
                                            </div>
                                            <div class="sm-row dim">
                                                <span>"cut offset"</span>
                                                <span>{format!("{:.1} m from {}", offset, if axis == 0 { "west" } else { "south" })}</span>
                                            </div>
                                            <div class="sm-row dim">
                                                <span>"bbox span"</span>
                                                <span>{format!("{:.1} m", span)}</span>
                                            </div>
                                            {(!ok).then(|| view! {
                                                <div class="sm-row warn">"cut invalid โ€” move slider (sliver or empty half)"</div>
                                            })}
                                        </div>
                                    </div>
                                }.into_any()
                            }}

                            <button
                                class="intent-commit split"
                                on:click=move |_| {
                                    let Some(fid) = selected_flat.get() else { return };
                                    let list = flats.get_untracked();
                                    let Some(src) = list.iter().find(|f| f.id == fid).cloned() else { return };
                                    let axis = split_axis.get_untracked();
                                    let ratio = split_ratio.get_untracked();
                                    let Some((a, b, _)) = split_flat_at(&src, axis, ratio) else { return };
                                    let a_m2 = area_m2(&a.coords);
                                    let b_m2 = area_m2(&b.coords);
                                    let a_id = a.id.clone();
                                    flats.update(|v| {
                                        v.retain(|f| f.id != fid);
                                        v.push(a);
                                        v.push(b);
                                    });
                                    leased.update(|v| v.retain(|x| x != &fid));
                                    {
                                        let mut full = load_leases();
                                        full.retain(|l| l.flat_id != fid);
                                        save_leases(&full);
                                    }
                                    selected_flat.set(Some(a_id));
                                    let id = next_id.get();
                                    next_id.set(id + 1);
                                    intents.update(|q| {
                                        q.insert(
                                            0,
                                            IntentRec {
                                                id,
                                                fleet: "YOU".into(),
                                                action: "split".into(),
                                                flat: format!(
                                                    "{fid} โ†’ {:.0}+{:.0} mยฒ",
                                                    a_m2, b_m2
                                                ),
                                            },
                                        );
                                        save_intents(q);
                                    });
                                    sheet.set(Sheet::None);
                                }
                            >
                                "CONFIRM SPLIT"
                            </button>
                        </div>
                    </div>
                }.into_any(),
                Sheet::MergeLand => view! {
                    <div class="cyberia-sheet-backdrop" on:click=move |_| {
                        merge_pick.set(None);
                        sheet.set(Sheet::None);
                    }>
                        <div class="cyberia-sheet" on:click=move |ev| ev.stop_propagation()>
                            <div class="sheet-h">
                                <span class="panel-kicker">"MERGE LAND"</span>
                                <button class="sheet-x" on:click=move |_| {
                                    merge_pick.set(None);
                                    sheet.set(Sheet::None);
                                }>"โœ•"</button>
                            </div>
                            <p class="sheet-note">
                                "Pick two flats: first is primary (A), second is B. Merge into one hold. Soft3 local intent."
                            </p>
                            <div class="intent-row" style="margin-bottom:8px;">
                                <span class="intent-k">"A"</span>
                                <span class="intent-v">
                                    {move || selected_flat.get().unwrap_or_else(|| "โ€”".into()).to_uppercase()}
                                </span>
                            </div>
                            <div class="intent-row" style="margin-bottom:10px;">
                                <span class="intent-k">"B"</span>
                                <span class="intent-v">
                                    {move || merge_pick.get().unwrap_or_else(|| "โ€”".into()).to_uppercase()}
                                </span>
                            </div>
                            <div class="sheet-catalog">
                                {move || flats.get().into_iter().map(|f| {
                                    let id = f.id.clone();
                                    let id2 = id.clone();
                                    let name = f.name.to_uppercase();
                                    view! {
                                        <button
                                            class=move || {
                                                let a = selected_flat.get().as_deref() == Some(id.as_str());
                                                let b = merge_pick.get().as_deref() == Some(id.as_str());
                                                format!(
                                                    "fleet-card catalog flat-pick{}{}",
                                                    if a { " sel" } else { "" },
                                                    if b { " merge-b" } else { "" },
                                                )
                                            }
                                            on:click=move |_| {
                                                let cur_a = selected_flat.get_untracked();
                                                if cur_a.as_deref() == Some(id2.as_str()) {
                                                    return;
                                                }
                                                if cur_a.is_none() {
                                                    selected_flat.set(Some(id2.clone()));
                                                } else if merge_pick.get_untracked().as_deref() == Some(id2.as_str()) {
                                                    merge_pick.set(None);
                                                } else {
                                                    merge_pick.set(Some(id2.clone()));
                                                }
                                            }
                                        >
                                            <div class="fleet-top">
                                                <span class="fleet-name">{name}</span>
                                                <span class="fleet-status" style:color="var(--cyber-magenta)">"MERGE"</span>
                                            </div>
                                            <div class="fleet-role">"1st click = A ยท 2nd = B"</div>
                                        </button>
                                    }
                                }).collect_view()}
                            </div>
                            <button
                                class="intent-commit merge"
                                on:click=move |_| {
                                    let Some(a_id) = selected_flat.get() else { return };
                                    let Some(b_id) = merge_pick.get() else { return };
                                    if a_id == b_id { return; }
                                    let list = flats.get_untracked();
                                    let Some(a) = list.iter().find(|f| f.id == a_id).cloned() else { return };
                                    let Some(b) = list.iter().find(|f| f.id == b_id).cloned() else { return };
                                    let merged = merge_flats(&a, &b);
                                    let mid = merged.id.clone();
                                    flats.update(|v| {
                                        v.retain(|f| f.id != a_id && f.id != b_id);
                                        v.push(merged);
                                    });
                                    leased.update(|v| {
                                        v.retain(|x| x != &a_id && x != &b_id);
                                    });
                                    {
                                        let mut full = load_leases();
                                        full.retain(|l| l.flat_id != a_id && l.flat_id != b_id);
                                        save_leases(&full);
                                    }
                                    selected_flat.set(Some(mid.clone()));
                                    merge_pick.set(None);
                                    let id = next_id.get();
                                    next_id.set(id + 1);
                                    intents.update(|q| {
                                        q.insert(
                                            0,
                                            IntentRec {
                                                id,
                                                fleet: "YOU".into(),
                                                action: "merge".into(),
                                                flat: format!("{a_id}+{b_id}"),
                                            },
                                        );
                                        save_intents(q);
                                    });
                                    sheet.set(Sheet::None);
                                }
                            >
                                "CONFIRM MERGE"
                            </button>
                        </div>
                    </div>
                }.into_any(),
            }}

            <div class="search-dock cyberia-dock">
                <span class="dock-count">
                    {move || {
                        let n = intents.get().len();
                        let m = flats.get().len();
                        let o = owned.get().len();
                        let l = leased.get().len();
                        format!(
                            "{n} intents ยท {m} flats ยท {}w+{}m+{o} fleets ยท {l} leases",
                            WORKERS.len(),
                            MACHINES.len(),
                        )
                    }}
                </span>
                <div class="cyberia-cta-bar dock-ctas">
                    <button class="cta-btn cta-buy cta-lg" on:click=move |_| sheet.set(Sheet::BuyRobot)>
                        <span class="cta-ico">"๐Ÿค–"</span>
                        <span class="cta-copy">
                            <span class="cta-title">"BUY ROBOT"</span>
                            <span class="cta-sub">"add a unit to your fleet"</span>
                        </span>
                    </button>
                    <button class="cta-btn cta-lease cta-lg" on:click=move |_| sheet.set(Sheet::LeaseLand)>
                        <span class="cta-ico">"๐Ÿ—บ"</span>
                        <span class="cta-copy">
                            <span class="cta-title">"LEASE LAND"</span>
                            <span class="cta-sub">"hold a phase-0 flat"</span>
                        </span>
                    </button>
                    <button class="cta-btn cta-split cta-lg cta-bold" on:click=move |_| {
                        split_ratio.set(0.50);
                        sheet.set(Sheet::SplitLand);
                    }>
                        <span class="cta-ico">"โœ‚"</span>
                        <span class="cta-copy">
                            <span class="cta-title">"SPLIT LAND"</span>
                            <span class="cta-sub">"precise cut ยท mยฒ preview"</span>
                        </span>
                    </button>
                    <button class="cta-btn cta-merge cta-lg cta-bold" on:click=move |_| {
                        merge_pick.set(None);
                        sheet.set(Sheet::MergeLand);
                    }>
                        <span class="cta-ico">"โง‰"</span>
                        <span class="cta-copy">
                            <span class="cta-title">"MERGE LAND"</span>
                            <span class="cta-sub">"join two flats"</span>
                        </span>
                    </button>
                </div>
                <a href="https://x.com/cyberiacap" target="_blank" rel="noopener" class="dock-credit">
                    "๐Ÿด a "<span style="color: var(--cyber-green);">"cyberia"</span>" project"
                </a>
            </div>
        </div>
    }
}

Homonyms

cyberia/cyberia-my/src/console.rs

Graph