# ━━━ gui.ling — LingMiao 灵庙: the temple window, in the LingStationX look ━━━
# Chrome and widgets come from theme.ling / core.ling / ds.ling / icons.ling
# (the LingDev design system). The repo logic is the same store/rites code
# the CLI uses; the GUI narrates its ceremonies to the terminal it was
# launched from. fns prefixed gx_.

use "theme.ling"
use "core.ling"
use "ds.ling"
use "icons.ling"

# core.ling's text_size_set calls this; the CLI has nothing to save.
fn settings_save() { return 0.0 }

# ── data refresh ──────────────────────────────────────────────────────
fn gx_refresh() {
    bind d = rt_status_data()
    bind rows_p = list_new()
    bind rows_k = list_new()
    bind sp = list_get(d, 0.0)
    bind ss = list_get(d, 1.0)
    bind i = 0.0
    while i < len(sp) {
        bind rows_p = list_push(rows_p, list_get(sp, i))
        bind rows_k = list_push(rows_k, "altar:" + list_get(ss, i))
        bind i = i + 1.0
    }
    bind rp = list_get(d, 2.0)
    bind rs = list_get(d, 3.0)
    bind i = 0.0
    while i < len(rp) {
        bind rows_p = list_push(rows_p, list_get(rp, i))
        bind rows_k = list_push(rows_k, "restless:" + list_get(rs, i))
        bind i = i + 1.0
    }
    bind wander = list_get(d, 4.0)
    bind i = 0.0
    while i < len(wander) {
        bind rows_p = list_push(rows_p, list_get(wander, i))
        bind rows_k = list_push(rows_k, "wander:new")
        bind i = i + 1.0
    }
    st_set("gx_rows_p", rows_p)
    st_set("gx_rows_k", rows_k)
    st_set("gx_diff_for", "")
    # lineage
    bind tips = list_new()
    bind souls = sx_souls()
    bind i = 0.0
    while i < len(souls) {
        bind t = sx_tip(list_get(souls, i))
        if t != "" {
            if ut_contains(tips, t) == false { bind tips = list_push(tips, t) }
        }
        bind i = i + 1.0
    }
    bind seen = list_new()
    bind queue = tips
    bind qi = 0.0
    while qi < len(queue) {
        bind id = list_get(queue, qi)
        if ut_contains(seen, id) == false {
            bind seen = list_push(seen, id)
            bind r = sx_rite_read(id)
            bind ps = list_get(r, 1.0)
            bind k = 0.0
            while k < len(ps) {
                bind queue = list_push(queue, list_get(ps, k))
                bind k = k + 1.0
            }
        }
        bind qi = qi + 1.0
    }
    # order by time, newest first (insertion sort on small n)
    bind ord = list_new()
    bind times = list_new()
    bind i = 0.0
    while i < len(seen) {
        bind r = sx_rite_read(list_get(seen, i))
        bind t = list_get(r, 3.0)
        bind at = 0.0
        bind go = true
        while go {
            if at >= len(ord) {
                bind go = false
            } else if list_get(times, at) < t {
                bind go = false
            } else {
                bind at = at + 1.0
            }
        }
        # insert at position `at`
        bind no = list_new()
        bind nt = list_new()
        bind q = 0.0
        while q < at {
            bind no = list_push(no, list_get(ord, q))
            bind nt = list_push(nt, list_get(times, q))
            bind q = q + 1.0
        }
        bind no = list_push(no, list_get(seen, i))
        bind nt = list_push(nt, t)
        while q < len(ord) {
            bind no = list_push(no, list_get(ord, q))
            bind nt = list_push(nt, list_get(times, q))
            bind q = q + 1.0
        }
        bind ord = no
        bind times = nt
        bind i = i + 1.0
    }
    st_set("gx_rites", ord)
    st_set("gx_flashT", 0.0)
    return 0.0
}

fn gx_flash(msg) {
    st_set("gx_flash", msg)
    st_set("gx_flashT", time_now())
    return 0.0
}

# ── diff cache for the selected file ──────────────────────────────────
fn gx_make_diff(p, kind) {
    bind head = sx_rite_manifest(sx_head_rite())
    bind a = sx_altar()
    bind oid = ""
    bind at = ut_index_of(list_get(a, 1.0), p)
    if at >= 0.0 { bind oid = list_get(list_get(a, 0.0), at) }
    if oid == "" || oid == "-" {
        bind hat = ut_index_of(list_get(head, 1.0), p)
        if hat >= 0.0 { bind oid = list_get(list_get(head, 0.0), hat) }
    }
    bind old_txt = ""
    if oid != "" {
        if oid != "-" {
            if rt_is_bin_spirit(oid) {
                st_set("gx_dk", ["h"])
                st_set("gx_dt", ["binary spirit — no yinyang to read"])
                st_set("gx_diff_for", p)
                return 0.0
            }
            bind old_txt = rt_spirit_text_or(oid)
        }
    }
    bind new_txt = ""
    if file_exists(sx_root() + "/" + p) {
        bind new_txt = rt_worktree_text(p)
        if new_txt == "" {
            bind hr = file_read_hex(sx_root() + "/" + p)
            if ut_is_bad(hr) == false {
                if len(hr.unwrap()) > 0.0 {
                    st_set("gx_dk", ["h"])
                    st_set("gx_dt", ["binary spirit — no yinyang to read"])
                    st_set("gx_diff_for", p)
                    return 0.0
                }
            }
        }
    }
    bind la = split(old_txt, "\n")
    bind lb = split(new_txt, "\n")
    bind ops = dy_ops(la, lb)
    bind dk = list_new()
    bind dt = list_new()
    bind apos = 0.0
    bind bpos = 0.0
    bind k = 0.0
    while k < len(ops) {
        bind kind2 = list_get(ops, k)
        bind n = list_get(ops, k + 1.0)
        if kind2 == "=" {
            bind lead = min(3.0, n)
            bind tail = min(3.0, n - lead)
            bind q = 0.0
            while q < lead {
                bind dk = list_push(dk, "c")
                bind dt = list_push(dt, "  " + list_get(la, apos + q))
                bind q = q + 1.0
            }
            if n > 6.0 {
                bind dk = list_push(dk, "h")
                bind dt = list_push(dt, "  ⋯ " + (n - 6.0) + " serene lines ⋯")
            }
            bind q = n - tail
            while q < n {
                bind dk = list_push(dk, "c")
                bind dt = list_push(dt, "  " + list_get(la, apos + q))
                bind q = q + 1.0
            }
            bind apos = apos + n
            bind bpos = bpos + n
        } else if kind2 == "-" {
            bind q = 0.0
            while q < n {
                bind dk = list_push(dk, "-")
                bind dt = list_push(dt, "− " + list_get(la, apos + q))
                bind q = q + 1.0
            }
            bind apos = apos + n
        } else {
            bind q = 0.0
            while q < n {
                bind dk = list_push(dk, "+")
                bind dt = list_push(dt, "+ " + list_get(lb, bpos + q))
                bind q = q + 1.0
            }
            bind bpos = bpos + n
        }
        bind k = k + 2.0
    }
    st_set("gx_dk", dk)
    st_set("gx_dt", dt)
    st_set("gx_diff_for", p)
    return 0.0
}

# a rite's aura: 7 tiles colored by its leading hex digits
fn gx_aura(x, y, id) {
    bind i = 0.0
    while i < 7.0 {
        bind c = hex_to_number(str_slice(id, i, 1.0))
        g_rrect(x + i * 10.0, y, 8.0, 8.0, 2.0, c_pal(c))
        bind i = i + 1.0
    }
    return 0.0
}

fn gx_row_col(k) {
    if str_starts_with(k, "altar:") { return C_YELLOW }
    if str_starts_with(k, "restless:") { return C_ORANGE }
    return C_VIOLET
}

fn gx_row_tag(k) {
    if k == "altar:new" { return "new on altar" }
    if k == "altar:gone" { return "release staged" }
    if str_starts_with(k, "altar:") { return "on the altar" }
    if k == "restless:gone" { return "departed" }
    if str_starts_with(k, "restless:") { return "restless" }
    return "wandering"
}

# ── tab 0: divine (status / offer / rite) ─────────────────────────────
fn gx_tab_divine(W, H) {
    bind top = TABS_H
    bind bot = H - STATUS_H - 44.0
    bind lw = 330.0
    # left: the spirits
    g_rect(0.0, top, lw, bot - top, C_BG_SIDE)
    u_vsep(lw - 1.0, top, bot - top)
    ds_word(10.0, top + 8.0, 9.0, "SPIRITS", ds_col("call"), true)
    if u_button("gx_offer_all", lw - 150.0, top + 4.0, 68.0, 20.0, "offer all", 0.0) {
        rt_offer(["."])
        gx_refresh()
        gx_flash("every spirit placed on the altar")
    }
    if u_button("gx_refresh", lw - 76.0, top + 4.0, 66.0, 20.0, "divine", 0.0) {
        gx_refresh()
        gx_flash("the oracle has been consulted")
    }
    u_hsep(6.0, top + 30.0, lw - 12.0)
    bind rows_p = st_get("gx_rows_p", [])
    bind rows_k = st_get("gx_rows_k", [])
    bind n = len(rows_p)
    bind ltop = top + 36.0
    bind lh = bot - ltop - 4.0
    bind rh = 22.0
    bind off = u_scroll_y("gx_sc0", 0.0, ltop, lw - 1.0, lh, n * rh + 8.0, st_get("gx_off0", 0.0))
    st_set("gx_off0", off)
    u_clip(0.0, ltop, lw - 1.0, lh)
    bind sel_p = st_get("gx_sel", "")
    bind i = 0.0
    while i < n {
        bind ry = ltop + 4.0 + i * rh - off
        if ry + rh >= ltop && ry <= ltop + lh {
            bind p = list_get(rows_p, i)
            bind k = list_get(rows_k, i)
            bind hot = u_hot(0.0, ry, lw - 14.0, rh)
            if sel_p == p { g_rect(0.0, ry, lw - 1.0, rh, C_SEL_ROW) }
            else if hot { g_rect(0.0, ry, lw - 1.0, rh, C_HOVER_ROW) }
            g_rrect(8.0, ry + 6.0, 10.0, 10.0, 2.0, gx_row_col(k))
            g_text_fit(26.0, ry + 5.0, lw - 140.0, 12.0, sel(sel_p == p, C_TEXT_STRONG, C_TEXT), p)
            g_text_r(lw - 16.0, ry + 6.0, 10.0, C_TEXT_DIM, gx_row_tag(k))
            if hot && u_pressed() {
                st_set("gx_sel", p)
                gx_make_diff(p, k)
            }
            if hot && u_double() {
                if str_starts_with(k, "altar:") {
                    rt_withdraw([p])
                } else {
                    rt_offer_one(p)
                }
                gx_refresh()
            }
        }
        bind i = i + 1.0
    }
    if n < 0.5 {
        g_text_c(lw * 0.5, ltop + 30.0, 12.0, C_TEXT_DIM, "the temple is serene")
    }
    u_unclip()
    # right: the yinyang of the selected spirit
    bind rx = lw + 8.0
    bind rw = W - rx - 8.0
    ds_frame(rx, top + 6.0, rw, bot - top - 12.0, ds_col("ink"), "YINYANG")
    if sel_p != "" {
        if st_get("gx_diff_for", "") != sel_p { gx_make_diff(sel_p, "") }
        # per-row buttons
        if u_button("gx_b_offer", rx + rw - 240.0, top + 12.0, 70.0, 20.0, "offer", C_YELLOW) {
            rt_offer_one(sel_p)
            gx_refresh()
            gx_flash(sel_p + " placed on the altar")
        }
        if u_button("gx_b_withdraw", rx + rw - 164.0, top + 12.0, 78.0, 20.0, "withdraw", 0.0) {
            rt_withdraw([sel_p])
            gx_refresh()
        }
        if u_button("gx_b_restore", rx + rw - 80.0, top + 12.0, 70.0, 20.0, "restore", C_RED) {
            rt_restore([sel_p])
            gx_refresh()
            gx_flash(sel_p + " returned to its enshrined form")
        }
        g_text_fit(rx + 14.0, top + 14.0, rw - 260.0, 12.0, C_TEXT_STRONG, sel_p)
        bind dk = st_get("gx_dk", [])
        bind dt = st_get("gx_dt", [])
        bind dn = len(dk)
        bind dtop = top + 40.0
        bind dh = bot - dtop - 14.0
        bind drh = 16.0
        bind doff = u_scroll_y("gx_sc0d", rx + 4.0, dtop, rw - 8.0, dh, dn * drh + 8.0, st_get("gx_offd", 0.0))
        st_set("gx_offd", doff)
        u_clip(rx + 4.0, dtop, rw - 8.0, dh)
        bind i = 0.0
        while i < dn {
            bind ry = dtop + 2.0 + i * drh - doff
            if ry + drh >= dtop && ry <= dtop + dh {
                bind k = list_get(dk, i)
                bind c = C_TEXT_DIM
                if k == "-" { bind c = C_RED }
                if k == "+" { bind c = C_GREEN }
                if k == "h" { bind c = C_INDIGO }
                g_text_fit(rx + 14.0, ry, rw - 40.0, 11.0, c, list_get(dt, i))
            }
            bind i = i + 1.0
        }
        u_unclip()
    } else {
        g_text_c(rx + rw * 0.5, top + (bot - top) * 0.5, 12.0, C_TEXT_DIM, "choose a spirit to read its yinyang")
    }
    # bottom: the rite bar
    bind by = bot + 6.0
    g_rect(0.0, bot, W, H - STATUS_H - bot, C_BG_PANEL)
    u_hsep(0.0, bot, W)
    bind msg = u_textfield("gx_msg", 12.0, by + 2.0, W - 180.0, 26.0, st_get("gx_msg", ""))
    st_set("gx_msg", msg)
    if msg == "" {
        if st_get("focus", "") != "gx_msg" { g_text(20.0, by + 9.0, 12.0, C_TEXT_DIM, "the rite's inscription…") }
    }
    if u_button("gx_seal", W - 156.0, by + 2.0, 144.0, 26.0, "✦ seal the rite", C_GREEN) {
        if msg == "" {
            gx_flash("a rite needs an inscription")
        } else {
            rt_rite(msg)
            st_set("gx_msg", "")
            gx_refresh()
            gx_flash("the rite is sealed")
        }
    }
    return 0.0
}

# ── tab 1: lineage ────────────────────────────────────────────────────
fn gx_tab_lineage(W, H) {
    bind top = TABS_H
    bind bot = H - STATUS_H
    bind rites = st_get("gx_rites", [])
    bind n = len(rites)
    bind lw = floor(W * 0.56)
    g_rect(0.0, top, lw, bot - top, C_BG_SIDE)
    u_vsep(lw - 1.0, top, bot - top)
    ds_word(10.0, top + 8.0, 9.0, "LINEAGE", ds_col("bind"), true)
    u_hsep(6.0, top + 30.0, lw - 12.0)
    bind ltop = top + 36.0
    bind lh = bot - ltop - 4.0
    bind rh = 40.0
    bind off = u_scroll_y("gx_sc1", 0.0, ltop, lw - 1.0, lh, n * rh + 8.0, st_get("gx_off1", 0.0))
    st_set("gx_off1", off)
    u_clip(0.0, ltop, lw - 1.0, lh)
    bind souls = sx_souls()
    bind selr = st_get("gx_selr", "")
    bind i = 0.0
    while i < n {
        bind ry = ltop + 4.0 + i * rh - off
        if ry + rh >= ltop && ry <= ltop + lh {
            bind id = list_get(rites, i)
            bind r = sx_rite_read(id)
            bind hot = u_hot(0.0, ry, lw - 14.0, rh)
            if selr == id { g_rect(0.0, ry, lw - 1.0, rh, C_SEL_ROW) }
            else if hot { g_rect(0.0, ry, lw - 1.0, rh, C_HOVER_ROW) }
            gx_aura(8.0, ry + 6.0, id)
            g_text(8.0, ry + 20.0, 10.0, C_TEXT_DIM, ut_short(id))
            g_text_fit(86.0, ry + 4.0, lw - 240.0, 12.0, sel(selr == id, C_TEXT_STRONG, C_TEXT), ut_first_line(list_get(r, 4.0)))
            g_text(86.0, ry + 20.0, 10.0, C_TEXT_DIM, ut_fmt_time(list_get(r, 3.0)) + " · " + list_get(r, 2.0))
            if len(list_get(r, 1.0)) > 1.5 { g_text(lw - 130.0, ry + 20.0, 10.0, C_VIOLET, "☯ union") }
            bind decx = lw - 130.0
            bind s = 0.0
            while s < len(souls) {
                if sx_tip(list_get(souls, s)) == id {
                    bind nm = list_get(souls, s)
                    bind wn = g_text_w(10.0, nm) + 10.0
                    g_rrect(decx, ry + 3.0, wn, 14.0, 3.0, C_SEL)
                    g_text(decx + 5.0, ry + 5.0, 10.0, C_TEXT_STRONG, nm)
                    bind decx = decx + wn + 4.0
                }
                bind s = s + 1.0
            }
            if hot && u_pressed() { st_set("gx_selr", id) }
        }
        bind i = i + 1.0
    }
    if n < 0.5 { g_text_c(lw * 0.5, ltop + 30.0, 12.0, C_TEXT_DIM, "no rites have been sealed yet") }
    u_unclip()
    # right: manifest of the selected rite
    bind rx = lw + 8.0
    bind rw = W - rx - 8.0
    ds_frame(rx, top + 6.0, rw, bot - top - 12.0, ds_col("mod"), "MANIFEST")
    if selr != "" {
        bind r = sx_rite_read(selr)
        gx_aura(rx + 14.0, top + 16.0, selr)
        g_text(rx + 90.0, top + 12.0, 12.0, C_TEXT_STRONG, ut_short(selr))
        g_text(rx + 14.0, top + 32.0, 11.0, C_TEXT_DIM, ut_fmt_time(list_get(r, 3.0)) + " · " + list_get(r, 2.0))
        g_text_wrap(rx + 14.0, top + 50.0, rw - 28.0, 12.0, C_TEXT, list_get(r, 4.0))
        if u_button("gx_summon_r", rx + rw - 110.0, top + 12.0, 96.0, 20.0, "summon", C_INDIGO) {
            rt_summon([selr])
            gx_refresh()
            gx_flash("wandering at " + ut_short(selr) + " (detached)")
        }
        bind m = sx_rite_manifest(selr)
        bind ids = list_get(m, 0.0)
        bind paths = list_get(m, 1.0)
        bind mtop = top + 96.0
        bind mh = bot - mtop - 16.0
        bind mn = len(paths)
        bind moff = u_scroll_y("gx_sc1m", rx + 4.0, mtop, rw - 8.0, mh, mn * 18.0 + 8.0, st_get("gx_offm", 0.0))
        st_set("gx_offm", moff)
        u_clip(rx + 4.0, mtop, rw - 8.0, mh)
        bind i = 0.0
        while i < mn {
            bind ry = mtop + 2.0 + i * 18.0 - moff
            if ry + 18.0 >= mtop && ry <= mtop + mh {
                bind info = sx_spirit_info(list_get(ids, i))
                bind strat = list_get(info, 0.0)
                bind c = C_GREEN
                bind glyph = "○"
                if strat == "delta" {
                    bind c = C_BLUE
                    bind glyph = "◐"
                }
                if strat == "chunks" {
                    bind c = C_VIOLET
                    bind glyph = "◍"
                }
                g_text(rx + 14.0, ry, 11.0, c, glyph + " " + strat)
                g_text_fit(rx + 90.0, ry, rw - 180.0, 11.0, C_TEXT, list_get(paths, i))
                g_text_r(rx + rw - 16.0, ry, 10.0, C_TEXT_DIM, "" + list_get(info, 3.0) + " B")
            }
            bind i = i + 1.0
        }
        u_unclip()
    } else {
        g_text_c(rx + rw * 0.5, top + (bot - top) * 0.5, 12.0, C_TEXT_DIM, "choose a rite to read its manifest")
    }
    return 0.0
}

# ── tab 2: souls ──────────────────────────────────────────────────────
fn gx_tab_souls(W, H) {
    bind top = TABS_H
    bind bot = H - STATUS_H
    bind souls = sx_souls()
    bind h = sx_head()
    bind cur = ""
    if list_get(h, 0.0) == "soul" { bind cur = list_get(h, 1.0) }
    bind lw = floor(W * 0.6)
    g_rect(0.0, top, lw, bot - top, C_BG_SIDE)
    u_vsep(lw - 1.0, top, bot - top)
    ds_word(10.0, top + 8.0, 9.0, "SOULS", ds_col("ctl"), true)
    u_hsep(6.0, top + 30.0, lw - 12.0)
    bind sels = st_get("gx_sels", "")
    bind i = 0.0
    while i < len(souls) {
        bind s = list_get(souls, i)
        bind ry = top + 38.0 + i * 34.0
        bind hot = u_hot(0.0, ry, lw - 8.0, 32.0)
        if sels == s { g_rect(0.0, ry, lw - 1.0, 32.0, C_SEL_ROW) }
        else if hot { g_rect(0.0, ry, lw - 1.0, 32.0, C_HOVER_ROW) }
        bind c = c_pal(i)
        if s == cur { bind c = C_GREEN }
        g_disc(16.0, ry + 16.0, 6.0, c)
        if s == cur { g_ring(16.0, ry + 16.0, 9.0, C_TEXT_STRONG) }
        g_text(32.0, ry + 4.0, 13.0, sel(s == cur, C_TEXT_STRONG, C_TEXT), s + sel(s == cur, "  (the living soul)", ""))
        bind tip = sx_tip(s)
        if tip != "" {
            bind r = sx_rite_read(tip)
            g_text_fit(32.0, ry + 19.0, lw - 60.0, 10.0, C_TEXT_DIM, ut_short(tip) + " · " + ut_first_line(list_get(r, 4.0)))
        } else {
            g_text(32.0, ry + 19.0, 10.0, C_TEXT_DIM, "(unborn)")
        }
        if hot && u_pressed() { st_set("gx_sels", s) }
        bind i = i + 1.0
    }
    # right: actions
    bind rx = lw + 8.0
    bind rw = W - rx - 8.0
    ds_frame(rx, top + 6.0, rw, bot - top - 12.0, ds_col("ctl"), "CEREMONIES")
    bind ay = top + 26.0
    if sels != "" {
        g_text(rx + 14.0, ay, 12.0, C_TEXT_STRONG, "chosen soul: " + sels)
        if u_button("gx_s_summon", rx + 14.0, ay + 24.0, rw - 28.0, 26.0, "summon " + sels, C_BLUE) {
            rt_summon([sels])
            gx_refresh()
            gx_flash("soul summoned: " + sels)
        }
        if u_button("gx_s_harm", rx + 14.0, ay + 56.0, rw - 28.0, 26.0, "harmonize " + sels + " into " + cur, C_GREEN) {
            rt_harmonize([sels])
            gx_refresh()
            gx_flash("harmony sought with " + sels)
        }
        if u_button("gx_s_del", rx + 14.0, ay + 88.0, rw - 28.0, 26.0, "release " + sels, C_RED) {
            rt_soul(["-d", sels])
            st_set("gx_sels", "")
            gx_refresh()
        }
        bind ay = ay + 128.0
    }
    u_hsep(rx + 10.0, ay, rw - 20.0)
    g_text(rx + 14.0, ay + 10.0, 11.0, C_TEXT_DIM, "kindle a new soul at the current rite:")
    bind nm = u_textfield("gx_newsoul", rx + 14.0, ay + 28.0, rw - 120.0, 24.0, st_get("gx_newsoul", ""))
    st_set("gx_newsoul", nm)
    if u_button("gx_s_new", rx + rw - 96.0, ay + 28.0, 82.0, 24.0, "kindle", C_YELLOW) {
        if nm != "" {
            rt_soul([nm])
            st_set("gx_newsoul", "")
            gx_refresh()
            gx_flash("new soul kindled: " + nm)
        }
    }
    return 0.0
}

# ── tab 3: fair (share over SSH) ──────────────────────────────────────
fn gx_fair_tick() {
    if st_get("gx_fair_on", 0.0) < 0.5 { return 0.0 }
    bind handled = 0.0
    bind go = true
    while go {
        bind msg = ssh_serve_recv()
        if msg == "" {
            bind go = false
        } else {
            bind sp = str_find(msg, "|")
            bind cid = to_number(str_slice(msg, 0.0, sp))
            bind cmd = str_trim(str_slice(msg, sp + 1.0, 99999999.0))
            nx_fair_handle(cid, cmd, path_name(sx_root()), st_get("gx_fair_trib", 0.0) > 0.5, st_get("gx_fair_wpw", ""))
            st_set("gx_fair_served", st_get("gx_fair_served", 0.0) + 1.0)
            bind handled = handled + 1.0
            if handled >= 4.0 { bind go = false }
        }
    }
    return 0.0
}

fn gx_tab_fair(W, H) {
    bind top = TABS_H
    bind bot = H - STATUS_H
    bind rx = 12.0
    bind rw = W - 24.0
    ds_frame(rx, top + 8.0, rw, bot - top - 16.0, ds_col("asm"), "TEMPLE FAIR 庙会")
    bind y = top + 34.0
    bind on = st_get("gx_fair_on", 0.0) > 0.5
    if on == false {
        g_text(rx + 16.0, y, 12.0, C_TEXT, "open your temple over SSH — pilgrims pull with a one-time link.")
        g_text(rx + 16.0, y + 18.0, 11.0, C_TEXT_DIM, "its own host key, its own credentials; your personal SSH keys are never touched.")
        g_text(rx + 16.0, y + 46.0, 11.0, C_TEXT_DIM, "gate (port):")
        bind ports = u_textfield("gx_fair_port", rx + 100.0, y + 40.0, 90.0, 24.0, st_get("gx_fair_port", "7447"))
        st_set("gx_fair_port", ports)
        bind trib = st_get("gx_fair_trib", 0.0) > 0.5
        if u_check("gx_fair_tribc", rx + 210.0, y + 44.0, "accept tribute (push)", trib) {
            st_set("gx_fair_trib", sel(trib, 0.0, 1.0))
        }
        if u_button("gx_fair_open", rx + 16.0, y + 80.0, 180.0, 28.0, "⛩ open the gates", C_GREEN) {
            bind port = to_number(ports)
            if port < 1.0 { bind port = 7447.0 }
            bind pw = random_hex(9.0)
            bind wpw = random_hex(9.0)
            make_dir(sx_dir() + "/fair")
            ssh_config(port, "pilgrim", pw, sx_dir() + "/fair/host_ed25519")
            bind okd = ssh_serve()
            if okd == false {
                gx_flash("the gates would not open — port busy?")
            } else {
                bind host = env_get("COMPUTERNAME", env_get("HOSTNAME", "<your-ip>"))
                bind link = "miao+ssh://pilgrim:" + pw + "@" + host + ":" + port + "/" + path_name(sx_root())
                st_set("gx_fair_on", 1.0)
                st_set("gx_fair_link", link)
                st_set("gx_fair_wpw", wpw)
                st_set("gx_fair_served", 0.0)
                write_file(sx_dir() + "/fair/link.txt", link + "\n")
                bind svg = qr_svg(link)
                if svg != "" { write_file(sx_dir() + "/fair/link.svg", svg) }
                net_announce(7448.0, "miao|" + path_name(sx_root()) + "|" + port)
                gx_flash("the fair is open")
            }
        }
    } else {
        g_text(rx + 16.0, y, 13.0, C_GREEN, "⛩ the fair is OPEN — pilgrims may pull")
        g_text(rx + 16.0, y + 26.0, 11.0, C_TEXT_DIM, "share this link (also in .miao/fair/link.txt, QR in link.svg):")
        g_rrect(rx + 14.0, y + 44.0, rw - 28.0, 26.0, 3.0, C_BG_DEEP)
        g_text_fit(rx + 22.0, y + 50.0, rw - 44.0, 12.0, C_YELLOW, st_get("gx_fair_link", ""))
        if st_get("gx_fair_trib", 0.0) > 0.5 {
            g_text(rx + 16.0, y + 80.0, 11.0, C_ORANGE, "tribute link (push access — guard it):")
            g_rrect(rx + 14.0, y + 96.0, rw - 28.0, 26.0, 3.0, C_BG_DEEP)
            g_text_fit(rx + 22.0, y + 102.0, rw - 44.0, 11.0, C_ORANGE, st_get("gx_fair_link", "") + "?tribute=" + st_get("gx_fair_wpw", ""))
        }
        g_text(rx + 16.0, y + 134.0, 11.0, C_TEXT_DIM, "ceremonies served: " + st_get("gx_fair_served", 0.0))
        if u_button("gx_fair_close", rx + 16.0, y + 158.0, 180.0, 28.0, "close the gates", C_RED) {
            ssh_stop()
            net_announce_stop()
            st_set("gx_fair_on", 0.0)
            gx_flash("the fair has closed")
        }
    }
    # mothers + pilgrimage/tribute
    bind my = y + 210.0
    u_hsep(rx + 10.0, my - 10.0, rw - 20.0)
    ds_word(rx + 16.0, my, 9.0, "INCENSE NETWORK", ds_col("func"), true)
    bind lines = ut_read_lines(sx_dir() + "/mothers")
    bind i = 0.0
    while i < len(lines) {
        bind ln = list_get(lines, i)
        bind sp = str_find(ln, "|")
        bind mname = str_slice(ln, 0.0, sp)
        bind link = str_slice(ln, sp + 1.0, 99999.0)
        bind at = str_find(link, "@")
        bind safe = link
        if at >= 0.0 { bind safe = "…@" + str_slice(link, at + 1.0, 99999.0) }
        g_text(rx + 16.0, my + 22.0 + i * 30.0, 12.0, C_BLUE, "祖 " + mname)
        g_text_fit(rx + 120.0, my + 23.0 + i * 30.0, rw - 420.0, 11.0, C_TEXT_DIM, safe)
        if u_button("gx_pil" + mname, rx + rw - 280.0, my + 18.0 + i * 30.0, 120.0, 24.0, "pilgrimage", C_BLUE) {
            nx_pilgrimage([mname])
            gx_refresh()
            gx_flash("pilgrimage to " + mname + " complete — see the terminal for details")
        }
        if u_button("gx_tri" + mname, rx + rw - 150.0, my + 18.0 + i * 30.0, 120.0, 24.0, "tribute", C_ORANGE) {
            nx_tribute([mname])
            gx_refresh()
            gx_flash("tribute carried to " + mname + " — see the terminal")
        }
        bind i = i + 1.0
    }
    if len(lines) < 0.5 {
        g_text(rx + 16.0, my + 24.0, 11.0, C_TEXT_DIM, "no ancestral temples — enshrine a link from the CLI, or add one with `miao mother add`")
    }
    return 0.0
}

# ── chrome ────────────────────────────────────────────────────────────
fn gx_tabs(W) {
    g_rect(0.0, 0.0, W, TABS_H, C_BG_PANEL)
    u_hsep(0.0, TABS_H - 1.0, W)
    bind names = ["Divine 问", "Lineage 谱", "Souls 魂", "Fair 庙会"]
    bind cols = [C_YELLOW, C_INDIGO, C_GREEN, C_RED]
    bind x = 10.0
    bind i = 0.0
    while i < len(names) {
        bind nm = list_get(names, i)
        bind w = g_text_w(12.0, nm) + 24.0
        if u_tab("gx_tab" + num_str(i), x, 3.0, w, 22.0, nm, st_get("gx_tab", 0.0) == i, list_get(cols, i)) {
            st_set("gx_tab", i)
            gx_refresh()
        }
        bind x = x + w + 3.0
        bind i = i + 1.0
    }
    # temple + soul, right-aligned
    bind h = sx_head()
    bind where = ""
    if list_get(h, 0.0) == "soul" { bind where = "魂 " + list_get(h, 1.0) }
    if list_get(h, 0.0) == "rite" { bind where = "wandering · " + ut_short(list_get(h, 1.0)) }
    bind label = path_name(sx_root()) + "  ·  " + where
    g_text_r(W - 12.0, 7.0, 12.0, C_TEXT_DIM, label)
    return 0.0
}

fn gx_status(W, H) {
    bind y = H - STATUS_H
    g_rect(0.0, y, W, STATUS_H, C_BG_PANEL)
    u_hsep(0.0, y, W)
    # seven tiny ROYGBIV beads, the miao signature
    bind i = 0.0
    while i < 7.0 {
        g_disc(12.0 + i * 12.0, y + 11.0, 3.0, c_pal(i))
        bind i = i + 1.0
    }
    bind rows = st_get("gx_rows_p", [])
    bind state = "serene"
    if len(rows) > 0.5 { bind state = "" + len(rows) + " spirit(s) stirring" }
    if st_get("gx_fair_on", 0.0) > 0.5 { bind state = state + "  ·  fair open" }
    g_text(100.0, y + 5.0, 10.0, C_TEXT_DIM, sx_root() + "  ·  keeper " + sx_author() + "  ·  " + state)
    bind fl = st_get("gx_flash", "")
    if fl != "" {
        if time_now() - st_get("gx_flashT", 0.0) < 4.0 {
            g_text_r(W - 12.0, y + 5.0, 10.0, C_YELLOW, fl)
        }
    }
    return 0.0
}

# ── no-temple greeting ────────────────────────────────────────────────
fn gx_no_temple(W, H) {
    bind cy = H * 0.5 - 60.0
    bind i = 0.0
    while i < 7.0 {
        g_disc(W * 0.5 - 42.0 + i * 14.0, cy - 30.0, 5.0, c_pal(i))
        bind i = i + 1.0
    }
    g_text_c(W * 0.5, cy, 18.0, C_TEXT_STRONG, "no temple stands here")
    g_text_c(W * 0.5, cy + 26.0, 12.0, C_TEXT_DIM, absolute_path("."))
    if u_button("gx_found", W * 0.5 - 130.0, cy + 60.0, 260.0, 30.0, "✦ consecrate this ground", C_GREEN) {
        sx_init(ut_norm(absolute_path(".")))
        st_set("sx_root", ut_norm(absolute_path(".")))
        gx_refresh()
        gx_flash("the ground is consecrated")
    }
    return 0.0
}

# ── entry ─────────────────────────────────────────────────────────────
fn gx_main(args) {
    bind shot = ""
    bind ai = 0.0
    while ai < len(args) {
        if list_get(args, ai) == "--shot" { bind shot = list_get(args, ai + 1.0) }
        if list_get(args, ai) == "--tab" { st_set("gx_tab", to_number(list_get(args, ai + 1.0))) }
        bind ai = ai + 1.0
    }
    open_window(1240.0, 800.0, "LingMiao — 灵庙", 1)
    px_flat()
    bind home = env_get("MIAO_HOME", "")
    if home != "" { bind home = str_replace(home, "\\", "/") }
    bind f = font_load("assets/font.ttf")
    if f < 0.0 { bind f = font_load("../assets/font.ttf") }
    if f < 0.0 { bind f = font_load(home + "assets/font.ttf") }
    if f < 0.0 { bind f = font_load("C:/Users/User/Programs/ling-source/assets/font.ttf") }
    bind uf = font_load("assets/fonts/DaemonSans.ttf")
    if uf < 0.0 { bind uf = font_load(home + "assets/fonts/DaemonSans.ttf") }
    if uf < 0.0 { bind uf = font_load("C:/Users/User/Programs/ling-source/assets/fonts/DaemonSans.ttf") }
    if f >= 0.0 {
        if uf >= 0.0 { font_fallback(f, uf, 0.88) }
    }
    if f < 0.0 { bind f = uf }
    st_set("font", f)
    if sx_root() != "" { gx_refresh() }
    bind frame = 0.0
    while window_is_open() && st_get("gx_quit", 0.0) < 0.5 {
        st_set("frame", frame)
        px_theme(0.0)
        u_begin()
        bind W = get_width()
        bind H = get_height()
        g_rect(0.0, 0.0, W, H, C_BG_APP)
        if sx_root() == "" {
            gx_no_temple(W, H)
        } else {
            gx_fair_tick()
            bind tab = st_get("gx_tab", 0.0)
            if tab == 0.0 { gx_tab_divine(W, H) }
            if tab == 1.0 { gx_tab_lineage(W, H) }
            if tab == 2.0 { gx_tab_souls(W, H) }
            if tab == 3.0 { gx_tab_fair(W, H) }
            gx_status(W, H)
        }
        gx_tabs(W)
        # tooltip
        bind tip = st_get("tip", "")
        if tip != "" {
            bind tw = g_text_w(11.0, tip) + 14.0
            bind tx = min(u_mx() + 14.0, W - tw - 4.0)
            g_rrect(tx, u_my() + 18.0, tw, 20.0, 4.0, C_POPUP)
            g_rframe(tx, u_my() + 18.0, tw, 20.0, 4.0, C_POPUP_LINE)
            g_text(tx + 7.0, u_my() + 22.0, 11.0, C_TEXT, tip)
        }
        if st_get("focus", "") == "" {
            if key_pressed("f5") {
                gx_refresh()
                gx_flash("the oracle has been consulted")
            }
            if u_ctrl() {
                if key_pressed("q") { st_set("gx_quit", 1.0) }
            }
        }
        combo_overlay()
        u_end()
        present()
        if shot != "" {
            if frame == 30.0 {
                screenshot(shot)
                st_set("gx_quit", 1.0)
            }
        }
        bind frame = frame + 1.0
    }
    if st_get("gx_fair_on", 0.0) > 0.5 {
        ssh_stop()
        net_announce_stop()
    }
    return 0.0
}
