cartridge.cafe · open source world

Tideglass

HOW TO PLAY

CLICK — turn dials, work levers, travel (chevrons at the screen edges) DRAG — aim the great lens No words will help you. Watch the bells. Read the stone. Match the sky. Wake the vault from the sea.

built by: Claude Fable 5
1 visual shader · 11 shader modules · 1 step hook · runs on WebGPU in the browser
This source is part of the cartridge.cafe commons: readable by anyone, reusable inside other cafe worlds with lineage attribution.

— VISUAL SHADERS (WGSL) —

visual · tideglass

// private segment-distance — the visual must be SELF-SUFFICIENT (its lone tg_lib
// dependency caused recurring quarantines when the browser composed it early)
fn mod_tgv_seg(p: vec2f, a: vec2f, b: vec2f) -> f32 {
  let pa = p - a; let ba = b - a;
  let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h);
}
// A MOON that reads as a moon: sphere-lit terminator, maria, limb halo (BONES Jul 30)
fn mod_tgv_moon(px: vec2f, ctr: vec2f, R: f32, elId: f32, t: f32) -> vec3f {
  // BONES->SUNS (Galen Aug 10): the plain sphere-lit disc is retired; each sky
  // body is now ARCANE SUN TECHNOLOGY (mod_tg_suntech, tg_lib). ruling vs other
  // is read from uni(78); the two call sites already gate presence, so alt stays
  // full — matching the moons' always-visible behavior.
  let rulingEl = uni(78);
  let isRuling = select(0.0, 1.0, abs(elId - rulingEl) < 0.25);
  let mm = vec2f(uni(27), uni(28));
  let hov = smoothstep(R * 2.2, R * 0.8, length(mm - ctr));
  return mod_tg_suntech(px, ctr, R, elId, t, isRuling, 1.0, hov);
}

// ── THE SATCHEL — a diegetic inventory strip along the bottom edge, drawn once any
//    item is owned. SDF icons only (no baked pixels): a gear is a ring + cos(k*theta)
//    teeth (teeth count 8/12/16/20 IS the icon); the spanner is a shaft + open C-jaw.
//    Reads uni(61) invMask (1 spanner ·2 g8 ·4 g12 ·8 g16 ·16 g20), uni(62) heldItem. ──
fn mod_tg_icon_gear(px: vec2f, c: vec2f, R: f32, teeth: f32, t: f32) -> f32 {
  let d = px - c; let r = length(d); let a = atan2(d.y, d.x) + t * 0.4;
  let rim = R + 3.0 * smoothstep(-0.1, 0.5, cos(a * teeth));
  return clamp(smoothstep(rim, rim - 2.5, r) - smoothstep(R * 0.30, R * 0.30 - 2.5, r), 0.0, 1.0);
}
fn mod_tg_icon_spanner(px: vec2f, c: vec2f, s: f32) -> f32 {
  let d = px - c;
  let handle = mod_tgv_seg(d, vec2f(-s, 0.0), vec2f(s * 0.4, 0.0));
  var m = smoothstep(0.22 * s, 0.14 * s, handle);
  let hc = d - vec2f(0.7 * s, 0.0);
  let rr = abs(length(hc) - 0.36 * s);
  m = max(m, smoothstep(0.14 * s, 0.07 * s, rr) * step(hc.x, 0.12 * s));
  return clamp(m, 0.0, 1.0);
}
fn mod_tg_bit(mask: f32, bit: f32) -> f32 { return floor(mask / bit) - 2.0 * floor(mask / (bit * 2.0)); }
fn mod_tg_satchel(cIn: vec3f, px: vec2f, mm: vec2f, t: f32) -> vec3f {
  var c = cIn;
  let inv = uni(61); let held = i32(uni(62) + 0.5);
  if (inv < 0.5 && held == 0 && !(uni(108) > 0.5 && uni(109) < 0.5)) { return c; }  // nothing carried yet -> no strip
  let bandY = 486.0;
  let band = smoothstep(28.0, 24.0, abs(px.y - bandY)) * step(96.0, px.x) * step(px.x, 416.0);
  c = mix(c, vec3f(0.04, 0.05, 0.06), band * 0.55);
  // BONES (Galen Jul 30): the satchel owns a RESERVED plate — no pickups or
  // level elements may live inside x 100..412, y bandY-28..512 (audited).
  let plateD = sdRoundedBox(px - vec2f(256.0, bandY), vec2f(152.0, 26.0), 12.0);
  c = mix(c, vec3f(0.020, 0.024, 0.032), smoothstep(1.5, -1.5, plateD) * 0.85);
  c += vec3f(0.30, 0.34, 0.42) * smoothstep(2.0, 0.0, abs(plateD)) * 0.5;
  // BONES: the deny flash — a locked interactive answers with a red pulse (uni79..81)
  if (uni(79) > 0.01) {
    let dnp = px - vec2f(uni(80), uni(81));
    c += vec3f(0.95, 0.22, 0.16) * smoothstep(3.5, 0.5, abs(length(dnp) - (30.0 + (1.0 - uni(79)) * 26.0))) * uni(79) * 0.9;
  }
  var slotX = array<f32, 5>(128.0, 192.0, 256.0, 320.0, 384.0);
  var bits  = array<f32, 5>(1.0, 2.0, 4.0, 8.0, 16.0);
  var teeth = array<f32, 5>(0.0, 8.0, 12.0, 16.0, 20.0);    // slot 0 = spanner
  for (var i = 0; i < 5; i = i + 1) {
    let ctr = vec2f(slotX[i], bandY);
    c += vec3f(0.10, 0.11, 0.13) * tg_ring(px, ctr, 22.0, 19.0);       // slot frame
    if (i == 3) {
      // SLOT 3 is RESERVED for the tideglass pearl (the unused 16t gear is gone).
      if (uni(108) > 0.5 && uni(109) < 0.5) {                          // carried, not yet seated
        let rr = length(px - ctr);
        c += mix(vec3f(0.55, 0.72, 0.82), vec3f(1.0, 1.0, 0.96), smoothstep(13.0, 0.0, rr)) * smoothstep(12.0, 9.0, rr);
        c += vec3f(0.9, 0.96, 1.0) * smoothstep(4.5, 0.0, length(px - (ctr - vec2f(3.0, 3.0)))) * 0.85;   // specular
      }
    } else {
      let owned = mod_tg_bit(inv, bits[i]);
      if (owned > 0.5 && held != i + 1) {
        if (i == 0) { c += vec3f(1.0, 0.82, 0.40) * mod_tg_icon_spanner(px, ctr, 14.0); }
        else { c += vec3f(0.90, 0.78, 0.45) * mod_tg_icon_gear(px, ctr, 14.0, teeth[i], t); }
      }
    }
  }
  if (held >= 1) {                                          // the held item rides the cursor
    if (held == 1) { c += vec3f(1.0, 0.85, 0.45) * mod_tg_icon_spanner(px, mm, 16.0); }
    else { c += vec3f(1.0, 0.82, 0.45) * mod_tg_icon_gear(px, mm, 15.0, teeth[held - 1], t); }
  }
  return c;
}
fn visual_tideglass(uv: vec2f, sdf: f32, color: vec4f, time: f32, params: vec4f, behind: vec4f) -> vec4f {
  let t = uni(3);
  let px = (uv * 0.5 + 0.5) * 512.0;
  let view = i32(uni(0) + 0.5);
  var c = mod_tg_scene(view, uv, px, t);
  // cross-fade during travel
  let fade = uni(2);
  if (fade > 0.003) {
    let pv = i32(uni(1) + 0.5);
    c = mix(c, mod_tg_scene(pv, uv, px, t), fade);
  }
  // ── nav chevrons (hook mirrors these hit zones) ──
  let mm = vec2f(uni(27), uni(28));
  let act = uni(24);
  let fin = uni(17);
  // ═══ SCENE-TABLE CHEVRONS — drawn from DATA (uni 88..103). One truth with the
  //     hook's NAV table: invisible or unclickable doors are impossible now. ═══
  for (var ni = 0; ni < 4; ni++) {
    let nbase = 88 + ni * 4;
    if (uni(nbase + 3) > 0.5) {
      let cpos = vec2f(uni(nbase), uni(nbase + 1));
      let ca = mod_tg_chev(px, cpos, i32(uni(nbase + 2)), smoothstep(55.0, 20.0, length(mm - cpos)), t);
      c = mix(c, ca.rgb + c, ca.a * 0.9);
    }
  }
  // INSPECT SKIN (Galen): unmistakably a different mode — cool blue cast + a faint
  //   64-unit coordinate grid; game clicks are suppressed while this shows.
  if (uni(87) > 0.5) {
    c = c * vec3f(0.78, 0.88, 1.12) + vec3f(0.008, 0.022, 0.055);
    let gx = abs(fract(px.x / 64.0) - 0.5);
    let gy = abs(fract(px.y / 64.0) - 0.5);
    let gridL = max(smoothstep(0.47, 0.5, gx), smoothstep(0.47, 0.5, gy));
    c += vec3f(0.10, 0.22, 0.40) * gridL * 0.18;
    let brd = max(smoothstep(6.0, 2.0, px.x), max(smoothstep(6.0, 2.0, px.y), max(smoothstep(506.0, 510.0, px.x), smoothstep(506.0, 510.0, px.y))));
    c += vec3f(0.15, 0.35, 0.75) * brd * 0.5;
  }
  // click-confirm: a thin gold ring blooms from every registered click
  if (uni(84) > 0.01) {
    let ckp = px - vec2f(uni(85), uni(86));
    c += vec3f(1.0, 0.85, 0.45) * smoothstep(2.5, 0.5, abs(length(ckp) - (12.0 + (1.0 - uni(84)) * 30.0))) * uni(84) * 0.8;
  }
  if (view == 0) {                                    // the shore
    // (right chevron removed — it never worked once the moon rose and the finale
    //  pinned the view; the building is the way in, the risen dome the way up.)
    // an arrow over the dome — ONLY once the dome has actually breached the sea
    // (uni(26)=vault); by day there's no dome, so no orphaned chevron over water.
    if (uni(26) > 0.03) {
      let dm = vec2f(214.0, 318.0);
      let dhov = smoothstep(92.0, 28.0, length(mm - dm));
      let da = mod_tg_chev(px, vec2f(214.0, 262.0), 3, 0.4 + 0.6 * dhov, t);
      c = mix(c, da.rgb + c, da.a * 0.9 * min(uni(26) * 3.0, 1.0));
    }
    // the observatory building answers the cursor too — its own door is the gate
    let bc = vec2f(440.0, 300.0);
    let bhov = smoothstep(82.0, 30.0, length(mm - bc));
    c += vec3f(0.22, 0.52, 0.62) * bhov * (0.10 + 0.05 * sin(t * 2.0));
    // right chevron → into the observatory (the gate). Restored: the finale no longer
    // pins the view, so a plain right-arrow off the shore works again.
    // left chevron → into the sea-cave, a grotto in the rock to the left (always open)
    // ── ACT 1 v2: the tide answers the moon. At low tide a wet-sand band emerges at
    //    the foot of the shore and the stranded 8t gear surfaces on the tideline. ──
    let tideL = uni(60);
    if (tideL < 0.30) {
      let low = smoothstep(0.30, 0.05, tideL);
      let bnd = smoothstep(440.0, 472.0, px.y);
      c = mix(c, vec3f(0.16, 0.15, 0.12) * (0.7 + 0.3 * sin(px.x * 0.1)), bnd * low * 0.5);
      c += vec3f(0.30, 0.40, 0.42) * smoothstep(3.0, 0.0, abs(px.y - 450.0)) * low * 0.25;
      // (the 8t gear pickup moved to the sea-cave plank — no gear surfaces here)
    }
    // the moon answers a touch (glass moon → mirror-still): a soft disc high in the sky
    // BONES (Galen Jul 30): moons READABLE in the sky — element colors the disc,
    //    the ring pulses when a moon is chosen, and the SEA ANSWERS the element.
    if (uni(113) < 0.0005) {   // suns belong to the SHORE - never over THE CROSSING
    let mc0 = vec2f(400.0, 96.0);
    let mh0 = smoothstep(46.0, 20.0, length(mm - mc0));
    let elId = uni(78);
    var mcol = vec3f(0.62, 0.78, 0.95);
    if (elId > 0.5) { mcol = vec3f(1.0, 0.55, 0.25); }
    c += mod_tgv_moon(px, mc0, 26.0, elId, t) * (0.85 + 0.20 * mh0);
    c += mcol * tg_ring(px, mc0, 34.0, 31.0) * (uni(75) * (0.22 + 0.22 * sin(t * 2.2)) + mh0 * 0.15);
    if (uni(73) > 1.5) {
      c += mod_tgv_moon(px, vec2f(338.0, 62.0), 13.0, 1.0 - step(0.5, elId), t) * 0.8;
    }
    }
    // (red tint band retired — the marched ocean itself converts via uni(82))
  }
  if (view == 13) {                                   // the drowned quarter → up the shaft
  }
  if (view == 8) {                                    // the sea-cave → shore (right) · MIRROR ISLAND (left)
    // BONES (Galen Jul 30): the mirror-island exit existed in the hook but was NEVER drawn — the door gets its knob
  }
  if (view == 5) {                                    // the observatory room → back down to the shore
    // once the constellation is gold, the heart becomes a door: a pulsing ring
    // at the centre invites the click that dives into the orb (view 6).
    if (uni(38) > 0.5) {
      let oc = vec2f(256.0, 256.0);
      let rp = length(px - oc);
      let hov = smoothstep(84.0, 24.0, length(mm - oc));
      let pulse = 0.5 + 0.5 * sin(t * 2.5);
      let ring = smoothstep(3.5, 0.0, abs(rp - (34.0 + pulse * 7.0)));
      c += vec3f(1.0, 0.82, 0.36) * ring * (0.22 + hov * 0.55);
    }
  }
  if (view == 6) {                                    // inside the orb → back to the constellation
    // once the core is woken, its heart becomes a door: a pulsing ring dives to the orrery
    if (uni(39) > 0.5) {
      let oc = vec2f(256.0, 256.0);
      let rp = length(px - oc);
      let hov = smoothstep(84.0, 24.0, length(mm - oc));
      let pulse = 0.5 + 0.5 * sin(t * 2.5);
      let ring = smoothstep(3.5, 0.0, abs(rp - (30.0 + pulse * 6.0)));
      c += vec3f(1.0, 0.85, 0.40) * ring * (0.25 + hov * 0.55) * uni(43);
    }
  }
  if (view == 7) {                                    // the orrery → back down into the core
  }
  if (view == 1) {
    // ── ACT 1 v2: THE DROWNED GATE. The disc's lower half sits underwater; at low
    //    tide it surfaces to reveal four notch-targets at 6 o'clock. A lever swings
    //    the targets 12 o'clock (up) <-> 6 o'clock (down). uni(77) notch, uni(60) tide. ──
    let notch = uni(77);
    // (blue half-ring reveal deleted — tide is pinned; it overlapped the doorway. Galen Jul 30)
    let lc = vec2f(432.0, 360.0);
    let lh = smoothstep(34.0, 14.0, length(mm - lc));
    c += vec3f(0.70, 0.65, 0.40) * tg_ring(px, lc, 20.0, 14.0) * (0.4 + 0.5 * lh);
    let ly = lc + vec2f(0.0, mix(-13.0, 13.0, notch));
    c += mix(vec3f(0.95, 0.85, 0.5), vec3f(0.4, 0.9, 0.85), notch) * tg_disc(px, ly, 7.0);
  }
  if (view == 4) {                                    // the record room → back to the gate
    // RESTORED (Jul 30, stale-clobber): tablet -> brambles, right edge
  }
  if (view == 2) {
    if (uni(14) > 0.5) {                              // ascend to the lens
    }
  }
  if (view == 3) {
  }
  if (view == 12) {                                    // the thicket -> back to the lens
  }
  // ── the satchel inventory strip (over the scene, under the click ripple) ──
  if (uni(113) < 0.0005) { c = mod_tg_satchel(c, px, mm, t); }
  // click ripple
  let cp = uni(25);
  if (cp > 0.003) {
    let cd = length(px - mm);
    c += vec3f(0.9, 0.85, 0.7) * exp(-pow((cd - (1.0 - cp) * 60.0) * 0.25, 2.0)) * cp * 0.5;
  }
  // finale white bloom breath
  c += vec3f(1.0, 0.95, 0.85) * fin * (1.0 - fin) * 1.2;
  // ── the island's one grade: amber highlights, teal-indigo shadows ──
  c = max(c, vec3f(0.0));
  let lum = dot(c, vec3f(0.2126, 0.7152, 0.0722));
  c = mix(vec3f(lum), c, 1.09);                                       // painterly saturation
  c += vec3f(0.030, 0.016, 0.0) * smoothstep(0.30, 1.30, lum);        // gold in the light
  c += vec3f(0.0, 0.010, 0.020) * (1.0 - smoothstep(0.0, 0.30, lum)); // sea-glass in the dark
  c *= 1.0 - 0.16 * dot(uv * 0.72, uv * 0.72);                        // breath of corner shadow
  // gentle grain
  c += (hash21(floor(px * 1.7)) - 0.5) * 0.012;
  return vec4f(max(c, vec3f(0.0)), 1.0);
}

— SHADER MODULES —

module · tg_air


fn mod_tg_airship(cIn: vec3f, p: vec2f, t: f32, dawn: f32, sel: f32, night: f32) -> vec3f {
  var c = cIn;

  // ===== SHARED FRAME: drift / bob / pitch folded in ONCE for every aspect =====
  let sway  = 0.030 * sin(t * 0.047 + 0.3) + 0.011 * sin(t * 0.113 + 1.7);   // lateral
  let bob   = 0.013 * sin(t * 0.190 + 0.6) + 0.004 * sin(t * 0.370 + 2.1);   // vertical
  let pitch = 0.022 * sin(t * 0.190 + 2.17);                                 // nose pitch (rad)
  let atmSun = mod_tgo_suncol(sel);                                          // warm dawn key

  let ac = vec2f(-0.58, -0.50) + vec2f(sway, bob);
  let aq = rot2(pitch) * (p - ac);

  // ══════════════════════ ATMOSPHERE (behind the hull) ══════════════════════
  // (a) soft dawn haze halo hugging the envelope crown
  {
    let hn  = aq / vec2f(0.315, 0.150);
    let hd  = length(hn);
    let bloom = exp(-max(hd - 0.55, 0.0) * 3.2);
    let crown = smoothstep(0.30, 0.02, abs(hd - 1.0)) * smoothstep(0.0, -0.10, aq.y);
    let lit = clamp(0.5 + 0.5 * dot(normalize(aq + vec2f(1e-4, 1e-4)), normalize(vec2f(0.85, 0.55))), 0.0, 1.0);
    let haloCol = mix(vec3f(0.42, 0.50, 0.62), atmSun, lit * 0.72);
    c += haloCol * (bloom * 0.16 + crown * 0.22 * lit) * dawn;
  }

  // (b) reflection + silhouette on the sea far below (ac already carries drift)
  {
    let refY = 0.375;
    let rc   = vec2f(ac.x, refY);
    var rp   = p - rc;
    rp.x += 0.020 * sin(p.y * 24.0 + t * 1.3) + 0.010 * sin(p.y * 57.0 - t * 0.7);
    let rq   = vec2f(rp.x, -rp.y * 2.7);
    let envM = 1.0 - smoothstep(0.72, 1.16, length(rq / vec2f(0.26, 0.10)));
    let gonM = 1.0 - smoothstep(0.70, 1.20, length((rq - vec2f(0.0, 0.15)) / vec2f(0.090, 0.045)));
    let shipM = max(envM, gonM * 0.85);
    let band  = smoothstep(0.300, 0.340, p.y) * (1.0 - smoothstep(0.44, 0.50, p.y));
    let refl  = shipM * band * dawn * step(uni(113), 0.0005);   // the sea reflection belongs to the SHORE — in the cine it read as a horizon line bound to the ship (Galen)
    let reflCol = mix(vec3f(0.05, 0.06, 0.09), atmSun * 0.75, 0.42);
    c = mix(c, reflCol, refl * 0.32);
  }

  // (c) two slow-tumbling distant gulls
  {
    for (var i = 0; i < 2; i = i + 1) {
      let fi = f32(i);
      let ph = fi * 3.10;
      let bx = -0.88 + glsl_mod(t * 0.013 + fi * 0.57, 1.9);
      let by = -0.64 + 0.05 * sin(t * 0.21 + ph) + fi * 0.055;
      let bp = p - vec2f(bx, by);
      let s  = 0.024;
      let flap = 0.55 + 0.42 * sin(t * 3.1 + ph);
      let wL = sdSegment(bp, vec2f(0.0, 0.0), vec2f(-s, -flap * s));
      let wR = sdSegment(bp, vec2f(0.0, 0.0), vec2f( s, -flap * s));
      let bm = (1.0 - smoothstep(0.0, 0.006, min(wL, wR))) * dawn * step(uni(113), 0.0005);   // gulls stay home too
      c = mix(c, vec3f(0.10, 0.10, 0.14), bm * 0.55);
    }
  }

  // ══════════════════════════ ENVELOPE (gas bag) ══════════════════════════
  {
    let Rx = 0.26;
    let Ry = 0.10;
    let ex = aq.x / Rx;
    let ey = aq.y / Ry;
    let f  = ex*ex + ey*ey - 1.0;
    let gr = 2.0 * length(vec2f(aq.x/(Rx*Rx), aq.y/(Ry*Ry)));
    let d  = f / max(gr, 1e-4);
    let aa = 0.006;
    let cover = smoothstep(aa, -aa, d);
    if (cover > 0.002) {
      let rx  = max(1.0 - ex*ex, 0.0);
      let zn2 = max(rx - ey*ey, 0.0);
      let zn  = sqrt(zn2);
      let N   = normalize(vec3f(aq.x/(Rx*Rx), aq.y/(Ry*Ry), zn/Ry));
      let V   = vec3f(0.0, 0.0, 1.0);

      let phi   = atan2(ey, zn + 0.0004);
      let Ngore = 14.0;
      let gg    = phi * (Ngore / 6.2831853);
      let sp    = gg - floor(gg + 0.5);
      let pid   = floor(gg + 0.5);
      let sd    = abs(sp);
      let seam  = smoothstep(0.055, 0.0, sd);
      let bulge = cos(sp * 3.14159);
      let stitch = smoothstep(0.05, 0.02, sd) * smoothstep(0.35, 0.95, sin(phi*170.0)*0.5+0.5);

      let rib   = cos(aq.y * 250.0) * 0.5 + 0.5;
      let weave = fbm3(aq * vec2f(80.0, 150.0)) - 0.5;

      let Lk = normalize(vec3f(0.55, 0.42, 0.55));
      let Lf = normalize(vec3f(-0.35, -0.92, 0.32));
      let warm = mod_tgo_suncol(sel);
      let sky  = vec3f(0.50, 0.60, 0.74);
      let Hk   = normalize(Lk + V);
      let diffK = max(dot(N, Lk), 0.0);
      let diffF = max(dot(N, Lf), 0.0);

      var albedo = vec3f(0.86, 0.80, 0.67);
      albedo *= 0.94 + 0.09 * hash11(pid * 3.17 + 1.7);
      albedo *= 1.0 + weave * 0.06;

      let ambient = 0.30;
      var fab = albedo * (ambient * (0.6 + 0.4*sky)
                          + diffK * 1.20 * warm
                          + diffF * 0.40 * sky);
      fab += pow(max(dot(N, Hk), 0.0), 6.0) * warm * 0.10;
      fab *= mix(0.80, 1.0, bulge);
      fab *= 1.0 - seam * 0.35;
      fab += stitch * warm * 0.06;
      fab *= 1.0 + (rib - 0.5) * 0.05;
      let sil = pow(1.0 - clamp(N.z, 0.0, 1.0), 3.0);
      let sunward = max(dot(normalize(N.xy + vec2f(1e-4, 0.0)), normalize(Lk.xy)), 0.0);
      fab += sil * sunward * warm * 0.22;

      let noseM  = smoothstep(-0.205, -0.250, aq.x);
      let tailM  = smoothstep( 0.208,  0.252, aq.x);
      let brassM = max(noseM, tailM);
      var brassAlb = vec3f(0.66, 0.48, 0.22);
      brassAlb = mix(vec3f(0.40, 0.50, 0.40), brassAlb, smoothstep(0.0, 0.5, brassM));
      let specB = pow(max(dot(N, Hk), 0.0), 42.0);
      var brass = brassAlb * (0.26 + diffK*1.25*warm + diffF*0.30*sky) + specB * warm * 1.6;
      let bandm  = max(smoothstep(0.012, 0.0, abs(aq.x + 0.205)),
                       smoothstep(0.012, 0.0, abs(aq.x - 0.208)));
      let rivet = smoothstep(0.55, 0.95, sin(phi*90.0)*0.5+0.5);
      brass = mix(brass, brass*1.35 + warm*0.18*rivet, bandm);

      var col = mix(fab, brass, brassM);

      let crownRim = pow(1.0 - clamp(N.z, 0.0, 1.0), 2.3) * smoothstep(-0.05, 0.85, -N.y);
      col += crownRim * warm * 1.05;

      col *= 1.0 - smoothstep(0.05, 0.85, N.y) * 0.32;

      let ao = smoothstep(0.11, 0.0, abs(aq.x))
               * smoothstep(0.045, 0.0, abs(aq.y - 0.088));
      col *= 1.0 - ao * 0.45;

      c = mix(c, col, cover * dawn);
    }
  }

  // ══════════ EMPENNAGE (fins + stabilizers + rudder + guy-wires) ══════════
  {
    let PI2 = 6.2831853;
    let aa = 0.0018;
    let sunc = mod_tgo_suncol(sel);
    let ldir = normalize(vec2f(0.72, 0.34));
    let skyc = vec3f(0.56, 0.64, 0.80);
    let fab = vec3f(0.86, 0.80, 0.66);
    let brass = vec3f(0.72, 0.50, 0.24);
    let brassHi = mix(vec3f(1.0, 0.86, 0.55), sunc, 0.4);
    let steel = vec3f(0.70, 0.73, 0.78);
    let trimw = 0.0065;

    // horizontal stabilizer (edge-on, behind)
    {
      let hc = vec2f(0.235, 0.004);
      let hq = aq - hc;
      let rr = vec2f(0.090, 0.020);
      let de = length(hq / rr) - 1.0;
      let dHS = de * 0.020;
      let cov = smoothstep(aa, -aa, dHS);
      if (cov > 0.001) {
        var col = fab * 0.90;
        col *= (1.0 + 0.04 * sin(hq.x * PI2 / 0.020));
        col *= (0.96 + 0.05 * fbm3(aq * 34.0));
        let kk = clamp(dot(normalize(hq + vec2f(0.0, 0.001)), ldir), 0.0, 1.0);
        col += sunc * kk * 0.10;
        let tr = smoothstep(-trimw, -0.001, dHS);
        col = mix(col, mix(brass, brassHi, 0.5), tr * 0.9);
        let seam = smoothstep(0.0016, 0.0, abs(hq.x - 0.052)) * step(hq.x, 0.082);
        col *= (1.0 - 0.4 * seam);
        c = mix(c, col, cov * dawn);
      }
    }

    // taut guy-wires bracing fins to the envelope (fine-line LOD: shore only)
    if (uni(113) < 0.0005) {
      let wW = 0.0018;
      let tipD = vec2f(0.272, -0.126);
      let tipV = vec2f(0.260,  0.080);
      let a1 = vec2f(0.050, -0.082);
      let a2 = vec2f(0.140, -0.058);
      let a3 = vec2f(0.062,  0.080);
      let dw1 = sdSegment(aq, tipD, a1);
      let dw2 = sdSegment(aq, tipD, a2);
      let dw3 = sdSegment(aq, tipV, a3);
      let dw = min(min(dw1, dw2), dw3);
      let cov = smoothstep(wW + aa, wW - aa, dw);
      if (cov > 0.001) {
        var col = steel;
        col += sunc * 0.12;
        col = mix(col, col * skyc, 0.15);
        c = mix(c, col, cov * dawn * 0.8);
      }
    }

    // ventral fin
    {
      let vF = vec2f(0.172, 0.000);
      let vK = vec2f(0.290, 0.000);
      let vT = vec2f(0.262, 0.086);
      let e0 = vK - vF; let n0 = normalize(vec2f(e0.y, -e0.x));
      let e1 = vT - vK; let n1 = normalize(vec2f(e1.y, -e1.x));
      let e2 = vF - vT; let n2 = normalize(vec2f(e2.y, -e2.x));
      let d0 = dot(aq - vF, n0);
      let d1 = dot(aq - vK, n1);
      let d2 = dot(aq - vT, n2);
      let d = max(max(d0, d1), d2);
      let cov = smoothstep(aa, -aa, d);
      if (cov > 0.001) {
        let ctr = vec2f(0.241, 0.029);
        var col = fab * 0.94;
        let ao = mix(0.72, 1.0, smoothstep(0.0, 0.060, aq.y));
        col *= ao;
        let kk = clamp(dot(normalize(aq - ctr), ldir), 0.0, 1.0);
        col += sunc * kk * 0.16;
        col *= (1.0 + 0.045 * sin(aq.x * PI2 / 0.017));
        let rl = smoothstep(0.80, 0.99, cos(aq.x * PI2 / 0.017));
        col *= (1.0 - 0.10 * rl);
        col *= (0.96 + 0.05 * fbm3(aq * 36.0));
        let seam = smoothstep(0.0016, 0.0, abs(d1 + 0.026 + 0.0012 * sin(t * 0.7)));
        col *= (1.0 - 0.45 * seam * cov);
        let tr = smoothstep(-trimw, -0.0008, d);
        col = mix(col, mix(brass, brassHi, kk), tr * 0.92);
        c = mix(c, col, cov * dawn);
      }
    }

    // dorsal fin + rudder (frontmost)
    {
      let vF = vec2f(0.160,  0.000);
      let vT = vec2f(0.276, -0.135);
      let vK = vec2f(0.300,  0.000);
      let e0 = vT - vF; let n0 = normalize(vec2f(e0.y, -e0.x));
      let e1 = vK - vT; let n1 = normalize(vec2f(e1.y, -e1.x));
      let e2 = vF - vK; let n2 = normalize(vec2f(e2.y, -e2.x));
      let d0 = dot(aq - vF, n0);
      let d1 = dot(aq - vT, n1);
      let d2 = dot(aq - vK, n2);
      let d = max(max(d0, d1), d2);
      let cov = smoothstep(aa, -aa, d);
      if (cov > 0.001) {
        let ctr = vec2f(0.245, -0.045);
        var col = fab;
        let ao = mix(0.70, 1.0, smoothstep(0.0, 0.075, -aq.y));
        col *= ao;
        let topF = smoothstep(0.0, -0.11, aq.y);
        col = mix(col, col * skyc * 1.5, topF * 0.22);
        let kk = clamp(dot(normalize(aq - ctr), ldir), 0.0, 1.0);
        col += sunc * kk * 0.14;
        col *= (1.0 + 0.045 * sin(aq.x * PI2 / 0.017));
        let rl = smoothstep(0.80, 0.99, cos(aq.x * PI2 / 0.017));
        col *= (1.0 - 0.10 * rl);
        col *= (0.96 + 0.05 * fbm3(aq * 36.0));
        let hinge = -0.030 + 0.0015 * sin(t * 0.7);
        let seam = smoothstep(0.0016, 0.0, abs(d1 - hinge));
        col *= (1.0 - 0.45 * seam * cov);
        let rud = smoothstep(hinge, hinge + 0.004, d1);
        col = mix(col, col * 1.04, rud * 0.6);
        let tr = smoothstep(-trimw, -0.0008, d);
        col = mix(col, mix(brass, brassHi, kk), tr * 0.92);
        c = mix(c, col, cov * dawn);
        let k1 = sdCircle(aq - vec2f(0.283, -0.085), 0.0042);
        let k2 = sdCircle(aq - vec2f(0.290, -0.030), 0.0042);
        let kc = smoothstep(aa, -aa, min(k1, k2));
        c = mix(c, mix(brass, brassHi, 0.6), kc * dawn);
      }
    }
  }

  // ═══════════════════════ GONDOLA (teak-and-brass cabin) ═══════════════════════
  {
    let gc = vec2f(0.0, 0.15);
    let gq = aq - gc;
    if (abs(gq.x) < 0.145 && abs(gq.y) < 0.09) {
      let hb  = vec2f(0.086, 0.036);
      let aaU = 0.0018;
      let sun = mod_tgo_suncol(sel);
      let keyDir  = normalize(vec2f(0.72, 0.60));
      let fillDir = normalize(vec2f(-0.25, -1.0));

      let sag = 0.012 * (1.0 - smoothstep(0.0, hb.x, abs(gq.x)));
      let bb  = vec2f(hb.x, hb.y + sag * 0.5);
      let gqh = vec2f(gq.x, gq.y - sag * 0.5);
      let dHull = sdRoundedBox(gqh, bb, 0.019);
      let hull  = smoothstep(aaU, -aaU, dHull);

      let nrm  = normalize(vec2f(gq.x / hb.x, gq.y / (hb.y * 1.6)) + vec2f(0.0, 0.0002));
      let diff = max(dot(nrm, keyDir), 0.0);
      let fill = max(dot(nrm, fillDir), 0.0);

      var col   = vec3f(0.0);
      var alpha = 0.0;

      if (hull > 0.001) {
        var grain = fbm3(vec2f(gq.x * 30.0, gq.y * 150.0));
        grain += 0.30 * fbm3(vec2f(gq.x * 85.0 + 4.0, gq.y * 30.0));
        var teak = mix(vec3f(0.33, 0.175, 0.085), vec3f(0.47, 0.285, 0.15),
                       clamp(grain * 0.6 + 0.4, 0.0, 1.0));

        let plankN = 4.0;
        let ph = fract((gq.y / (hb.y * 2.0) + 0.5) * plankN);
        let seam = smoothstep(0.0, 0.09, ph) * smoothstep(0.0, 0.09, 1.0 - ph);
        teak *= mix(0.60, 1.0, seam);

        var lit = vec3f(0.32)
                + 0.95 * diff * sun
                + 0.26 * fill * vec3f(0.5, 0.63, 0.85);
        let belly = smoothstep(0.15, 1.0, gq.y / hb.y);
        lit += belly * 0.55 * sun * vec3f(1.0, 0.86, 0.70);
        let topAO = smoothstep(-hb.y * 1.05, -hb.y * 0.15, gq.y);
        lit *= mix(0.5, 1.0, topAO);
        col = teak * lit;

        let brass = vec3f(0.72, 0.52, 0.22);
        let brassLit = brass * (0.42 + 0.85 * diff) * (0.6 + 0.4 * sun)
                     + sun * pow(diff, 8.0) * 0.6;
        let railD = abs(gq.y - (-hb.y + 0.006)) - 0.0055;
        let rail  = smoothstep(aaU, -aaU, railD);
        let bx    = abs(gq.x) - 0.068;
        let bandD = abs(bx) - 0.0055;
        let band  = smoothstep(aaU, -aaU, bandD);
        let brassMask = max(rail, band) * hull;
        let rivY  = fract(gq.y * 42.0);
        let rivet = smoothstep(0.30, 0.18, abs(rivY - 0.5)) * band * hull;
        col = mix(col, brassLit, brassMask * 0.95);
        col = mix(col, brassLit * 1.25 + 0.10, rivet * 0.6);

        alpha = hull;
      }

      // windows: 3 warm-lit glass panes
      {
        let wx   = gq.x - clamp(round(gq.x / 0.046), -1.0, 1.0) * 0.046;
        let winY = -0.003;
        let wq   = vec2f(wx, gq.y - winY);
        let wHalf = vec2f(0.0125, 0.0135);
        let dWin  = sdRoundedBox(wq, wHalf, 0.004);
        let winIn = smoothstep(aaU, -aaU, dWin) * hull;
        let frame = smoothstep(aaU, -aaU, abs(dWin - 0.0006) - 0.0026) * hull;
        if (winIn > 0.001 || frame > 0.001) {
          let brassF = vec3f(0.75, 0.55, 0.24) * (0.5 + 0.8 * diff);
          let paneV = wq.y / wHalf.y;
          let core  = smoothstep(1.0, 0.0, length(wq / wHalf));
          let warmLamp = vec3f(1.05, 0.74, 0.36);
          let coolSky  = vec3f(0.48, 0.60, 0.82);
          var glass = warmLamp * (0.55 + 0.85 * core);
          let refl  = smoothstep(0.2, -0.9, paneV);
          glass = mix(glass, coolSky, 0.40 * refl);
          let streak = smoothstep(0.03, 0.0, abs(wq.x + wq.y * 0.8 - 0.004));
          glass += vec3f(0.9, 0.95, 1.0) * streak * 0.35;
          col = mix(col, brassF, frame);
          col = mix(col, glass, winIn);
          alpha = max(alpha, max(winIn, frame));
        }
      }

      c = mix(c, col, clamp(alpha, 0.0, 1.0) * dawn);

      // keel skid + contact shadow
      {
        let skidY = hb.y + 0.006 - 0.012 * (1.0 - smoothstep(0.0, hb.x * 0.9, abs(gq.x)));
        let dSkid = sdSegment(gq, vec2f(-hb.x * 0.72, skidY), vec2f(hb.x * 0.72, skidY)) - 0.0035;
        let skid  = smoothstep(aaU, -aaU, dSkid);
        let skidLit = max(dot(normalize(vec2f(0.4, 1.0)), keyDir), 0.0);
        let skidCol = vec3f(0.60, 0.44, 0.20) * (0.45 + 0.8 * skidLit) + sun * 0.30;
        let contact = smoothstep(0.012, 0.0, dSkid) * (1.0 - skid);
        c = mix(c, c * 0.6, contact * 0.5 * dawn);
        c = mix(c, skidCol, skid * dawn);
      }

      // rudder at stern + brass hinge pin
      {
        let rq   = gq - vec2f(hb.x + 0.010, 0.002);
        let dRud = sdRoundedBox(rq, vec2f(0.011, 0.019), 0.004);
        let rud  = smoothstep(aaU, -aaU, dRud);
        let rgrain = fbm3(vec2f(rq.x * 120.0, rq.y * 60.0));
        let rudLit = max(dot(normalize(vec2f(0.8, 0.4)), keyDir), 0.0);
        let rudCol = mix(vec3f(0.30, 0.16, 0.08), vec3f(0.44, 0.26, 0.14), rgrain * 0.6 + 0.4)
                   * (0.4 + 0.9 * rudLit * length(sun));
        let pin = smoothstep(aaU, -aaU, sdCircle(rq - vec2f(-0.009, 0.0), 0.004));
        c = mix(c, rudCol, rud * dawn);
        c = mix(c, vec3f(0.80, 0.60, 0.28), pin * rud * dawn);
      }

      // warm lamp bloom leaking from the window row
      {
        let wx = gq.x - clamp(round(gq.x / 0.046), -1.0, 1.0) * 0.046;
        let wq = vec2f(wx, gq.y + 0.003);
        let g  = exp(-dot(wq, wq) / 0.00035);
        c += vec3f(1.0, 0.66, 0.30) * g * 0.22 * dawn;
      }
    }
  }

  // ═══════════ RIGGING (suspension between envelope and gondola) ═══════════
  // FINE-LINE LOD: thin dark strokes read as noise at cinematic scale (Galen)
  if (uni(113) < 0.0005) {
    let sunc      = mod_tgo_suncol(sel);
    let steelDark = vec3f(0.085, 0.095, 0.115);
    let steelLit  = mix(vec3f(0.58, 0.60, 0.66), sunc, 0.55) * 1.15;
    let brassDk   = mix(vec3f(0.34, 0.24, 0.12), sunc, 0.25);
    let brassLt   = mix(vec3f(0.86, 0.64, 0.32), sunc, 0.45) * 1.2;

    var rig    = 0.0;
    var rigCol = vec3f(0.0);
    var glint  = 0.0;
    var beadC  = 0.0;
    var beadCol= vec3f(0.0);

    let N = 9;
    for (var i = 0; i < N; i = i + 1) {
      let fr   = f32(i) / f32(N - 1);
      let topX = mix(-0.16, 0.16, fr);
      let botX = mix(-0.075, 0.075, fr);
      let eyU  = 0.10 * sqrt(max(0.0, 1.0 - (topX / 0.26) * (topX / 0.26)));
      let A    = vec2f(topX, eyU - 0.004);
      let B    = vec2f(botX, 0.117);
      let AB   = B - A;
      let L2   = max(dot(AB, AB), 1e-6);
      let u    = clamp(dot(aq - A, AB) / L2, 0.0, 1.0);
      let sag  = 0.009 * u * (1.0 - u);
      let curve= A + AB * u + vec2f(0.0, sag);
      let d    = length(aq - curve);
      let line = smoothstep(0.0028, 0.0014, d);
      let lit  = clamp(0.30 + 0.70 * smoothstep(-0.22, 0.20, curve.x), 0.0, 1.0);
      let col  = mix(steelDark, steelLit, lit);
      if (line > rig) { rig = line; rigCol = col; }
      glint = max(glint, line * lit * lit);

      let bA = smoothstep(0.0060, 0.0030, length(aq - A));
      let bB = smoothstep(0.0058, 0.0028, length(aq - B));
      let bl = clamp(0.30 + 0.70 * smoothstep(-0.22, 0.20, aq.x), 0.0, 1.0);
      let bc = mix(brassDk, brassLt, bl);
      let bcov = max(bA, bB);
      if (bcov > beadC) { beadC = bcov; beadCol = bc; }
    }

    // attachment batten following the envelope underside curve
    {
      let ey   = 0.10 * sqrt(max(0.0, 1.0 - (aq.x / 0.26) * (aq.x / 0.26))) - 0.004;
      let mask = step(abs(aq.x), 0.165);
      let dbat = abs(aq.y - ey);
      let bat  = smoothstep(0.0026, 0.0013, dbat) * mask;
      let lit  = clamp(0.30 + 0.70 * smoothstep(-0.22, 0.20, aq.x), 0.0, 1.0);
      let col  = mix(steelDark, steelLit, lit);
      if (bat > rig) { rig = bat; rigCol = col; }
      glint = max(glint, bat * lit * lit);
    }

    // faint load-net curtain over the lower envelope
    {
      let eyLow   = 0.10 * sqrt(max(0.0, 1.0 - (aq.x / 0.26) * (aq.x / 0.26)));
      let inLower = smoothstep(0.0, 0.020, aq.y)
                  * smoothstep(0.004, 0.022, eyLow - aq.y)
                  * step(abs(aq.x), 0.215);
      let g1  = abs(fract((aq.x + aq.y) * 26.0) - 0.5);
      let g2  = abs(fract((aq.x - aq.y) * 26.0) - 0.5);
      let net = (smoothstep(0.055, 0.0, g1) + smoothstep(0.055, 0.0, g2)) * inLower;
      let lit = clamp(0.35 + 0.65 * smoothstep(-0.22, 0.20, aq.x), 0.0, 1.0);
      c = mix(c, mix(steelDark, steelLit, lit), clamp(net, 0.0, 1.0) * 0.13 * dawn);
    }

    c = mix(c, rigCol, rig * 0.92 * dawn);
    c += sunc * glint * 0.35 * dawn;
    c = mix(c, beadCol, beadC * dawn);
    c += sunc * beadC * 0.25 * dawn;
  }

  // ═════════ PROPULSION (nacelles, motion-blur props, exhaust) ═════════
  {
    let dc = clamp(dawn, 0.0, 1.0);
    if (dc > 0.002) {
      let aa = 0.0022;
      let key      = mod_tgo_suncol(sel);
      let brass    = vec3f(0.42, 0.29, 0.13);
      let brassHi  = vec3f(0.98, 0.80, 0.44);
      let steel    = vec3f(0.66, 0.70, 0.78);
      let coolFill = vec3f(0.40, 0.50, 0.64);
      let L = normalize(vec3f(0.55, 0.45, 0.70));

      for (var i = 0; i < 2; i = i + 1) {
        let sx = select(-1.0, 1.0, i == 1);
        let nc = vec2f(sx * 0.10, 0.13);
        let bnac = vec2f(0.05, 0.026);
        let pc = nc + vec2f(-0.05, 0.0);
        let R = 0.05;

        // exhaust heat shimmer, trailing aft
        {
          let s0 = nc + vec2f(0.052, 0.0);
          let tv = aq - s0;
          let along = tv.x;
          if (along > -0.005 && along < 0.16) {
            let n = fbm3(vec2f(tv.x * 26.0 - t * 1.6, tv.y * 46.0 + f32(i) * 7.0));
            let wob = (n * 2.0 - 1.0);
            let yc = tv.y - wob * 0.014 * smoothstep(0.0, 0.10, along);
            let w = 0.014 + along * 0.12;
            let prof = smoothstep(w, 0.0, abs(yc));
            let fall = smoothstep(0.16, 0.0, along) * smoothstep(-0.005, 0.02, along);
            let heat = prof * fall;
            c += key * heat * 0.10 * dc;
            c = mix(c, c * vec3f(1.05, 1.015, 0.97), heat * 0.6 * dc);
          }
        }

        // outrigger strut
        {
          let a = nc + vec2f(0.0, -0.02);
          let b = vec2f(sx * 0.05, 0.145);
          let ds = sdSegment(aq, a, b);
          let cov = smoothstep(0.006, 0.003, ds) * dc;
          let scol = mix(brass * 0.7, brassHi, 0.30);
          c = mix(c, scol, cov * 0.9);
        }

        // nacelle body
        {
          let d = aq - nc;
          let sd = sdRoundedBox(d, bnac, 0.016);
          let cov = smoothstep(aa, -aa, sd) * dc;
          if (cov > 0.0) {
            let ny = clamp(d.y / bnac.y, -1.0, 1.0);
            let nz = sqrt(max(0.0, 1.0 - ny * ny));
            let nrm = vec3f(0.0, ny, nz);
            let diff = max(dot(nrm, L), 0.0);
            var col = brass * (0.30 + 0.75 * diff);
            col += coolFill * max(0.0, -ny) * 0.22;
            col += brassHi * pow(diff, 10.0) * 0.6;
            col *= 0.9 + 0.15 * fbm3(d * 40.0 + vec2f(f32(i) * 3.0));
            let rv = abs(fract(d.x * 36.0) - 0.5);
            let rrow = smoothstep(0.04, 0.02, abs(d.y - bnac.y * 0.55));
            col += brassHi * smoothstep(0.06, 0.0, rv) * rrow * 0.12;
            col *= mix(1.0, 0.7, smoothstep(0.0, -bnac.y, d.y) * 0.6);
            let rim = smoothstep(-0.004, 0.0, sd);
            col = mix(col, brassHi, rim * 0.25 * max(0.0, diff));
            c = mix(c, col, cov);
          }
        }

        // spinning propeller: translucent motion-blurred disc
        {
          let d = aq - pc;
          let rr = length(d);
          let rn = rr / R;
          if (rn < 1.06) {
            let ang = atan2(d.y, d.x);
            let omega = t * 11.0;
            let blades = 0.5 + 0.5 * sin(ang * 3.0 - omega);
            let fine   = 0.5 + 0.5 * sin(ang * 24.0 - omega * 3.0);
            let streak = mix(blades, blades * fine, 0.55);
            let a_rad = pow(clamp(1.0 - rn, 0.0, 1.0), 0.55);
            let core  = smoothstep(0.10, 0.30, rn);
            let alpha = a_rad * core * (0.09 + 0.24 * streak) * dc;
            let sweep = 0.5 + 0.5 * sin(ang - omega * 0.5);
            let tint = mix(steel * 0.7, brassHi, sweep * 0.8);
            c = mix(c, tint, clamp(alpha, 0.0, 0.85));
            let tip = smoothstep(0.82, 1.0, rn) * smoothstep(0.75, 1.0, streak)
                      * (1.0 - smoothstep(1.0, 1.06, rn));
            c += key * tip * 0.35 * dc;
          }
        }

        // glinting hub
        {
          let d = aq - pc;
          let hd = length(d);
          let hub = smoothstep(0.014, 0.010, hd) * dc;
          if (hub > 0.0) {
            let ny = clamp(d.y / 0.012, -1.0, 1.0);
            let nz = sqrt(max(0.0, 1.0 - ny * ny));
            let diff = max(dot(vec3f(0.0, ny, nz), L), 0.0);
            var hcol = mix(steel * 0.5, steel, diff);
            hcol += brassHi * pow(diff, 6.0) * 0.7;
            c = mix(c, hcol, hub);
            let pip = smoothstep(0.004, 0.0, length(d - vec2f(0.003, 0.003)));
            c += brassHi * pip * 0.8 * dc;
          }
        }
      }
    }
  }

  // ═════════ VENTED STEAM — wisp curling off the aft engine ═════════
  {
    var sp = aq - vec2f(0.12, 0.13);
    let rise = clamp(-sp.y, 0.0, 0.36);
    sp.x -= rise * 0.55;
    sp.x += 0.045 * sin(rise * 20.0 - t * 2.0);
    let core = exp(-(sp.x * sp.x) / (0.0016 + rise * 0.022));
    let life = smoothstep(0.0, 0.03, rise) * (1.0 - smoothstep(0.12, 0.36, rise));
    let puff = 0.45 + 0.55 * vnoise3(vec3f(sp.x * 11.0, sp.y * 11.0, t * 0.6));
    let steam = core * life * puff * dawn;
    c += mix(vec3f(0.86, 0.90, 0.96), atmSun, 0.20) * steam * 0.30;
  }

  // ══════════════════ NAV LIGHTS + GLOW (additive, on top) ══════════════════
  {
    let warm = mod_tgo_suncol(sel);
    let lampBoost = (0.80 + night * 0.55) * dawn;

    {
      let bq = aq / vec2f(0.32, 0.22);
      let bloom = exp(-dot(bq, bq) * 2.0);
      c += warm * bloom * 0.05 * dawn;
    }

    let twP = 0.92 + 0.08 * sin(t * 2.7 + 0.4);
    let twS = 0.92 + 0.08 * sin(t * 2.3 + 2.1);
    let twT = 0.85 + 0.15 * sin(t * 5.1 + 1.0);

    // PORT lamp (red)
    {
      let q = aq - vec2f(-0.135, 0.112);
      let r = length(q);
      let hot  = smoothstep(0.012, 0.0, r);
      let lamp = exp(-r * 44.0);
      let col  = vec3f(1.00, 0.11, 0.07);
      c += (col * lamp * 0.85 + mix(col, vec3f(1.0), 0.7) * hot * 1.4) * twP * lampBoost;
    }

    // STARBOARD lamp (green)
    {
      let q = aq - vec2f(0.135, 0.112);
      let r = length(q);
      let hot  = smoothstep(0.012, 0.0, r);
      let lamp = exp(-r * 44.0);
      let col  = vec3f(0.10, 1.00, 0.30);
      c += (col * lamp * 0.85 + mix(col, vec3f(1.0), 0.7) * hot * 1.4) * twS * lampBoost;
    }

    // TAIL beacon (warm-white)
    {
      let q = aq - vec2f(0.262, 0.0);
      let r = length(q);
      let hot  = smoothstep(0.011, 0.0, r);
      let lamp = exp(-r * 48.0);
      let col  = mix(vec3f(1.0), warm, 0.25);
      c += (col * lamp * 0.80 + vec3f(1.0) * hot * 1.5) * twT * lampBoost;
    }

    // NOSE headlamp (warm amber) + forward cone
    {
      let lp = vec2f(-0.258, 0.0);
      let q  = aq - lp;
      let r  = length(q);
      let hot  = smoothstep(0.016, 0.0, r);
      let lamp = exp(-r * 36.0);
      let col  = mix(vec3f(1.00, 0.82, 0.55), warm, 0.35);
      c += (col * lamp * 1.0 + vec3f(1.00, 0.93, 0.78) * hot * 1.6) * lampBoost;

      let fwd    = -q.x;
      let ahead  = smoothstep(0.0, 0.015, fwd);
      let reach  = exp(-fwd * 4.0);
      let spread = 0.045 + fwd * 0.5;
      let lat    = exp(-(q.y * q.y) / (spread * spread));
      let flick  = 0.9 + 0.1 * sin(t * 7.0);
      c += col * ahead * reach * lat * 0.14 * flick * dawn;
    }
  }

  // ═══════════ THE KEEL CRYSTAL (Act I finale) — slung under the gondola; the
  //   sea-cave's pearl-beam lights it, and only then will the helm turn. ═══════════
  {
    let powered = uni(110);
    let cq = aq - vec2f(0.0, 0.345);
    // a hang-wire from the keel down to the stone
    let wire = smoothstep(0.0028, 0.0, abs(cq.x)) * step(cq.y, 0.0) * step(-0.12, cq.y) * step(uni(113), 0.0005);
    c = mix(c, vec3f(0.14, 0.12, 0.09), wire * 0.6);
    // faceted crystal (a tapered bipyramid in 2D)
    let dia = abs(cq.x) / 0.028 + abs(cq.y) / 0.058 - 1.0;
    let body = smoothstep(0.05, -0.03, dia);
    let facet = 0.45 + 0.55 * sin(atan2(cq.y, cq.x) * 4.0 + 0.6);
    let dark = vec3f(0.10, 0.15, 0.20) * (0.5 + 0.7 * facet);
    let lit  = mix(vec3f(0.15, 0.55, 0.75), vec3f(0.7, 1.0, 1.0), facet) * (0.7 + 0.5 * sin(t * 4.0 + cq.y * 30.0));
    let cry  = mix(dark, lit, powered);
    c = mix(c, cry, body);
    // the crystal's own light once powered — a cool bloom + a hot core
    c += vec3f(0.20, 0.60, 0.85) * powered * exp(-dot(cq, cq) * 55.0) * (0.6 + 0.4 * sin(t * 3.0));
    c += vec3f(0.6, 0.95, 1.0) * powered * smoothstep(-0.02, -0.10, dia) * 0.5;
  }
  // the LASER out of the sea (lower-left, the sea-cave's bearing) to the crystal
  {
    let powered = uni(110) * step(uni(113), 0.0005);   // the works' beam belongs to the SHORE — never the crossing cinematic
    if (powered > 0.5) {
      let a = vec2f(-1.06, -0.13);          // TG_OCU — born at the crystal works' tall spire tip (half off-screen left)
      let b = ac + vec2f(0.0, 0.345);      // the keel crystal
      let ab = b - a; let ap = p - a;
      let hh = clamp(dot(ap, ab) / dot(ab, ab), 0.0, 1.0);
      let dd = length(ap - ab * hh);
      let core = exp(-dd * dd * 6500.0);
      let glow = exp(-dd * dd * 380.0);
      c += (vec3f(0.75, 1.0, 1.0) * core + vec3f(0.20, 0.60, 0.90) * glow) * (0.7 + 0.3 * sin(t * 16.0 - hh * 12.0));
    }
  }

  return c;
}

// ═══ THE SUMMONING (uni 115 = s, 0..1 over 12s). Phase map is LAW (shared with
//     the hook): 0–.12 bolt · .12–.35 portal opens · .30–.80 ship emerges ·
//     .80–.95 seal (flash .92) · ≥.95 clean sky. Portal center TG_PC sits
//     up-right of the mooring; the ship slides PC→mooring while its own dawn
//     fade brings it up — reads as coming THROUGH without a hard clip. ═══
fn tg_portalR(s: f32) -> f32 {
  // the aperture's radius over the summon clock — ONE truth for the portal draw
  // and the emergence clip (the ship must come OUT OF the hole, not fade behind it)
  return 0.26 * smoothstep(0.12, 0.35, s) * (1.0 - smoothstep(0.80, 0.95, s));
}
fn tg_portalAt(cIn: vec3f, p: vec2f, t: f32, s: f32, PCin: vec2f) -> vec3f {
  var c = cIn;
  let PC = PCin;
  let openA = smoothstep(0.12, 0.35, s);                 // iris out
  let sealA = 1.0 - smoothstep(0.80, 0.95, s);           // iris back in
  let R = tg_portalR(s);
  if (R < 0.004) {
    // the seal-flash: one bright collapse spark at s≈0.92
    let fl = exp(-abs(s - 0.92) * 90.0);
    c += vec3f(0.9, 0.8, 1.1) * fl * exp(-dot(p - PC, p - PC) * 120.0);
    return c;
  }
  let d = p - PC;
  let r = length(d);
  let a = atan2(d.y, d.x);
  // interior: an elsewhere-sky — darker, star-flecked, slow swirl
  let inA = smoothstep(R, R * 0.85, r);
  let sw = rot2(t * 0.15 + r * 6.0) * d;
  let stars = max(fbm(sw * 30.0 + vec2f(9.0), 2) - 0.62, 0.0) * 6.0;
  let inCol = vec3f(0.015, 0.020, 0.045) + vec3f(0.5, 0.6, 0.9) * stars * 0.35;
  c = mix(c, inCol, inA * 0.92);
  // rim: the same arcane ring-language as the suns — bright annulus + a slow
  // second ring precessing just inside it
  let rim = smoothstep(0.020, 0.004, abs(r - R));
  c += vec3f(0.55, 0.70, 1.15) * rim * (0.8 + 0.2 * sin(a * 6.0 - t * 2.0));
  let q = rot2(t * 0.4) * d;
  let e2 = abs(length(vec2f(q.x, q.y / 0.55)) - R * 0.82);
  c += vec3f(0.35, 0.45, 0.85) * smoothstep(0.014, 0.003, e2) * 0.5;
  // filament lightning crawling the rim: angular noise spikes, always ≥ 0
  let fil = max(fbm(vec2f(a * 3.0, t * 2.2), 2) - 0.55, 0.0) * 4.0;
  c += vec3f(0.75, 0.80, 1.2) * fil * smoothstep(0.05, 0.0, abs(r - R)) * 0.8;
  // breath of light spilling from the mouth
  c += vec3f(0.20, 0.28, 0.55) * exp(-max(r - R, 0.0) * 9.0) * 0.35 * openA * sealA;
  return c;
}
fn tg_portal(cIn: vec3f, p: vec2f, t: f32, s: f32) -> vec3f {
  return tg_portalAt(cIn, p, t, s, vec2f(-0.24, -0.66));   // the shore's arrival gate
}
fn tg_skybolt(cIn: vec3f, p: vec2f, t: f32, s: f32) -> vec3f {
  // the summons as seen from ANY outdoor sky: one violet-white flash-bolt
  // dropping toward the portal point during s 0–0.12.
  var c = cIn;
  let k = smoothstep(0.0, 0.02, s) * (1.0 - smoothstep(0.09, 0.13, s));
  if (k < 0.003) { return c; }
  let PC = vec2f(-0.24, -0.66);
  var m = 0.0;
  var aPt = vec2f(PC.x + 0.10, -1.05);
  for (var i = 0; i < 5; i = i + 1) {
    let fi = f32(i);
    let bPt = mix(aPt, PC, 0.30 + fi * 0.05) + vec2f(sin(fi * 17.7 + floor(t * 24.0)) * 0.05, 0.06);
    m = max(m, smoothstep(0.010, 0.0, sdSegment(p, aPt, bPt)));
    aPt = bPt;
  }
  m = max(m, smoothstep(0.010, 0.0, sdSegment(p, aPt, PC)));
  c += vec3f(0.85, 0.75, 1.25) * m * k;
  c += vec3f(0.30, 0.28, 0.55) * k * exp(-(p.y + 0.8) * (p.y + 0.8) * 4.0) * 0.25;  // sky afterglow
  return c;
}

// ═══ THE CAVE-HOUSE — the outside of the sea-cave machine, crouched on the
//     left rocks. Its brass oculus TG_OCU is where the keel-crystal ray is
//     born (was: bare ocean). Drawn always; powered (uni 110) wakes the eye. ═══
// TG_OCU = vec2f(-0.80, 0.355) ← THE shared anchor. The laser block below must
// use this exact value as `a`.
fn tg_cavehouse(cIn: vec3f, p: vec2f, t: f32, night: f32, dawn: f32) -> vec3f {
  // ═══ THE CRYSTAL WORKS (Galen Aug 10 v2): not a hut — a distant crystal
  //     building half off the LEFT edge, behind the bell-buoys, subtle against
  //     the dusk. Three faceted spires rising off the waterline; the tallest
  //     tip is TG_OCU where the keel-crystal ray is born (uni 110). ═══
  var c = cIn;
  let H = vec2f(-1.02, 0.17);                    // base center — half off-screen, on the sea line
  let d = p - H;
  if (d.x > 0.34 || abs(d.y) > 0.42) { return c; }
  let powered = uni(110);
  let haze = 0.62;                               // distance: everything blends toward sky
  // three spires: (dx, half-width, height). Tallest carries the oculus.
  for (var i = 0; i < 3; i = i + 1) {
    var sx = 0.08; var hw = 0.052; var ht = 0.21;
    if (i == 1) { sx = -0.04; hw = 0.075; ht = 0.30; }        // the tall one
    if (i == 2) { sx = 0.19;  hw = 0.045; ht = 0.14; }
    let q = d - vec2f(sx, 0.0);
    // a tapered crystal: width shrinks with height, faceted by a fold on |x|
    let up = clamp(-q.y / ht, 0.0, 1.0);
    let w = hw * (1.0 - up * 0.82);
    let inside = step(abs(q.x), w) * step(-ht, q.y) * step(q.y, 0.015);
    if (inside > 0.5) {
      // facet shading: a hard crease down the middle, dusk key from the right
      let side = step(0.0, q.x);
      var fc = mix(vec3f(0.070, 0.088, 0.110), vec3f(0.135, 0.150, 0.170), side);
      fc += vec3f(0.16, 0.11, 0.06) * side * (1.0 - up) * 0.5;          // sunset kiss on the lit face
      fc += vec3f(0.06, 0.14, 0.16) * (0.5 + 0.5 * sin(q.y * 90.0 + f32(i) * 2.1)) * 0.14;  // internal lamination
      // faint inner life — a slow breathing glow, brighter when powered
      fc += vec3f(0.10, 0.30, 0.36) * exp(-abs(q.x) * 26.0) * (0.10 + 0.22 * powered) * (0.6 + 0.4 * sin(t * 0.9 + f32(i)));
      c = mix(c, fc, haze);                       // hazy: the sky bleeds through — it reads distant
    }
    // crest spark at each tip
    let tipd = length(q - vec2f(0.0, -ht));
    c += vec3f(0.45, 0.70, 0.80) * exp(-tipd * tipd * 2600.0) * (0.10 + 0.30 * powered);
  }
  // waterline root: a dark shelf + its broken reflection
  let shelf = step(abs(d.y - 0.02), 0.018) * step(abs(d.x - 0.06), 0.30)
  ;
  c = mix(c, vec3f(0.045, 0.050, 0.058), shelf * 0.5);
  c += vec3f(0.10, 0.12, 0.13) * step(0.02, d.y) * step(d.y, 0.16) * step(abs(d.x + 0.04), 0.10)
       * (0.5 + 0.5 * sin(d.y * 120.0 + t * 1.4)) * 0.10;
  // TG_OCU — the tall spire's tip, where the ray is born (world anchor -1.06+0.0? see laser)
  let od = p - vec2f(-1.06, -0.13);
  let orr = length(od);
  c += vec3f(0.25, 0.70, 0.95) * exp(-orr * orr * 2400.0) * powered * (0.7 + 0.3 * sin(t * 3.0));
  c += vec3f(0.10, 0.35, 0.55) * exp(-orr * orr * 300.0) * powered * 0.4;
  return c;
}

module · tg_flycine

// ══ THE CROSSING — Act I finale cinematic (uni 113 = timeline 0..1). Not a
//    baked video: every frame computes live from the timeline — the pixel-
//    playback spirit. The helm flash → the airship, belly lit by fire, crosses
//    an OCEAN OF FLAMES under a smoke sky and the ruling moon → the ACT I card
//    holds until a tap. Author: Claude Fable 5.

fn tgc_h1(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgc_n2(p: vec2f) -> f32 {
  let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
  return mix(mix(tgc_h1(i), tgc_h1(i + vec2f(1.0, 0.0)), u.x),
             mix(tgc_h1(i + vec2f(0.0, 1.0)), tgc_h1(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgc_fbm(p: vec2f, oct: i32) -> f32 {
  var v = 0.0; var a = 0.5; var q = p;
  for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgc_n2(q); q = q * 2.07 + vec2f(11.0, 5.0); a *= 0.5; }
  return v;
}
fn tgc_seg(p: vec2f, a: vec2f, b: vec2f) -> f32 {
  let pa = p - a; let ba = b - a;
  let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h);
}
// a stroked letter mark: bright where within w of the stroke
fn tgc_stroke(d: f32, w: f32) -> f32 { return smoothstep(w, w - 1.4, d); }

// ── THE FLAME SEA: an emissive rolling fire-field on the y=0 plane ──
fn tgc_flamesea(ro: vec3f, rd: vec3f, t: f32) -> vec3f {
  let ts = -ro.y / rd.y;                    // rd.y < 0 guaranteed by caller
  var sp = ro + rd * ts;
  // rolling swell + fine crackle, drifting toward the horizon
  let drift = vec2f(t * 0.35, t * 0.9);
  var hgt = tgc_fbm(sp.xz * 0.33 + drift * 0.3, 4);
  // parallax lick: re-sample where the crest would displace the surface
  sp += rd * hgt * 1.3;
  hgt = tgc_fbm(sp.xz * 0.33 + drift * 0.3, 4);
  let crackle = tgc_fbm(sp.xz * 1.7 + drift, 3);
  let tongues = pow(tgc_fbm(sp.xz * 0.9 - drift * 0.6, 3), 2.0);
  // emissive palette: deep ember → orange body → yellow crest → white-hot tips
  var f = clamp(hgt * 0.75 + crackle * 0.45 + tongues * 0.5, 0.0, 1.6);
  var col = mix(vec3f(0.22, 0.025, 0.008), vec3f(0.95, 0.26, 0.03), smoothstep(0.15, 0.7, f));
  col = mix(col, vec3f(1.35, 0.85, 0.22), smoothstep(0.7, 1.05, f));
  col = mix(col, vec3f(1.6, 1.35, 0.9), smoothstep(1.05, 1.35, f));
  // living flicker
  col *= 0.85 + 0.3 * tgc_n2(sp.xz * 3.0 + vec2f(t * 4.0, t * 5.0));
  // distance: the fire sinks into its own smoke toward the horizon
  let dist = clamp(ts * 0.055, 0.0, 1.0);
  col = mix(col, vec3f(0.30, 0.06, 0.02), dist * 0.85);
  return col;
}

// ── THE AIRSHIP, crossing — a silhouette underlit by the burning sea ──
fn tgc_ship(cIn: vec3f, p: vec2f, s: f32, t: f32) -> vec3f {
  // THE REAL SHIP crosses (Galen Aug 10: the cinematic ship must BE the shore
  // ship). flycine compiles last, so it borrows mod_tg_airship whole under an
  // affine map: cine pos/scale/bank → the ship's home frame at ac(-0.58,-0.50),
  // envelope 0.26/0.34 matching the old silhouette's proportions. Its sea-
  // reflection band maps below the frame at these scales; the works' laser is
  // gated off during the cine inside tg_air.
  var c = cIn;
  let e = smoothstep(0.04, 0.88, s);                        // eased leg of the crossing
  var pos = vec2f(mix(-0.30, 0.14, e), mix(0.10, -0.335, pow(e, 0.8)));
  // THROUGH the gate (Galen): past the mouth the path keeps going along the
  // entry direction, so the clip plane swallows her nose-to-tail; gone by s=.94
  pos += normalize(vec2f(0.24, -0.23)) * smoothstep(0.68, 0.94, s) * 0.30;
  let scale = mix(0.66, 0.115, pow(e, 0.85));
  let bank = mix(0.07, -0.03, e) + 0.010 * sin(t * 0.5);
  let cb = cos(bank); let sb = sin(bank);
  var q = (p - pos - vec2f(0.006 * sin(t * 0.23), 0.010 * sin(t * 0.19))) / scale;
  q = vec2f(q.x * cb - q.y * sb, q.x * sb + q.y * cb);
  let pm = vec2f(-0.58, -0.50) + q * 0.7647;                // cine local → ship home frame
  c = mod_tg_airship(c, pm, t, 1.0, -0.16, 0.2);
  // THE FIRE BELOW still paints her belly amber — the cine's signature light,
  // added over the real hull in cine-local coords (same formula as the old cut)
  let env = length(q / vec2f(0.34, 0.125)) - 1.0;
  let belly = smoothstep(-0.02, 0.10, q.y) * smoothstep(0.30, 0.20, abs(q.x)) * step(env, 0.0);
  c += vec3f(0.95, 0.34, 0.07) * belly * (0.35 + 0.12 * sin(t * 3.1 + q.x * 9.0));
  // hot rim hugging the envelope's lower edge — BANDED, not a half-plane (the
  // unmasked version washed everything below the centerline + made the clip wedge)
  c += vec3f(1.2, 0.5, 0.12) * smoothstep(-0.06, -0.015, env) * (1.0 - smoothstep(0.0, 0.045, env)) * step(0.0, q.y) * 0.30;
  return c;
}

fn mod_tg_flycine(p: vec2f, px: vec2f, t: f32) -> vec3f {
  let s = clamp(uni(113), 0.0, 1.0);
  // camera: fixed wide shot over the burning sea
  let ro = vec3f(0.0, 2.3, 6.0);
  var uvx = px.x / 256.0 - 1.0;
  var uvy = -(px.y / 256.0 - 1.0);
  // heat shimmer: the whole frame breathes near the fire line
  let heat = smoothstep(0.25, -0.35, uvy) * 0.012;
  uvx += (tgc_n2(vec2f(uvx * 7.0, uvy * 7.0 - t * 2.4)) - 0.5) * heat * 2.0;
  uvy += (tgc_n2(vec2f(uvx * 6.0 + 9.0, uvy * 6.0 - t * 3.1)) - 0.5) * heat;
  let rd = normalize(vec3f(uvx, uvy * 0.75 - 0.14, -1.25));
  // ── SKY: smoke banks lit from beneath, the ruling moon drowned in haze ──
  // sky lifted (Galen: "just dark") — the smoke itself burns, dark red up top,
  // molten toward the fire line, so the frame reads FIRE not void
  var c = mix(vec3f(0.30, 0.075, 0.030), vec3f(0.10, 0.030, 0.035), clamp(rd.y * 2.2 + 0.4, 0.0, 1.0));
  let smoke = tgc_fbm(vec2f(rd.x * 3.0 + t * 0.10, rd.y * 4.5 - t * 0.05), 4);
  c = mix(c, vec3f(0.10, 0.035, 0.03), smoothstep(0.35, 0.75, smoke) * 0.8);
  c += vec3f(1.0, 0.36, 0.08) * exp(-abs(rd.y + 0.02) * 9.0) * 0.85;          // the fire's under-glow on the smoke
  c += vec3f(0.55, 0.16, 0.05) * exp(-abs(rd.y - 0.30) * 3.0) * 0.30;         // high smoke banks catch the burn too
  // THE FIRE MOON, FURIOUS — the burning eye that rules this crossing (Galen:
  // "a really angry version of the fire moon"). Molten core, seething prominence
  // licks, a wrath-corona that ignites the smoke around it.
  let mdir = normalize(vec3f(0.46, 0.44, -1.0));
  let md = distance(normalize(rd), mdir);
  let mang = atan2(rd.y - mdir.y, rd.x - mdir.x);
  let rage = 0.82 + 0.18 * sin(t * 6.3) * sin(t * 2.9 + 1.7);
  let mlick = 0.5 + 0.5 * sin(mang * 9.0 - t * 4.2 + tgc_n2(vec2f(mang * 2.0 + 9.0, t * 0.9)) * 5.5);
  let mR = 0.050 * (1.0 + mlick * 0.28);
  let mbody = smoothstep(mR, mR * 0.5, md);
  var mcol = mix(vec3f(1.35, 0.28, 0.05), vec3f(1.6, 0.95, 0.38), smoothstep(0.040, 0.0, md));
  mcol *= 0.85 + 0.45 * tgc_n2(vec2f(mang * 3.0, md * 60.0 - t * 1.6));       // churning surface
  c = mix(c, mcol * rage, mbody * 0.96);
  c += vec3f(1.25, 0.24, 0.05) * exp(-md * md * 150.0) * 0.85 * rage;         // furious corona
  c += vec3f(0.80, 0.12, 0.03) * exp(-md * md * 26.0) * 0.38;                 // wide wrath-glow
  c += vec3f(0.55, 0.13, 0.04) * smoke * exp(-md * md * 36.0) * 0.6;          // the smoke itself ignites around it
  // ── THE OCEAN OF FLAMES ──
  if (rd.y < -0.015) {
    let sea = tgc_flamesea(ro, rd, t);
    c = mix(c, sea, smoothstep(-0.015, -0.05, rd.y));
  }
  // fire-line blaze right at the horizon
  c += vec3f(1.2, 0.5, 0.1) * exp(-pow((rd.y + 0.015) * 26.0, 2.0)) * (0.5 + 0.25 * tgc_n2(vec2f(rd.x * 11.0, t * 1.7)));
  // ── EMBERS, rising on the heat ──
  {
    let cell = vec2f(px.x * 0.055, px.y * 0.055 + t * 1.35);
    let id = floor(cell); let fr = fract(cell) - 0.5;
    let sp = tgc_h1(id);
    let em = smoothstep(0.16, 0.02, length(fr - (vec2f(tgc_h1(id + 7.0), tgc_h1(id + 13.0)) - 0.5) * 0.6));
    c += vec3f(1.2, 0.55, 0.12) * em * step(0.965, sp) * (0.4 + 0.6 * sin(t * 5.0 + sp * 40.0)) * smoothstep(-0.45, 0.1, -uvy + 0.4);
  }
  // ── THE SHIP crosses it all — and leaves through its own gate (Galen):
  //    an exit portal opens ahead on the path's far end; the ship is clipped
  //    INTO the hole (parts past the portal plane are already elsewhere). ──
  let XPC = vec2f(0.16, -0.34);                       // where the path ends
  let xphase = smoothstep(0.52, 0.60, s) * 0.35;       // hold the aperture OPEN (tg_portalR peaks ~0.35)
  if (s < 0.945) {
    let before = c;
    c = tgc_ship(c, p, s, t);
    if (s > 0.52) {
      let entryDir = normalize(XPC - vec2f(-0.08, -0.11));   // travel direction at the mouth
      let inHole = smoothstep(0.015, -0.015, length(p - XPC) - tg_portalR(xphase));
      let notYet = smoothstep(0.03, -0.01, dot(p - XPC, entryDir));   // still on our side
      c = mix(before, c, max(inHole, notYet));
    }
  }
  if (s > 0.50 && s < 0.97) { c = tg_portalAt(c, p, t, mix(0.12, 0.95, smoothstep(0.50, 0.96, s)), XPC); }
  // ── the helm-pull FLASH opens the shot ──
  c = mix(c, vec3f(1.0, 0.98, 0.92), smoothstep(0.10, 0.0, s));
  // ── THE ACT I CARD (s ≥ 0.82): vignette falls, the mark burns in ──
  let card = smoothstep(0.82, 0.96, s);
  if (card > 0.001) {
    c *= 1.0 - card * 0.62 * smoothstep(1.35, 0.35, length(p));                // the dark comes down
    let gold = vec3f(1.0, 0.84, 0.45);
    var mk = 0.0;
    // A
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(203.0, 238.0), vec2f(214.0, 206.0)), 2.6));
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(214.0, 206.0), vec2f(225.0, 238.0)), 2.6));
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(208.0, 227.0), vec2f(221.0, 227.0)), 2.2));
    // C — a ring with its right side open
    {
      let cd = px - vec2f(247.0, 222.0);
      let ring = abs(length(cd) - 15.0);
      mk = max(mk, tgc_stroke(ring, 2.6) * smoothstep(0.45, 0.25, cd.x / max(length(cd), 0.001)));
    }
    // T
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(268.0, 206.0), vec2f(290.0, 206.0)), 2.6));
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(279.0, 206.0), vec2f(279.0, 238.0)), 2.6));
    // the numeral I, set apart
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(308.0, 206.0), vec2f(308.0, 238.0)), 2.8));
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(303.0, 206.0), vec2f(313.0, 206.0)), 2.2));
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(303.0, 238.0), vec2f(313.0, 238.0)), 2.2));
    // ornament: — ◆ — beneath
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(216.0, 258.0), vec2f(244.0, 258.0)), 1.4));
    mk = max(mk, tgc_stroke(abs(px.x - 258.0) + abs(px.y - 258.0) - 5.0, 2.0));
    mk = max(mk, tgc_stroke(tgc_seg(px, vec2f(272.0, 258.0), vec2f(300.0, 258.0)), 1.4));
    c += gold * mk * card * (0.85 + 0.15 * sin(t * 1.8));
    c += gold * mk * card * 0.35 * exp(-tgc_seg(px, vec2f(256.0, 222.0), vec2f(256.0, 222.0)) * 0.0);   // soft self-glow
    // the tap-to-continue breath, once the card holds
    if (s >= 0.995) {
      c += vec3f(0.9, 0.78, 0.5) * tg_ring(px, vec2f(256.0, 388.0), 14.0, 11.5) * (0.25 + 0.25 * sin(t * 2.2));
    }
  }
  return max(c, vec3f(0.0));
}

module · tg_kiln

// THE KILN (view 14) — past the burned thicket. Raymarched: the old
// glassmaker's clearing, a beehive stone kiln with a glowing firebox, charred
// bramble framing the edges under a fire-moon night. The island's glass was
// born here; the fire moon has woken it. PAYOFF: click the mouth and a GLASS
// BLOOM rises on the lip (uni(104)). Shore recipe: SDF map → march → normal →
// shadow/AO → materials → fire key light. Anchor (px, LAW): kiln mouth
// (256,290) r60. State: uni(104) kilnBloom · uni(82) elBlend · uni(78) element ·
// mouse uni(27,28) · clickFx uni(84/85/86). Author: Claude Fable 5.

fn tgk_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgk_n2(p: vec2f) -> f32 {
  let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
  return mix(mix(tgk_h(i), tgk_h(i + vec2f(1.0, 0.0)), u.x),
             mix(tgk_h(i + vec2f(0.0, 1.0)), tgk_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgk_fbm(p: vec2f, oct: i32) -> f32 {
  var v = 0.0; var a = 0.5; var q = p;
  for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgk_n2(q); q = q * 2.03 + vec2f(9.0, 5.0); a *= 0.5; }
  return v;
}
fn tgk_sph(p: vec3f, r: f32) -> f32 { return length(p) - r; }
fn tgk_box(p: vec3f, b: vec3f) -> f32 { let q = abs(p) - b; return length(max(q, vec3f(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0); }
fn tgk_cap(p: vec3f, a: vec3f, b: vec3f, r: f32) -> f32 {
  let pa = p - a; let ba = b - a; let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h) - r;
}
fn tgk_smin(a: f32, b: f32, k: f32) -> f32 { let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); }

const TGK_KC = vec3f(0.0, 0.95, -1.1);   // kiln centre (mouth projects near px 256,290)

// SDF → (dist, matId). mats: 1 kiln stone · 2 mouth-arch brick · 3 ash ground ·
// 4 charred bramble · 5 kiln lip/plinth · 6 glass-bloom stem
fn tgk_map(p: vec3f, bloom: f32, t: f32) -> vec2f {
  var d = 1e5; var m = 1.0;
  // ── the beehive KILN: stacked stone domes narrowing upward ──
  let kp = p - TGK_KC;
  var kiln = tgk_sph(kp / vec3f(1.0, 1.15, 1.0), 1.05) * 1.0;
  kiln = tgk_smin(kiln, tgk_sph((kp - vec3f(0.0, 0.55, 0.0)) / vec3f(0.82, 1.0, 0.82), 0.78), 0.25);
  kiln = tgk_smin(kiln, tgk_sph((kp - vec3f(0.0, 1.05, 0.0)) / vec3f(0.6, 0.9, 0.6), 0.5), 0.22);
  // hollow it and cut the FIREBOX MOUTH — an arch on the front face
  kiln = max(kiln, -(tgk_sph(kp / vec3f(0.9, 1.05, 0.9), 0.9)));       // hollow shell
  let mouth = tgk_box(kp - vec3f(0.0, -0.25, 0.95), vec3f(0.42, 0.5, 0.5));
  let mouthArch = min(mouth, tgk_sph((kp - vec3f(0.0, 0.2, 0.95)) / vec3f(0.42, 0.42, 0.6), 0.42));
  kiln = max(kiln, -mouthArch);                                        // carve the opening
  if (kiln < d) { d = kiln; m = 1.0; }
  // arch brick lip around the mouth
  let ring = abs(length((kp - vec3f(0.0, -0.05, 0.98)).xy / vec2f(0.5, 0.62)) - 1.0) - 0.09;
  let brick = max(ring, abs(kp.z - 0.98) - 0.14);
  if (brick < d) { d = brick; m = 2.0; }
  // ── the ASH GROUND ──
  let g = p.y + 0.35 + 0.05 * tgk_fbm(p.xz * 3.0, 3);
  if (g < d) { d = g; m = 3.0; }
  // stone PLINTH the kiln sits on + a work-LIP shelf at the mouth
  let plinth = tgk_box(p - vec3f(0.0, -0.15, -1.1), vec3f(1.25, 0.22, 1.1));
  if (plinth < d) { d = plinth; m = 5.0; }
  let lip = tgk_box(p - vec3f(0.0, 0.28, 0.05), vec3f(0.6, 0.08, 0.28));   // where the bloom stands
  if (lip < d) { d = lip; m = 5.0; }
  // ── CHARRED BRAMBLE framing the clearing (twisted arcs at the sides) ──
  for (var i = 0; i < 5; i++) {
    let fi = f32(i);
    let side = select(1.0, -1.0, (i % 2) == 0);
    let bx = side * (2.3 + 0.2 * tgk_h(vec2f(fi, 1.0)));
    let a = vec3f(bx, -0.35, -0.2 + fi * 0.4);
    let b = vec3f(bx * 0.72, 1.4 + 0.3 * tgk_h(vec2f(fi, 3.0)), -1.5 + fi * 0.3);
    let mid = (a + b) * 0.5 + vec3f(side * 0.4, 0.2, 0.0);
    let br = min(tgk_cap(p, a, mid, 0.05), tgk_cap(p, mid, b, 0.04));
    if (br < d) { d = br; m = 4.0; }
  }
  // ── the GLASS BLOOM stem (only once annealed) — the flower is drawn in shade ──
  if (bloom > 0.5) {
    let stem = tgk_cap(p, vec3f(0.0, 0.36, 0.05), vec3f(0.0, 1.05, 0.05), 0.045);
    if (stem < d) { d = stem; m = 6.0; }
    let bulb = tgk_sph(p - vec3f(0.0, 1.12, 0.05), 0.16);
    if (bulb < d) { d = bulb; m = 6.0; }
  }
  return vec2f(d, m);
}

fn tgk_nrm(p: vec3f, bloom: f32, t: f32) -> vec3f {
  let e = vec2f(0.006, 0.0);
  return normalize(vec3f(
    tgk_map(p + e.xyy, bloom, t).x - tgk_map(p - e.xyy, bloom, t).x,
    tgk_map(p + e.yxy, bloom, t).x - tgk_map(p - e.yxy, bloom, t).x,
    tgk_map(p + e.yyx, bloom, t).x - tgk_map(p - e.yyx, bloom, t).x));
}
fn tgk_march(ro: vec3f, rd: vec3f, bloom: f32, t: f32) -> vec2f {
  var tt = 0.02;
  for (var i = 0; i < 90; i++) {
    let h = tgk_map(ro + rd * tt, bloom, t);
    if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
    tt += h.x * 0.85; if (tt > 24.0) { break; }
  }
  return vec2f(-1.0, 0.0);
}
fn tgk_shadow(ro: vec3f, rd: vec3f, bloom: f32, t: f32) -> f32 {
  var res = 1.0; var tt = 0.05;
  for (var i = 0; i < 14; i++) {
    let h = tgk_map(ro + rd * tt, bloom, t).x;
    res = min(res, 8.0 * h / tt); tt += clamp(h, 0.03, 0.4);
    if (res < 0.03 || tt > 6.0) { break; }
  }
  return clamp(res, 0.0, 1.0);
}
fn tgk_ao(p: vec3f, n: vec3f, bloom: f32, t: f32) -> f32 {
  var occ = 0.0; var w = 1.0;
  for (var i = 1; i <= 4; i++) { let hh = 0.05 * f32(i); occ += w * (hh - tgk_map(p + n * hh, bloom, t).x); w *= 0.72; }
  return clamp(1.0 - 2.1 * occ, 0.0, 1.0);
}
fn tgk_alb(m: f32, p: vec3f) -> vec3f {
  if (m < 1.5) {        // kiln stone — fired clay, warm ochre, soot-blushed, coursed
    var a = vec3f(0.20, 0.145, 0.105) * (0.75 + 0.5 * tgk_fbm(p.xy * 3.5 + p.zz, 3));
    a *= 1.0 - 0.35 * smoothstep(0.35, 0.05, abs(fract(p.y * 3.0) - 0.5));   // stone courses
    a *= 1.0 - 0.35 * smoothstep(0.5, 0.0, p.y - TGK_KC.y);   // soot up the flue
    return a;
  } else if (m < 2.5) { return vec3f(0.26, 0.13, 0.08) * (0.8 + 0.4 * tgk_fbm(p.xy * 8.0, 2)); }  // mouth brick
  else if (m < 3.5) { return vec3f(0.075, 0.065, 0.058) * (0.7 + 0.6 * tgk_fbm(p.xz * 5.0, 3)); }  // ash
  else if (m < 4.5) { return vec3f(0.045, 0.038, 0.035); }                                          // charred bramble
  else if (m < 5.5) { return vec3f(0.10, 0.088, 0.078) * (0.8 + 0.3 * tgk_fbm(p.xz * 4.0, 2)); }    // plinth stone
  return vec3f(0.4, 0.75, 0.85);                                                                     // glass (lit specially)
}

fn tgk_shade(ro: vec3f, rd: vec3f, tt: f32, m: f32, bloom: f32, warm: f32, t: f32) -> vec3f {
  let p = ro + rd * tt;
  let n = tgk_nrm(p, bloom, t);
  // KEY = the fire inside the kiln mouth (a warm point light at the firebox)
  let fire = TGK_KC + vec3f(0.0, -0.2, 0.7);
  let ldir = normalize(fire - p);
  let flick = 0.8 + 0.2 * sin(t * 7.0) * sin(t * 4.3 + 1.0);
  let fireCol = vec3f(1.5, 0.72, 0.28) * flick;
  let dif = max(dot(n, ldir), 0.0);
  let sh = tgk_shadow(p + n * 0.03, ldir, bloom, t);
  let ao = tgk_ao(p, n, bloom, t);
  let fall = 1.6 / (1.0 + 0.42 * dot(fire - p, fire - p));   // the fire reaches the stone
  // cool moon fill from above (fire-tinted by elBlend)
  let moonFill = mix(vec3f(0.14, 0.18, 0.27), vec3f(0.30, 0.16, 0.13), warm);
  var c = tgk_alb(m, p) * (0.10 + 2.6 * dif * sh * fall) * ao + tgk_alb(m, p) * moonFill * (0.55 + 0.5 * max(n.y, 0.0));
  c += fireCol * (0.30 + 0.5 * dif) * fall * ao * 0.25;      // warm bounce washing the clearing
  // the GLASS BLOOM — translucent, refracting ember light, the room's treasure
  if (m > 5.5) {
    let fres = pow(1.0 - max(dot(-rd, n), 0.0), 2.5);
    let inner = fireCol * (0.5 + 0.8 * dif);
    c = mix(vec3f(0.18, 0.42, 0.52), vec3f(1.4, 0.85, 0.5), fres) * 0.5 + inner * 0.6;
    c += vec3f(1.0, 0.9, 0.7) * pow(max(dot(reflect(rd, n), ldir), 0.0), 40.0) * 1.2;   // sparkle
  } else {
    // specular glints on wet-fired stone
    let hv = normalize(ldir - rd);
    c += fireCol * pow(max(dot(n, hv), 0.0), 22.0) * sh * 0.4 * fall;
  }
  return c;
}

fn mod_tg_kiln(pp: vec2f, px: vec2f, t: f32) -> vec3f {
  let mm = vec2f(uni(27), uni(28));
  let bloom = uni(104);
  let warm = clamp(uni(82), 0.0, 1.0);
  let ro = vec3f(0.0, 1.35, 3.9);
  let uvx = px.x / 256.0 - 1.0;
  let uvy = px.y / 256.0 - 1.0;
  let rd = normalize(vec3f(uvx, -uvy - 0.05, -1.0));
  // ── night sky through the burned canopy: deep dark, fire-moon warmth ──
  var c = mix(vec3f(0.020, 0.026, 0.050), vec3f(0.060, 0.030, 0.032), clamp(-uvy, 0.0, 1.0));
  c = mix(c, vec3f(0.10, 0.05, 0.04), warm * 0.5);
  let st = tgk_h(floor(px / 3.0));
  if (st > 0.994) { c += vec3f(0.7, 0.75, 0.95) * (0.4 + 0.6 * sin(t * 1.5 + st * 60.0)) * 0.4; }
  // fire-moon low on the right
  let mc = vec2f(430.0, 96.0);
  c += vec3f(1.2, 0.55, 0.22) * smoothstep(30.0, 22.0, length(px - mc)) * 0.7;
  c += vec3f(0.9, 0.4, 0.18) * smoothstep(150.0, 0.0, length(px - mc)) * 0.12;
  // ── march the clearing ──
  let hit = tgk_march(ro, rd, bloom, t);
  if (hit.x > 0.0) {
    c = tgk_shade(ro, rd, hit.x, hit.y, bloom, warm, t);
    c = mix(c, vec3f(0.03, 0.03, 0.05), smoothstep(7.0, 18.0, hit.x) * 0.4);
  }
  // ── the FIREBOX: a bed of breathing coals + flame licks in the mouth
  //    (screen-space, anchored 256,290) ──
  let mo = vec2f(256.0, 290.0);
  let md = length(px - mo);
  let breath = 0.7 + 0.3 * sin(t * 3.0) * sin(t * 1.7);
  c += vec3f(1.35, 0.62, 0.24) * smoothstep(64.0, 6.0, md) * breath * (0.55 + 0.35 * (1.0 - bloom));
  // coal bed: hot cells at the base of the mouth
  if (px.y > 285.0 && md < 48.0) {
    let cell = tgk_n2(px * 0.12 + vec2f(0.0, t * 0.5));
    c += vec3f(1.5, 0.5, 0.15) * smoothstep(0.45, 0.85, cell) * smoothstep(48.0, 10.0, md) * (0.5 + 0.4 * sin(t * 5.0 + cell * 20.0));
  }
  // flame licks rising from the mouth
  for (var fl = 0; fl < 4; fl++) {
    let ff = f32(fl);
    let fx = 256.0 + (ff - 1.5) * 13.0;
    let flame = smoothstep(9.0, 0.0, abs(px.x - fx - 5.0 * sin(px.y * 0.06 + t * 5.0 + ff)))
              * smoothstep(300.0, 250.0, px.y) * smoothstep(250.0, 285.0, px.y);
    c += vec3f(1.4, 0.65, 0.2) * flame * (0.4 + 0.3 * sin(t * 8.0 + ff)) * (1.0 - bloom * 0.5);
  }
  let hov = smoothstep(70.0, 30.0, length(mm - mo));
  if (bloom < 0.5) {
    // the molten SEED — a bright bead waiting to be worked; hover invites
    c += vec3f(1.7, 0.9, 0.4) * smoothstep(18.0, 2.0, length(px - mo - vec2f(0.0, 6.0))) * (0.6 + 0.4 * sin(t * 4.0)) * (0.6 + 0.5 * hov);
  } else {
    // ── the GLASS BLOOM: a flower of translucent petals on the lip (px ~256,224),
    //    glowing from within, refracting ember → the island's thank-you ──
    let fc = vec2f(256.0, 224.0);
    let fd0 = px - fc;
    // stem catching light
    c += vec3f(0.55, 0.75, 0.85) * smoothstep(3.0, 0.5, abs(fd0.x)) * smoothstep(64.0, 30.0, length(px - vec2f(256.0, 262.0))) * 0.5;
    let ang = atan2(fd0.y, fd0.x);
    let rr = length(fd0);
    // six petals: radius modulated by a 6-lobe rose
    let petal = 34.0 * (0.55 + 0.45 * abs(cos(ang * 3.0)));
    let inPetal = smoothstep(petal, petal - 10.0, rr);
    let glassA = mix(vec3f(0.35, 0.62, 0.75), vec3f(1.5, 0.9, 0.55), 0.5 + 0.5 * cos(ang * 3.0));
    c = mix(c, glassA, inPetal * 0.8);
    c += glassA * inPetal * (0.3 + 0.3 * sin(t * 2.0)) * 0.5;          // inner glow
    c += vec3f(1.6, 1.2, 0.9) * smoothstep(9.0, 0.0, rr) * (0.7 + 0.3 * sin(t * 3.0));  // molten heart
    c += vec3f(0.9, 1.0, 1.0) * smoothstep(2.0, 0.0, abs(rr - petal)) * inPetal;         // petal rim light
    // soft light column + settling fireflies above the bloom
    c += vec3f(0.5, 0.7, 0.85) * smoothstep(55.0, 0.0, abs(px.x - 256.0)) * smoothstep(300.0, 40.0, px.y) * 0.09;
    for (var i = 0; i < 10; i++) {
      let fi = f32(i);
      let fx = 256.0 + 130.0 * sin(fi * 2.3 + t * 0.3 + fi);
      let fy = 90.0 + 220.0 * fract(fi * 0.13 + t * 0.05);
      c += vec3f(1.0, 0.8, 0.5) * smoothstep(2.6, 0.5, length(px - vec2f(fx, fy))) * (0.4 + 0.4 * sin(t * 3.0 + fi));
    }
  }
  // rising embers along the charred frame
  for (var i = 0; i < 10; i++) {
    let fi = f32(i);
    let ex = select(40.0, 472.0, (i % 2) == 0) + 26.0 * sin(fi + t * 0.6);
    let ey = 512.0 - 512.0 * fract(fi * 0.17 + t * 0.08);
    c += vec3f(1.0, 0.5, 0.2) * smoothstep(2.2, 0.4, length(px - vec2f(ex, ey))) * 0.5;
  }
  // click-confirm ripple
  let cfx = uni(84);
  if (cfx > 0.003) { let cd = length(px - vec2f(uni(85), uni(86))); c += vec3f(1.0, 0.8, 0.5) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4; }
  c = tgk_bolt(c, px, t);
  return c;
}

// ═══ THE DISCHARGE — the risen bloom fires the summons skyward (kiln view).
//     s = uni(115); visible only for the first beat. Kiln px space, y-down;
//     the bloom bulb sits at px(256,224). ═══
fn tgk_bolt(cIn: vec3f, px: vec2f, t: f32) -> vec3f {
  var c = cIn;
  let s = uni(115);
  let k = smoothstep(0.0, 0.015, s) * (1.0 - smoothstep(0.10, 0.14, s));
  if (k < 0.003) { return c; }
  var aPt = vec2f(256.0, 218.0);
  var m = 0.0;
  for (var i = 0; i < 6; i = i + 1) {
    let fi = f32(i);
    let nx = 256.0 + sin(fi * 13.1 + floor(t * 24.0) * 1.7) * (14.0 + fi * 7.0);
    let bPt = vec2f(nx, 218.0 - (fi + 1.0) * 38.0);
    m = max(m, smoothstep(2.2, 0.0, sdSegment(px, aPt, bPt)));
    // one short branch per joint, flickering
    let br = vec2f(nx + sin(fi * 7.7 + floor(t * 31.0)) * 26.0, bPt.y - 14.0);
    m = max(m, smoothstep(1.4, 0.0, sdSegment(px, bPt, br)) * 0.6);
    aPt = bPt;
  }
  c += vec3f(0.85, 0.75, 1.25) * m * k * 1.4;
  c += vec3f(0.9, 0.85, 1.2) * k * exp(-abs(s - 0.05) * 60.0) * 0.10;   // one soft full-frame breath
  // the bloom itself burns star-white while it gives the call
  let bd = length(px - vec2f(256.0, 224.0));
  c += vec3f(1.1, 1.0, 1.3) * exp(-bd * bd * 0.004) * k * 0.8;
  return c;
}

module · tg_deck

// AIRSHIP DECK (view 9) — raymarched. The moored brass airship at first dawn:
// a planked deck in perspective, rope rigging climbing to a warm gasbag canopy
// overhead, the ship's helm forward, dawn bleeding amber at the horizon. Shore
// recipe: SDF map → march → normal → soft shadow/AO → materials → dawn key light.
// Anchors (px, LAW): spanner (256,404) r34 · hatch→engine (432,256) r44 ·
// helm (256,120) r52. State: uni(53) spannerOwned · uni(56)/uni(68) engineOK ·
// uni(57) moored(dawn) · uni(58) act(1 island,2 flight) · uni(71) fly · uni(44)
// dawn · mouse uni(27,28). Author: Claude Fable 5.

fn tgd_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgd_n2(p: vec2f) -> f32 {
  let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
  return mix(mix(tgd_h(i), tgd_h(i + vec2f(1.0, 0.0)), u.x),
             mix(tgd_h(i + vec2f(0.0, 1.0)), tgd_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgd_fbm(p: vec2f, oct: i32) -> f32 {
  var v = 0.0; var a = 0.5; var q = p;
  for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgd_n2(q); q = q * 2.02 + vec2f(11.0, 7.0); a *= 0.5; }
  return v;
}
fn tgd_box(p: vec3f, b: vec3f) -> f32 { let q = abs(p) - b; return length(max(q, vec3f(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0); }
fn tgd_rbox(p: vec3f, b: vec3f, r: f32) -> f32 { let q = abs(p) - b; return length(max(q, vec3f(0.0))) - r + min(max(q.x, max(q.y, q.z)), 0.0); }
fn tgd_cyl(p: vec3f, h: f32, r: f32) -> f32 { let d = vec2f(length(p.xz) - r, abs(p.y) - h); return min(max(d.x, d.y), 0.0) + length(max(d, vec2f(0.0))); }
fn tgd_torusY(p: vec3f, R: f32, r: f32) -> f32 { let q = vec2f(length(p.xz) - R, p.y); return length(q) - r; }
fn tgd_cap(p: vec3f, a: vec3f, b: vec3f, r: f32) -> f32 {
  let pa = p - a; let ba = b - a; let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h) - r;
}
fn tgd_rotZ(p: vec3f, a: f32) -> vec3f { let c = cos(a); let s = sin(a); return vec3f(c * p.x - s * p.y, s * p.x + c * p.y, p.z); }
// project a SHIP-FRAME point to screen px through the same camera + deck roll the
// geometry uses, so rigging anchored to the rail/balloon tracks them as the deck
// shifts (the SDF renders ship-coord X at world rotZ(X,-roll); project that).
fn tgd_proj(shipPt: vec3f, roll: f32) -> vec2f {
  let c = cos(-roll); let s = sin(-roll);
  let wp = vec3f(c * shipPt.x - s * shipPt.y, s * shipPt.x + c * shipPt.y, shipPt.z);
  let ro = vec3f(0.0, 1.35, 4.4);
  let denom = max(ro.z - wp.z, 0.05);
  let uvx = (wp.x - ro.x) / denom;
  let uvy = -0.14 - (wp.y - ro.y) / denom;
  return vec2f((uvx + 1.0) * 256.0, (uvy + 1.0) * 256.0);
}

// anchors in WORLD, composed to project to their px coords under the camera
// (eye (0,1.35,4.4) looking -z, slight downward tilt): deck fills the lower
// frame, helm forward-high, hatch to the right, spanner near on the planks.
const TGD_HELM = vec3f(0.0, 1.62, -2.35);   // → px ~ (256,120)
const TGD_HATCH = vec3f(1.62, 0.02, -0.55); // → px ~ (432,256) (deck level, right)
const TGD_SPAN = vec3f(0.0, 0.06, 1.35);    // → px ~ (256,404) (near planks)

// SDF → (dist, matId). mats: 1 plank · 2 brass rail · 3 rope · 4 helm wheel ·
// 5 binnacle · 6 hatch iron · 7 spanner · 8 gunwale post · 9 gasbag
fn tgd_map(p: vec3f, fly: f32, spOwned: f32, t: f32) -> vec2f {
  var d = 1e5; var m = 1.0;
  let roll = fly * 0.06 * sin(t * 0.7);          // the deck leans once flight begins
  let pr = tgd_rotZ(p, roll);
  // hull HALF-WIDTH tapers toward the bow (front = -z), so the ship comes to a
  // prow instead of a floating rectangle (Galen: "fill in the end of the ship")
  let taper = 2.5 - 1.6 * smoothstep(-1.6, -3.9, pr.z);
  // ── the DECK: a thick planked slab, clipped to the tapering hull ──
  var deck = tgd_rbox(pr - vec3f(0.0, -0.16, 0.3), vec3f(2.5, 0.16, 3.15), 0.02);
  deck = max(deck, abs(pr.x) - taper);
  deck = max(deck, pr.z - 2.9);                 // stern edge
  if (deck < d) { d = deck; m = 1.0; }
  // ── BULWARK: a solid planked hull wall up each tapering side (the ship has
  //    sides you can't see through) ──
  let bw = tgd_box(vec3f(abs(pr.x) - taper, pr.y - 0.10, pr.z - 0.3), vec3f(0.10, 0.28, 3.1));
  if (bw < d) { d = bw; m = 1.0; }
  // ── the BOW: where the sides meet, a solid raised forecastle block ──
  let bow = tgd_rbox(pr - vec3f(0.0, 0.02, -3.05), vec3f(0.62, 0.40, 0.5), 0.06);
  if (bow < d) { d = bow; m = 1.0; }
  // a bowsprit spearing forward from the prow
  let spr = tgd_cap(pr, vec3f(0.0, 0.30, -3.4), vec3f(0.0, 0.42, -4.4), 0.045);
  if (spr < d) { d = spr; m = 8.0; }
  // ── brass rail cap riding the top of each bulwark ──
  let rail = min(tgd_cap(pr, vec3f(-2.5, 0.40, -2.9), vec3f(-1.0, 0.40, -3.6), 0.05),
                 tgd_cap(pr, vec3f(2.5, 0.40, -2.9), vec3f(1.0, 0.40, -3.6), 0.05));
  let railBack = min(tgd_cap(pr, vec3f(-2.5, 0.40, -2.9), vec3f(-2.5, 0.40, 2.95), 0.05),
                     tgd_cap(pr, vec3f(2.5, 0.40, -2.9), vec3f(2.5, 0.40, 2.95), 0.05));
  let railAll = min(rail, railBack);
  if (railAll < d) { d = railAll; m = 2.0; }
  // rail posts (repeated along the straight run)
  let rpz = clamp(pr.z, -2.6, 2.8);
  let rpx = vec3f(pr.x, pr.y, glsl_mod(rpz + 2.3, 1.05) - 0.525);
  let post = tgd_box(vec3f(abs(rpx.x) - min(taper, 2.5), rpx.y - 0.08, rpx.z), vec3f(0.05, 0.24, 0.05));
  if (post < d) { d = post; m = 8.0; }
  // ── HELM: a ship's wheel forward-high on a binnacle column. A torus in the
  //    X-Y plane facing the camera (+z), plus spokes/handles + hub ──
  let hp = pr - TGD_HELM;
  var wheel = length(vec2f(length(hp.xy) - 0.60, hp.z)) - 0.05;
  for (var k = 0; k < 6; k++) {
    let a = f32(k) * 1.0472 + fly * (0.9 + 0.6 * sin(t * 1.5));
    let dir = vec3f(cos(a), sin(a), 0.0) * 0.78;
    wheel = min(wheel, tgd_cap(hp, vec3f(0.0, 0.0, 0.0), dir, 0.035));    // spoke + handle spike
  }
  wheel = min(wheel, length(hp) - 0.12);                          // hub
  if (wheel < d) { d = wheel; m = 4.0; }
  let binn = tgd_cyl(pr - vec3f(0.0, 0.9, -2.3), 0.55, 0.16);     // binnacle column under the helm
  if (binn < d) { d = binn; m = 5.0; }
  // ── the COMPANIONWAY: a raised housing whose hatch face sits AT the click
  //    zone px(432,256) (the old flat lid projected ~90px off — Galen). ──
  let qp = pr - vec3f(2.05, 0.50, 1.19);
  let house = tgd_rbox(qp, vec3f(0.32, 0.50, 0.32), 0.04);
  if (house < d) { d = house; m = 6.0; }
  let lid = tgd_rbox(qp - vec3f(0.0, 0.54, -0.06), vec3f(0.34, 0.05, 0.38), 0.02);
  if (lid < d) { d = lid; m = 6.0; }
  let pull = tgd_torusY(vec3f(qp.x, qp.z - 0.33, qp.y - 0.10), 0.10, 0.024);
  if (pull < d) { d = pull; m = 2.0; }
  // ── SPANNER on the planks (only while unowned) ──
  if (spOwned < 0.5) {
    let sp = tgd_rotZ(pr - TGD_SPAN, 0.5);
    var span = tgd_box(sp, vec3f(0.30, 0.045, 0.05));             // shaft
    span = min(span, tgd_torusY(vec3f(sp.x - 0.30, sp.z, sp.y), 0.075, 0.03));  // open jaw ring
    span = min(span, tgd_torusY(vec3f(sp.x + 0.30, sp.z, sp.y), 0.06, 0.028));
    if (span < d) { d = span; m = 7.0; }
  }
  // ── the GASBAG canopy overhead: a great rounded hull filling the sky ──
  let gb = pr - vec3f(0.0, 4.6, -0.4);
  let bag = length(gb / vec3f(3.4, 1.7, 4.2)) - 1.0;             // ellipsoid (approx SDF)
  let bagD = bag * 1.7;
  if (bagD < d) { d = bagD; m = 9.0; }
  return vec2f(d, m);
}

fn tgd_nrm(p: vec3f, fly: f32, sp: f32, t: f32) -> vec3f {
  let e = vec2f(0.007, 0.0);
  return normalize(vec3f(
    tgd_map(p + e.xyy, fly, sp, t).x - tgd_map(p - e.xyy, fly, sp, t).x,
    tgd_map(p + e.yxy, fly, sp, t).x - tgd_map(p - e.yxy, fly, sp, t).x,
    tgd_map(p + e.yyx, fly, sp, t).x - tgd_map(p - e.yyx, fly, sp, t).x));
}
fn tgd_march(ro: vec3f, rd: vec3f, fly: f32, sp: f32, t: f32) -> vec2f {
  var tt = 0.02;
  for (var i = 0; i < 90; i++) {
    let h = tgd_map(ro + rd * tt, fly, sp, t);
    if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
    tt += h.x * 0.85; if (tt > 30.0) { break; }
  }
  return vec2f(-1.0, 0.0);
}
fn tgd_shadow(ro: vec3f, rd: vec3f, fly: f32, sp: f32, t: f32) -> f32 {
  var res = 1.0; var tt = 0.05;
  for (var i = 0; i < 16; i++) {
    let h = tgd_map(ro + rd * tt, fly, sp, t).x;
    res = min(res, 8.0 * h / tt); tt += clamp(h, 0.03, 0.4);
    if (res < 0.02 || tt > 7.0) { break; }
  }
  return clamp(res, 0.0, 1.0);
}
fn tgd_ao(p: vec3f, n: vec3f, fly: f32, sp: f32, t: f32) -> f32 {
  var occ = 0.0; var w = 1.0;
  for (var i = 1; i <= 4; i++) { let hh = 0.06 * f32(i); occ += w * (hh - tgd_map(p + n * hh, fly, sp, t).x); w *= 0.72; }
  return clamp(1.0 - 2.0 * occ, 0.0, 1.0);
}

fn tgd_alb(m: f32, p: vec3f, engineOK: f32) -> vec3f {
  if (m < 1.5) {        // planks — warm oiled wood, grain + seams
    let grain = tgd_fbm(vec2f(p.x * 2.0, p.z * 22.0), 4);
    var a = mix(vec3f(0.14, 0.085, 0.045), vec3f(0.20, 0.13, 0.075), grain);
    a *= 0.78 + 0.22 * step(0.06, abs(fract(p.x * 1.6) - 0.5));   // plank seams (dark grooves)
    return a;
  } else if (m < 2.5) { return vec3f(0.24, 0.17, 0.075) * 1.1; }   // brass rail
  else if (m < 3.5) { return vec3f(0.12, 0.10, 0.08); }            // rope
  else if (m < 4.5) { return vec3f(0.20, 0.14, 0.07); }            // helm wheel — brass-bound wood
  else if (m < 5.5) { return vec3f(0.16, 0.13, 0.10); }            // binnacle
  else if (m < 6.5) { return vec3f(0.065, 0.06, 0.062) * (0.85 + 0.3 * tgd_fbm(p.xz * 8.0, 2)); }  // hatch iron
  else if (m < 7.5) { return vec3f(0.28, 0.22, 0.10); }            // spanner bronze
  else if (m < 8.5) { return vec3f(0.055, 0.05, 0.045); }          // posts (dark iron)
  return vec3f(0.30, 0.20, 0.14);                                  // gasbag canopy (lit separately)
}

fn tgd_shade(ro: vec3f, rd: vec3f, tt: f32, m: f32, fly: f32, sp: f32, engineOK: f32, dawn: f32, t: f32) -> vec3f {
  let p = ro + rd * tt;
  let n = tgd_nrm(p, fly, sp, t);
  // dawn key: low warm sun from the horizon behind-right
  let sun = normalize(vec3f(0.5, 0.22, 0.7));
  let key = mix(vec3f(1.15, 0.72, 0.42), vec3f(1.30, 0.60, 0.28), dawn);
  let dif = max(dot(n, sun), 0.0);
  let sh = tgd_shadow(p + n * 0.03, sun, fly, sp, t);
  let ao = tgd_ao(p, n, fly, sp, t);
  let sky = vec3f(0.22, 0.24, 0.34);                               // cool sky fill from above
  let skyf = max(dot(n, vec3f(0.0, 1.0, 0.0)), 0.0);
  var c = tgd_alb(m, p, engineOK) * (0.14 + 1.0 * dif * sh) * ao + sky * skyf * 0.22 * ao;
  // specular (brass & iron & spanner sing)
  let hv = normalize(sun - rd);
  let spec = pow(max(dot(n, hv), 0.0), 42.0) * sh;
  let metal = step(1.5, m) * step(m, 2.5) + step(3.5, m) * step(m, 4.5) + step(6.5, m) * step(m, 7.5);
  c += key * spec * (0.2 + 0.9 * metal);
  // the GASBAG — cream fabric envelope, gore panels + seams + weave, matching
  // the airship on the shore (tg_air): warm dawn key + cool sky fill, not a
  // flat brown blob (Galen: "balloon needs texture … like the airship on main")
  if (m > 8.5) {
    let lp = p - vec3f(0.0, 4.6, -0.4);              // local to the bag centre
    // longitudinal GORES: panels running along the length (z), seams between
    let phi = atan2(lp.y, lp.x);
    let Ngore = 16.0;
    let gg = phi * (Ngore / 6.2831853);
    let sp = gg - floor(gg + 0.5);
    let pid = floor(gg + 0.5);
    let seam = smoothstep(0.06, 0.0, abs(sp));       // dark stitched seam
    let bulge = 0.85 + 0.15 * cos(sp * 3.14159);     // each gore rounds outward
    // circumferential RIBS + fabric weave
    let rib = 0.5 + 0.5 * cos(lp.z * 7.0);
    let weave = tgd_fbm(vec2f(phi * 6.0, lp.z * 9.0), 4) - 0.5;
    var alb = vec3f(0.86, 0.80, 0.67);               // off-white canvas
    alb *= 0.93 + 0.10 * tgd_h(vec2f(pid, 3.1));     // per-gore tint variation
    alb *= 1.0 + weave * 0.10;
    alb *= bulge;
    alb *= 1.0 - 0.28 * seam;
    alb *= 0.94 + 0.06 * rib;
    let warmK = mix(vec3f(1.05, 0.72, 0.42), vec3f(1.25, 0.62, 0.30), dawn);
    let sky2 = vec3f(0.45, 0.52, 0.66);
    let dK = max(dot(n, sun), 0.0);
    let dF = max(dot(n, vec3f(-0.3, -0.9, 0.3)), 0.0);
    c = alb * (0.34 + dK * 1.05 * warmK + dF * 0.32 * sky2);
    // rim/crown glow where the envelope meets the sky
    let rim = pow(1.0 - max(dot(-rd, n), 0.0), 2.2);
    c += warmK * rim * (0.18 + 0.28 * dawn);
    // painted keel band + nose cap accents
    c *= 1.0 - 0.4 * smoothstep(0.05, 0.0, abs(lp.y + 1.55)) * step(abs(lp.x), 3.0);
  }
  // engineOK: the binnacle + helm rim light up (ship has power)
  if (engineOK > 0.5 && (m > 3.5 && m < 5.5)) {
    c += vec3f(0.9, 0.75, 0.35) * (0.35 + 0.2 * sin(t * 2.5)) * (0.4 + 0.6 * dif);
  }
  return c;
}

fn mod_tg_deck(pp: vec2f, px: vec2f, t: f32) -> vec3f {
  let mm = vec2f(uni(27), uni(28));
  let spOwned = uni(53);
  let engineOK = max(uni(56), uni(68));
  let crystalPowered = uni(110);                     // the keel crystal, lit by the cave beam
  let helmLive = select(0.0, 1.0, engineOK > 0.5 && crystalPowered > 0.5);  // wheel turns only with both
  let dawn = clamp(max(uni(44), uni(57)), 0.0, 1.0);
  let act = uni(58);
  let fly = uni(71);
  // ── camera on the deck, looking forward toward the helm ──
  let ro = vec3f(0.0, 1.35, 4.4);
  let uvx = px.x / 256.0 - 1.0;
  let uvy = px.y / 256.0 - 1.0;
  let rd = normalize(vec3f(uvx, -uvy - 0.14, -1.0));               // slight downward tilt to see the deck
  // ── the SKY: dawn gradient; open cloudscape once in flight (act 2) ──
  let horizon = clamp(-uvy * 0.5 + 0.5, 0.0, 1.0);
  var sky = mix(mix(vec3f(0.35, 0.22, 0.16), vec3f(0.85, 0.48, 0.26), dawn),  // warm horizon
                mix(vec3f(0.05, 0.06, 0.12), vec3f(0.12, 0.13, 0.24), dawn),  // cool zenith
                clamp(-uvy, 0.0, 1.0));
  // sun disc at the horizon
  let sunp = vec2f(360.0, 300.0);
  sky += vec3f(1.2, 0.7, 0.35) * smoothstep(46.0, 30.0, length(px - sunp)) * dawn;
  sky += vec3f(0.9, 0.5, 0.28) * smoothstep(220.0, 0.0, length(px - sunp)) * 0.18 * dawn;
  // clouds (drift faster in flight)
  let drift = t * (0.02 + 0.14 * step(1.5, act));
  let cl = tgd_fbm(vec2f(px.x * 0.006 + drift, px.y * 0.010), 4);
  sky = mix(sky, mix(vec3f(0.5, 0.34, 0.30), vec3f(0.14, 0.15, 0.26), clamp(-uvy, 0.0, 1.0)),
            smoothstep(0.55, 0.85, cl) * clamp(-uvy + 0.6, 0.0, 1.0) * (0.4 + 0.4 * step(1.5, act)));
  var c = sky;
  // ── march the ship ──
  let hit = tgd_march(ro, rd, fly, spOwned, t);
  if (hit.x > 0.0) {
    c = tgd_shade(ro, rd, hit.x, hit.y, fly, spOwned, engineOK, dawn, t);
    // the keel crystal's power runs up into the helm — a cool charge on the brass
    if (helmLive > 0.5 && hit.y > 3.5 && hit.y < 5.5) {
      let pw = ro + rd * hit.x;
      c += vec3f(0.25, 0.6, 0.85) * (0.4 + 0.4 * sin(t * 4.0 - pw.y * 6.0));
    }
    c = mix(c, sky, smoothstep(9.0, 22.0, hit.x) * 0.35);          // aerial haze on far parts
  }
  // ── SHROUD RIGGING: every rope is anchored to a real 3D point — its FOOT
  //    cleated to the gunwale rail, its TOP made fast to the balloon's keel —
  //    and both ends are PROJECTED through the same camera+roll the ship uses.
  //    So the ropes track the deck as it shifts AND stay attached to the bag
  //    (Galen: they didn't connect to the shifting deck or the balloon). ──
  let roll = fly * 0.06 * sin(t * 0.7);
  for (var r = 0; r < 6; r++) {
    let side = select(1.0, -1.0, r < 3);
    let fi = f32(r % 3);
    let footW = vec3f(side * 2.42, 0.42, -1.9 + fi * 1.5);        // cleat on the rail
    let topW  = vec3f(side * 0.92, 3.02 + 0.04 * fi, -1.0 + fi * 0.9);  // keel of the balloon
    let foot = tgd_proj(footW, roll);
    let top  = tgd_proj(topW, roll);
    let pa = px - foot; let ba = top - foot; let hh = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    let dl = length(pa - ba * hh);
    c += vec3f(0.20, 0.15, 0.10) * smoothstep(2.2, 0.4, dl) * 0.75;
    c += vec3f(0.9, 0.6, 0.35) * smoothstep(1.4, 0.2, dl) * dawn * 0.3;
    // CLEAT at the foot (brass knot gripping the rail) + a small lashing where
    // the rope makes fast to the balloon — both prove the anchoring
    c += vec3f(0.5, 0.38, 0.18) * smoothstep(6.0, 2.0, length(px - foot)) * 0.9;
    c += vec3f(0.95, 0.7, 0.35) * smoothstep(3.0, 0.5, length(px - foot)) * dawn * 0.4;
    c += vec3f(0.55, 0.45, 0.28) * smoothstep(4.5, 1.5, length(px - top)) * 0.7;
  }
  // ── anchor affordances (screen-space light only) ──
  // HELM (256,120): dead until engineOK, then lamps + hover
  let hc = vec2f(256.0, 120.0);
  let hovH = smoothstep(60.0, 24.0, length(mm - hc));
  if (helmLive > 0.5) {
    // both gears AND crystal: the wheel is takeable — a bright gold-and-cyan pulse
    c += mix(vec3f(0.9, 0.75, 0.35), vec3f(0.4, 0.85, 1.0), 0.4 + 0.3 * sin(t * 3.0)) * tg_ring(px, hc, 50.0, 44.0) * (0.4 + 0.4 * sin(t * 3.0)) * (0.5 + 0.5 * hovH);
  } else if (engineOK > 0.5) {
    // gears turn but the crystal is dark — the helm shows it wants power (cold, waiting)
    c += vec3f(0.30, 0.42, 0.5) * tg_ring(px, hc, 50.0, 45.0) * (0.25 + 0.15 * sin(t * 1.5)) * (0.4 + 0.6 * hovH);
  } else {
    c += vec3f(0.5, 0.42, 0.28) * tg_ring(px, hc, 50.0, 45.0) * hovH * 0.35;
  }
  if (helmLive > 0.5) {
    // the take-the-wheel chevron: an up-glyph bobbing over the helm (matches nav chevrons)
    let cb = vec2f(256.0, 54.0 + 4.0 * sin(t * 2.2));
    let cd = px - cb;
    let arm = abs(cd.y - 0.7 * abs(cd.x) + 5.0);                 // ^ apex at top, arms falling outward
    let chev = smoothstep(3.5, 1.2, arm) * smoothstep(16.0, 14.0, abs(cd.x));
    c += mix(vec3f(0.95, 0.8, 0.4), vec3f(0.5, 0.9, 1.0), 0.5 + 0.5 * sin(t * 3.0)) * chev * (0.55 + 0.35 * sin(t * 3.0));
  }
  // HATCH (432,256): NO UI overlay — the game's own down-CHEVRON (shown when the
  // spanner is held) is the affordance now (Galen: stick with chevrons). Here we
  // only add scenery: bolt-heads on the raymarched lid and a faint warm breath of
  // engine-light from the seam, so it reads as a real hatch you descend.
  let qc = vec2f(432.0, 256.0);
  let hd = px - qc;
  for (var b = 0; b < 4; b++) {
    let ba2 = f32(b) * 1.5708 + 0.7854;
    let bp = qc + vec2f(cos(ba2), sin(ba2)) * 30.0;
    c += vec3f(0.36, 0.30, 0.21) * smoothstep(5.0, 1.5, length(px - bp)) * 0.8;
  }
  c += vec3f(0.9, 0.5, 0.22) * smoothstep(30.0, 8.0, length(hd)) * 0.06 * (0.6 + 0.4 * sin(t * 2.0));  // faint seam warmth
  // SPANNER (256,404): a gold invitation while unowned
  if (spOwned < 0.5) {
    let sc = vec2f(256.0, 404.0);
    c += vec3f(0.95, 0.80, 0.40) * tg_ring(px, sc, 30.0, 24.0) * (0.35 + 0.35 * sin(t * 2.5))
       * (0.6 + 0.4 * smoothstep(50.0, 18.0, length(mm - sc)));
  }
  // click-confirm ripple (shared convention)
  let cfx = uni(84);
  if (cfx > 0.003) {
    let cd = length(px - vec2f(uni(85), uni(86)));
    c += vec3f(0.9, 0.8, 0.6) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4;
  }
  return c;
}

module · tg_engine

// ENGINE ROOM (view 10) — THE GEARHEART. Raymarched brass machine wall in warm
// lamplight: three gear SEATS the player fills, real cogs whose tooth-count and
// radius match the id (8/12/16/20t), spinning off enginePhase and counter-
// rotating through fixed idler pinions, a driven flywheel, and a cadence
// resonator that reads GREEN-GOLD at the tide-bell tgt — the legibility the
// puzzle lacked. Shore recipe: SDF map → march → normal → shadow/AO → material.
// Anchors (px, LAW): seat1 (176,256) · seat2 (256,256) · seat3 (336,256) r46 ·
// seized 12t (90,404) r38. State: uni(51) phase · uni(63/64/65) seat ids ·
// uni(66) shaftCadence · uni(67) targetCadence · uni(68) engineOK · uni(70)
// gear12free · uni(62) held · mouse uni(27,28). Author: Claude Fable 5.

fn tge_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tge_n2(p: vec2f) -> f32 {
  let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
  return mix(mix(tge_h(i), tge_h(i + vec2f(1.0, 0.0)), u.x),
             mix(tge_h(i + vec2f(0.0, 1.0)), tge_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tge_fbm(p: vec2f, oct: i32) -> f32 {
  var v = 0.0; var a = 0.5; var q = p;
  for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tge_n2(q); q = q * 2.02 + vec2f(13.0, 5.0); a *= 0.5; }
  return v;
}
fn tge_cyl(p: vec3f, hz: f32, r: f32) -> f32 { let d = vec2f(length(p.xy) - r, abs(p.z) - hz); return min(max(d.x, d.y), 0.0) + length(max(d, vec2f(0.0))); }
fn tge_box(p: vec3f, b: vec3f) -> f32 { let q = abs(p) - b; return length(max(q, vec3f(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0); }
fn tge_rotZ2(p: vec3f, a: f32) -> vec3f { let c = cos(a); let s = sin(a); return vec3f(c * p.x - s * p.y, s * p.x + c * p.y, p.z); }

// A COG facing camera (+z): toothed rim by count + raised hub + centre bore,
// rotated by `rot`. pl is local to the gear centre.
fn tge_gear(pl: vec3f, R: f32, teeth: f32, rot: f32) -> f32 {
  let cr = cos(rot); let sr = sin(rot);
  let q = vec2f(cr * pl.x - sr * pl.y, sr * pl.x + cr * pl.y);
  let ang = atan2(q.y, q.x);
  let toothH = R * 0.14;
  let tw = smoothstep(-0.35, 0.35, cos(ang * teeth));      // square-ish tooth wave
  let rimR = (R - toothH * 0.5) + toothH * tw;             // radius rises on each tooth
  let disc2d = length(q) - rimR;
  var g = max(disc2d, abs(pl.z) - R * 0.15);               // extrude to a thin cog
  let hub = tge_cyl(pl, R * 0.22, R * 0.30);               // raised centre hub
  g = min(g, hub);
  let bore = max(length(q) - R * 0.10, abs(pl.z) - R * 0.35);  // axle bore through
  g = max(g, -bore);
  return g;
}
fn tge_teeth(id: f32) -> f32 {
  if (id > 4.5) { return 20.0; } if (id > 3.5) { return 16.0; } if (id > 2.5) { return 12.0; } return 8.0;
}
fn tge_radius(id: f32) -> f32 { return 0.30 + tge_teeth(id) * 0.018; }   // 8t≈0.44 … 20t≈0.66
fn tge_sx(k: i32) -> f32 { if (k <= 0) { return -1.19; } if (k == 1) { return 0.0; } return 1.19; }
fn tge_pick(k: i32, a: f32, b: f32, c: f32) -> f32 { return select(select(a, b, k == 1), c, k == 2); }

const TGE_SY = 1.20;
const TGE_SZ = -0.30;

// SDF → (dist, matId). mats: 1 wall plate · 2 rivet/frame brass · 3 cog gold-
// brass · 4 axle steel · 5 flywheel · 6 bell bronze · 7 seized rust · 8 pipe
fn tge_map(p: vec3f, s0: f32, s1: f32, s2: f32, phase: f32, seized: f32, t: f32) -> vec2f {
  var d = 1e5; var m = 1.0;
  // ── the machine WALL: a riveted plate behind everything ──
  let wall = tge_box(p - vec3f(0.0, 1.2, -0.75), vec3f(3.4, 1.9, 0.30));
  if (wall < d) { d = wall; m = 1.0; }
  let beam = min(tge_box(p - vec3f(0.0, 3.05, -0.55), vec3f(3.4, 0.14, 0.16)),
                 tge_box(p - vec3f(0.0, -0.62, -0.55), vec3f(3.4, 0.14, 0.16)));
  if (beam < d) { d = beam; m = 2.0; }
  // side pipes (cylinders standing vertical → local z is world y)
  let pl2 = vec3f(p.x + 3.05, p.z + 0.5, p.y - 1.2);
  let pr2 = vec3f(p.x - 3.05, p.z + 0.5, p.y - 1.2);
  let pipe = min(tge_cyl(pl2, 1.9, 0.10), tge_cyl(pr2, 1.9, 0.10));
  if (pipe < d) { d = pipe; m = 8.0; }
  // ══ A REAL MESHING GEAR TRAIN ══
  // One shared MODULE (tooth pitch) so any two gears' teeth actually interlock;
  // pitch radius R = module·teeth/2 (a 20t is bigger than an 8t by that ratio);
  // idlers between the fixed seats are SIZED to bridge the gap so pitch circles
  // are TANGENT (touch, never overlap or float); rotation is PHASE-LOCKED down
  // the chain (φ_next = -φ_prev·T_prev/T_next + interlock-offset) so a tooth of
  // one lands in the gap of the next at every mesh, and neighbours counter-turn.
  let MOD = 0.0595;                       // 1.19 seat-gap / 20 → biggest pair just touches
  let PIc = 3.14159265;
  let gy = TGE_SY; let gz = TGE_SZ;
  let sxA = -1.19; let sxB = 0.0; let sxC = 1.19;
  let T0 = tge_teeth(s0); let T1 = tge_teeth(s1); let T2 = tge_teeth(s2);
  let R0 = MOD * T0 * 0.5; let R1 = MOD * T1 * 0.5; let R2 = MOD * T2 * 0.5;
  let f0 = s0 > 1.5; let f1 = s1 > 1.5; let f2 = s2 > 1.5;
  // idlers sized so they exactly span the 1.19 gap tangent to both seat gears
  let TA = max((1.19 - R0 - R1) / MOD, 5.0); let RA = MOD * TA * 0.5; let xA = sxA + R0 + RA;
  let TB = max((1.19 - R1 - R2) / MOD, 5.0); let RB = MOD * TB * 0.5; let xB = sxB + R1 + RB;
  // phase chain (β=0, gears in a rightward row): interlock offset = π - π/T
  let p0 = phase;
  let pA = -p0 * (T0 / TA) + (PIc - PIc / TA);
  let p1 = -pA * (TA / T1) + (PIc - PIc / T1);
  let pB = -p1 * (T1 / TB) + (PIc - PIc / TB);
  let p2 = -pB * (TB / T2) + (PIc - PIc / T2);
  // axle boss always (an empty seat is a waiting hub)
  for (var k = 0; k < 3; k++) {
    let axle = tge_cyl(p - vec3f(tge_sx(k), gy, gz) + vec3f(0.0, 0.0, 0.14), 0.22, 0.11);
    if (axle < d) { d = axle; m = 4.0; }
  }
  // seat gears
  if (f0) { let g = tge_gear(p - vec3f(sxA, gy, gz), R0, T0, p0); if (g < d) { d = g; m = 3.0; } }
  if (f1) { let g = tge_gear(p - vec3f(sxB, gy, gz), R1, T1, p1); if (g < d) { d = g; m = 3.0; } }
  if (f2) { let g = tge_gear(p - vec3f(sxC, gy, gz), R2, T2, p2); if (g < d) { d = g; m = 3.0; } }
  // bridging idlers — only where BOTH seats are filled (else the train is broken)
  if (f0 && f1) { let g = tge_gear(p - vec3f(xA, gy, gz), RA, TA, pA); if (g < d) { d = g; m = 3.0; } }
  if (f1 && f2) { let g = tge_gear(p - vec3f(xB, gy, gz), RB, TB, pB); if (g < d) { d = g; m = 3.0; } }
  // ── the driven FLYWHEEL, right of seat C — belt-coupled (drawn in the shader),
  //    spins at seat C's rate; a smooth heavy wheel (flywheels have no teeth) ──
  let flySpin = p2 * 0.6;
  let fp = tge_rotZ2(p - vec3f(2.55, 1.20, gz - 0.05), flySpin);
  var fly = max(length(fp.xy) - 0.62, abs(fp.z) - 0.14);  // outer disc
  fly = max(fly, -(length(fp.xy) - 0.50));                // hollow to a heavy rim
  for (var k = 0; k < 5; k++) {                            // spokes
    let a = f32(k) * 1.25664;
    let dir = vec2f(cos(a), sin(a));
    let along = clamp(dot(fp.xy, dir), 0.0, 0.55);
    let perp = length(fp.xy - dir * along);
    fly = min(fly, max(perp - 0.05, abs(fp.z) - 0.10));
  }
  fly = min(fly, tge_cyl(fp, 0.17, 0.16));
  if (fly < d) { d = fly; m = 5.0; }
  // ── the cadence BELL / resonator, top centre ──
  let bp = p - vec3f(0.0, 2.55, -0.20);
  let bell = max(tge_cyl(bp, 0.20, 0.24), -(bp.y + 0.06));  // open-bottom bell
  if (bell < d) { d = bell; m = 6.0; }
  // ── the SEIZED 12t gear, rusted in at lower-left ──
  if (seized < 0.5) {
    let sg = p - vec3f(-1.85, -0.30, 0.05);
    let g = tge_gear(sg, 0.42, 12.0, 0.3);
    if (g < d) { d = g; m = 7.0; }
  }
  return vec2f(d, m);
}

fn tge_nrm(p: vec3f, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, t: f32) -> vec3f {
  let e = vec2f(0.006, 0.0);
  return normalize(vec3f(
    tge_map(p + e.xyy, s0, s1, s2, ph, sz, t).x - tge_map(p - e.xyy, s0, s1, s2, ph, sz, t).x,
    tge_map(p + e.yxy, s0, s1, s2, ph, sz, t).x - tge_map(p - e.yxy, s0, s1, s2, ph, sz, t).x,
    tge_map(p + e.yyx, s0, s1, s2, ph, sz, t).x - tge_map(p - e.yyx, s0, s1, s2, ph, sz, t).x));
}
fn tge_march(ro: vec3f, rd: vec3f, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, t: f32) -> vec2f {
  var tt = 0.02;
  for (var i = 0; i < 92; i++) {
    let h = tge_map(ro + rd * tt, s0, s1, s2, ph, sz, t);
    if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
    tt += h.x * 0.8; if (tt > 22.0) { break; }
  }
  return vec2f(-1.0, 0.0);
}
fn tge_shadow(ro: vec3f, rd: vec3f, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, t: f32) -> f32 {
  var res = 1.0; var tt = 0.05;
  for (var i = 0; i < 14; i++) {
    let h = tge_map(ro + rd * tt, s0, s1, s2, ph, sz, t).x;
    res = min(res, 8.0 * h / tt); tt += clamp(h, 0.03, 0.4);
    if (res < 0.03 || tt > 6.0) { break; }
  }
  return clamp(res, 0.0, 1.0);
}
fn tge_ao(p: vec3f, n: vec3f, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, t: f32) -> f32 {
  var occ = 0.0; var w = 1.0;
  for (var i = 1; i <= 4; i++) { let hh = 0.05 * f32(i); occ += w * (hh - tge_map(p + n * hh, s0, s1, s2, ph, sz, t).x); w *= 0.72; }
  return clamp(1.0 - 2.1 * occ, 0.0, 1.0);
}
fn tge_alb(m: f32, p: vec3f) -> vec3f {
  if (m < 1.5) {
    var a = vec3f(0.070, 0.062, 0.058) * (0.8 + 0.4 * tge_fbm(p.xy * 4.0, 3));
    let rv = length(fract(p.xy * 1.3) - vec2f(0.5));
    a += vec3f(0.10, 0.09, 0.07) * smoothstep(0.12, 0.06, rv) * 0.5;   // rivets
    return a;
  } else if (m < 2.5) { return vec3f(0.22, 0.16, 0.075); }
  else if (m < 3.5) { return vec3f(0.30, 0.215, 0.10); }               // cog gold-brass
  else if (m < 4.5) { return vec3f(0.11, 0.11, 0.12); }                // axle steel
  else if (m < 5.5) { return vec3f(0.16, 0.13, 0.09); }                // flywheel
  else if (m < 6.5) { return vec3f(0.26, 0.20, 0.11); }                // bell
  else if (m < 7.5) { return vec3f(0.12, 0.075, 0.05) * (0.7 + 0.6 * tge_fbm(p.xy * 9.0, 3)); }  // rust
  return vec3f(0.14, 0.12, 0.10);
}
fn tge_shade(ro: vec3f, rd: vec3f, tt: f32, m: f32, s0: f32, s1: f32, s2: f32, ph: f32, sz: f32, engineOK: f32, t: f32) -> vec3f {
  let p = ro + rd * tt;
  let n = tge_nrm(p, s0, s1, s2, ph, sz, t);
  let lp = vec3f(-1.7, 2.6, 2.2);
  let ldir = normalize(lp - p);
  let lampCol = vec3f(1.25, 0.78, 0.38);
  let dif = max(dot(n, ldir), 0.0);
  let sh = tge_shadow(p + n * 0.03, ldir, s0, s1, s2, ph, sz, t);
  let ao = tge_ao(p, n, s0, s1, s2, ph, sz, t);
  let fall = 1.0 / (1.0 + 0.06 * dot(lp - p, lp - p));
  var c = tge_alb(m, p) * (0.12 + 1.6 * dif * sh * fall) * ao;
  c += vec3f(0.10, 0.09, 0.12) * ao * 0.5;
  if (engineOK > 0.5) { c += tge_alb(m, p) * lampCol * 0.18 * (0.6 + 0.4 * sin(t * 3.0)); }
  let hv = normalize(ldir - rd);
  let spec = pow(max(dot(n, hv), 0.0), 40.0) * sh;
  c += lampCol * spec * (0.2 + 0.9 * step(1.5, m));
  return c;
}

fn mod_tg_engine(pp: vec2f, px: vec2f, t: f32) -> vec3f {
  let mm = vec2f(uni(27), uni(28));
  let s0 = uni(63); let s1 = uni(64); let s2 = uni(65);
  let phase = uni(51);
  let seized = uni(70);
  let engineOK = uni(68);
  let held = uni(62);
  let cad = uni(66); let tgt = uni(67);
  let ro = vec3f(0.0, 1.2, 3.5);
  let uvx = px.x / 256.0 - 1.0;
  let uvy = px.y / 256.0 - 1.0;
  let rd = normalize(vec3f(uvx, -uvy, -1.0));
  var c = vec3f(0.045, 0.040, 0.045) * (0.8 + 0.3 * tge_fbm(px * 0.01, 2));
  c += vec3f(0.55, 0.28, 0.10) * exp(-pow((uvy + 0.7) * 1.8, 2.0)) * 0.35;   // lamp glow band
  let hit = tge_march(ro, rd, s0, s1, s2, phase, seized, t);
  if (hit.x > 0.0) { c = tge_shade(ro, rd, hit.x, hit.y, s0, s1, s2, phase, seized, engineOK, t); }
  // the oil lamp, upper-left
  let lampP = vec2f(96.0, 70.0);
  c += vec3f(1.3, 0.85, 0.42) * smoothstep(20.0, 6.0, length(px - lampP));
  c += vec3f(0.8, 0.45, 0.20) * smoothstep(150.0, 0.0, length(px - lampP)) * 0.18;
  // ── seat affordances (screen-space) ──
  for (var k = 0; k < 3; k++) {
    let sc = vec2f(176.0 + f32(k) * 80.0, 256.0);
    let id = tge_pick(k, s0, s1, s2);
    let hov = smoothstep(52.0, 24.0, length(mm - sc));
    if (id < 1.5) {
      let ready = select(0.3, 0.85, held > 1.5);
      c += vec3f(0.55, 0.62, 0.68) * tg_ring(px, sc, 44.0, 39.0) * (0.3 + 0.3 * sin(t * 2.0)) * ready * (0.5 + 0.5 * hov);
      if (held > 1.5 && hov > 0.4) { c += vec3f(0.9, 0.72, 0.35) * tg_ring(px, sc, 30.0 + held * 3.0, 26.0 + held * 3.0) * 0.4; }
    } else {
      c += vec3f(0.9, 0.75, 0.4) * tg_ring(px, sc, 46.0, 43.0) * hov * 0.3;
    }
  }
  // ── CADENCE RESONATOR: green-gold near tgt, red-amber far ──
  let bc = vec2f(256.0, 60.0);
  let err = abs(cad - tgt) / max(tgt, 0.001);
  let good = 1.0 - clamp(err * 2.5, 0.0, 1.0);
  let bellCol = mix(vec3f(1.0, 0.35, 0.12), vec3f(0.30, 1.0, 0.45), good);
  c += bellCol * tg_disc(px, bc, 13.0) * (0.5 + 0.35 * sin(t * (1.5 + cad * 6.0)));
  c += bellCol * tg_ring(px, bc, 22.0, 18.0) * (0.4 + 0.3 * good);
  c += bellCol * smoothstep(70.0, 0.0, length(px - bc)) * good * engineOK * 0.15;
  // tgt-vs-actual gauge under the bell
  let bx = px.x - 116.0; let by = px.y - 96.0;
  c += vec3f(0.18, 0.42, 0.48) * step(0.0, bx) * step(bx, 280.0) * smoothstep(6.0, 2.0, abs(by)) * 0.6;
  c += vec3f(0.30, 0.85, 0.45) * step(0.0, bx) * step(bx, clamp(cad, 0.0, 1.0) * 280.0) * smoothstep(5.0, 2.0, abs(by));
  c += vec3f(0.95, 0.85, 0.45) * smoothstep(3.0, 1.0, abs(bx - tgt * 280.0)) * smoothstep(9.0, 3.0, abs(by));
  // ── seized 12t invitation (90,404) — wants the spanner HELD ──
  if (seized < 0.5) {
    let gc = vec2f(90.0, 404.0);
    let ready = select(0.35, 0.9, held > 0.5 && held < 1.5);
    c += vec3f(0.95, 0.80, 0.40) * tg_ring(px, gc, 30.0, 24.0) * (0.3 + 0.35 * sin(t * 2.5)) * ready
       * (0.6 + 0.4 * smoothstep(50.0, 18.0, length(mm - gc)));
  }
  // click-confirm ripple
  let cfx = uni(84);
  if (cfx > 0.003) { let cd = length(px - vec2f(uni(85), uni(86))); c += vec3f(0.9, 0.8, 0.6) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4; }
  return c;
}

module · tg_mirror

// MIRROR ISLE (view 11) — raymarched. The island's reflection gone wrong: a
// black-glass sea, a polished mirror wall holding THE SLUICE WHEEL, standing
// shards of broken reflection. Shore-recipe: SDF map → march → normal → soft
// shadow → material albedo → key light + planar sea reflection + foam collar.
// Anchors (px, LAW): sluice wheel (346,250) r~50 · moon (400,96) r24.
// State: uni(59) sluiceFreed · uni(62) held (1=spanner) · uni(78) element ·
// uni(82) elBlend · mouse uni(27,28). Author: Claude Fable 5.

fn tgm_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgm_n2(p: vec2f) -> f32 {
  let i = floor(p); let f = fract(p);
  let u = f * f * (3.0 - 2.0 * f);
  return mix(mix(tgm_h(i), tgm_h(i + vec2f(1.0, 0.0)), u.x),
             mix(tgm_h(i + vec2f(0.0, 1.0)), tgm_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgm_fbm(p: vec2f, oct: i32) -> f32 {
  var v = 0.0; var a = 0.5; var q = p;
  for (var i = 0; i < 4; i++) {
    if (i >= oct) { break; }
    v += a * tgm_n2(q); q = q * 2.03 + vec2f(17.0, 9.0); a *= 0.5;
  }
  return v;
}

fn tgm_sdBox(p: vec3f, b: vec3f) -> f32 {
  let q = abs(p) - b;
  return length(max(q, vec3f(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0);
}
fn tgm_sdTorusZ(p: vec3f, R: f32, r: f32) -> f32 {
  let q = vec2f(length(p.xy) - R, p.z);
  return length(q) - r;
}
fn tgm_sdCap(p: vec3f, a: vec3f, b: vec3f, r: f32) -> f32 {
  let pa = p - a; let ba = b - a;
  let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h) - r;
}

// wheel geometry constants — wheel center in WORLD, chosen so it projects to
// px(346,250) under the camera below (eye (0,1.7,5.2) → +x right, +uv.y down)
const TGM_WC = vec3f(1.83, 1.83, 0.10);
const TGM_WHEEL_R = 0.92;

// scene SDF → (distance, materialId)
// mats: 1 wall basalt frame · 2 mirror face · 3 wheel bronze · 4 grate iron ·
//       5 silt choke · 6 shard glass · 7 rock ledge
fn tgm_map(p: vec3f, freed: f32, t: f32) -> vec2f {
  var d = 1e5; var m = 1.0;
  // (the gate slot reads as shadowed depth via jambs/grate/silt — no boolean cut)
  // ── the mirror wall: a tall slab behind everything, carved frame + face ──
  let wall = tgm_sdBox(p - vec3f(0.0, 2.1, -0.35), vec3f(4.6, 2.2, 0.30));
  if (wall < d) { d = wall; m = 1.0; }
  // the polished FACE floats a breath in front of the slab
  let face = tgm_sdBox(p - vec3f(0.0, 2.05, -0.02), vec3f(4.15, 1.85, 0.015));
  if (face < d) { d = face; m = 2.0; }
  // ── THE SLUICE WHEEL — torus rim + five spokes + hub, half-proud of the wall ──
  let wp = p - TGM_WC;
  // freed → the wheel rests TURNED: spokes carry a settled extra angle
  // VALVE angle from the hook (U105): rest↔turned + the click-turn kick — the
  // wheel VISIBLY TURNS when worked (Galen). Pre-freed it rests at zero.
  let spin = uni(105);
  var wheel = tgm_sdTorusZ(wp, TGM_WHEEL_R, 0.085);
  for (var k = 0; k < 5; k++) {
    let a = f32(k) * 1.2566 + 0.35 + spin;
    let dir = vec3f(cos(a), sin(a), 0.0) * (TGM_WHEEL_R - 0.06);
    wheel = min(wheel, tgm_sdCap(wp, vec3f(0.0), dir, 0.055));
  }
  wheel = min(wheel, length(wp) - 0.16);                       // hub
  if (wheel < d) { d = wheel; m = 3.0; }
  // ── the sluice gate: a slot below the wheel, grated; silted until freed ──
  let gp = p - vec3f(1.83, 0.62, -0.02);
  // the slot is implied by its iron jambs, lintel, grate and (until freed) silt
  let jamb = min(tgm_sdBox(gp - vec3f(-0.42, 0.0, 0.06), vec3f(0.07, 0.52, 0.10)),
                 tgm_sdBox(gp - vec3f(0.42, 0.0, 0.06), vec3f(0.07, 0.52, 0.10)));
  if (jamb < d) { d = jamb; m = 4.0; }
  let lintel = tgm_sdBox(gp - vec3f(0.0, 0.55, 0.06), vec3f(0.49, 0.07, 0.10));
  if (lintel < d) { d = lintel; m = 4.0; }
  // grate bars across the slot
  var grate = 1e5;
  for (var g = 0; g < 4; g++) {
    let gx = -0.255 + f32(g) * 0.17;
    grate = min(grate, tgm_sdCap(gp, vec3f(gx, -0.42, 0.05), vec3f(gx, 0.46, 0.05), 0.028));
  }
  if (grate < d) { d = grate; m = 4.0; }
  // silt choke fills the slot until the wheel turns
  if (freed < 0.5) {
    let silt = tgm_sdBox(gp - vec3f(0.0, -0.10, 0.02), vec3f(0.30, 0.30, 0.09));
    let bulge = 0.03 * tgm_fbm(gp.xy * 6.0, 3);
    if (silt - bulge < d) { d = silt - bulge; m = 5.0; }
  }
  // ── broken mirror shards standing out of the black water, left side —
  //    thin tilted planes that catch the moon on their edges ──
  let s1 = tgm_sdBox(tgm_rotY(p - vec3f(-2.45, 0.66, 1.45), 0.42), vec3f(0.028, 0.86, 0.36));
  if (s1 < d) { d = s1; m = 6.0; }
  let s2 = tgm_sdBox(tgm_rotY(p - vec3f(-1.75, 0.42, 2.15), -0.65), vec3f(0.024, 0.58, 0.27));
  if (s2 < d) { d = s2; m = 6.0; }
  let s3 = tgm_sdBox(tgm_rotY(p - vec3f(-3.05, 0.35, 2.35), 1.05), vec3f(0.022, 0.44, 0.21));
  if (s3 < d) { d = s3; m = 6.0; }
  let s4 = tgm_sdBox(tgm_rotY(p - vec3f(-1.15, 0.28, 2.85), 0.2), vec3f(0.02, 0.30, 0.15));
  if (s4 < d) { d = s4; m = 6.0; }
  // ── wall pilasters + cornice: the mirror is HELD, not floating ──
  let pil = min(tgm_sdBox(p - vec3f(-4.25, 2.1, 0.02), vec3f(0.16, 2.15, 0.14)),
                tgm_sdBox(p - vec3f(4.25, 2.1, 0.02), vec3f(0.16, 2.15, 0.14)));
  if (pil < d) { d = pil; m = 1.0; }
  let corn = tgm_sdBox(p - vec3f(0.0, 4.02, 0.05), vec3f(4.45, 0.12, 0.22));
  if (corn < d) { d = corn; m = 1.0; }
  // ── low broken stones at the wall's foot — gapped in front of the gate so the
  //    sluice mouth stays visible and its water can reach the sea ──
  let lp = vec3f(glsl_mod(p.x + 0.55, 1.1) - 0.55, p.y, p.z);
  var ledge = tgm_sdBox(lp - vec3f(0.0, 0.045, 0.55), vec3f(0.42, 0.085, 0.42)) - 0.035 * tgm_fbm(p.xz * 4.0, 3);
  ledge = max(ledge, -(abs(p.x - 1.83) - 0.62));      // the gap before the gate
  ledge = max(ledge, abs(p.x - 0.6) - 3.6);           // bounded run
  if (ledge < d) { d = ledge; m = 7.0; }
  return vec2f(d, m);
}

fn tgm_rotY(p: vec3f, a: f32) -> vec3f {
  let c = cos(a); let s = sin(a);
  return vec3f(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
}

fn tgm_nrm(p: vec3f, freed: f32, t: f32) -> vec3f {
  let e = vec2f(0.006, 0.0);
  return normalize(vec3f(
    tgm_map(p + e.xyy, freed, t).x - tgm_map(p - e.xyy, freed, t).x,
    tgm_map(p + e.yxy, freed, t).x - tgm_map(p - e.yxy, freed, t).x,
    tgm_map(p + e.yyx, freed, t).x - tgm_map(p - e.yyx, freed, t).x));
}

fn tgm_march(ro: vec3f, rd: vec3f, freed: f32, t: f32) -> vec2f {
  var tt = 0.02;
  for (var i = 0; i < 80; i++) {
    let h = tgm_map(ro + rd * tt, freed, t);
    if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
    tt += h.x * 0.9;
    if (tt > 26.0) { break; }
  }
  return vec2f(-1.0, 0.0);
}

fn tgm_shadow(ro: vec3f, rd: vec3f, freed: f32, t: f32) -> f32 {
  var res = 1.0; var tt = 0.06;
  for (var i = 0; i < 14; i++) {
    let h = tgm_map(ro + rd * tt, freed, t).x;
    res = min(res, 9.0 * h / tt);
    tt += clamp(h, 0.04, 0.5);
    if (res < 0.02 || tt > 8.0) { break; }
  }
  return clamp(res, 0.0, 1.0);
}

fn tgm_ao(p: vec3f, n: vec3f, freed: f32, t: f32) -> f32 {
  var occ = 0.0; var w = 1.0;
  for (var i = 1; i <= 4; i++) {
    let hh = 0.05 * f32(i);
    occ += w * (hh - tgm_map(p + n * hh, freed, t).x);
    w *= 0.75;
  }
  return clamp(1.0 - 2.2 * occ, 0.0, 1.0);
}

// material albedo (+ small procedural detail), matching the island's palette
fn tgm_alb(m: f32, p: vec3f, warm: f32) -> vec3f {
  if (m < 1.5) {        // basalt frame — carved, wet-streaked
    var a = vec3f(0.055, 0.052, 0.060) * (0.75 + 0.5 * tgm_fbm(p.xy * 3.1 + p.zz, 3));
    a *= 1.0 - 0.22 * smoothstep(0.35, 0.5, abs(fract(p.y * 2.2) - 0.5));   // course lines
    a *= 1.0 - 0.3 * smoothstep(0.6, 0.0, p.y) * tgm_fbm(p.xz * 5.0, 2);    // damp foot
    return a;
  } else if (m < 2.5) { // mirror face — near-black; carved panel seams whisper across it
    var f = vec3f(0.012, 0.015, 0.022);
    let seam = smoothstep(0.030, 0.012, abs(fract(p.x * 0.62 + 0.5) - 0.5) * 1.6)
             + smoothstep(0.030, 0.012, abs(fract(p.y * 0.62) - 0.5) * 1.6);
    f += vec3f(0.010, 0.014, 0.020) * seam;
    return f;
  } else if (m < 3.5) { // wheel — verdigris bronze
    let ver = tgm_fbm(p.xy * 7.0, 3);
    return mix(vec3f(0.16, 0.115, 0.055), vec3f(0.075, 0.145, 0.115), smoothstep(0.45, 0.75, ver)) * (1.0 + warm * 0.25);
  } else if (m < 4.5) { // iron
    return vec3f(0.052, 0.048, 0.050) * (0.8 + 0.4 * tgm_fbm(p.xy * 9.0, 2));
  } else if (m < 5.5) { // silt
    return vec3f(0.070, 0.075, 0.070) * (0.7 + 0.6 * tgm_fbm(p.xy * 11.0, 3));
  } else if (m < 6.5) { // shard glass — cold, faintly teal
    return vec3f(0.05, 0.09, 0.10);
  }
  // wet rock ledge
  return vec3f(0.060, 0.062, 0.068) * (0.7 + 0.5 * tgm_fbm(p.xz * 5.0, 3));
}

// shade a solid hit — key light is the moon (cool → warms with elBlend)
fn tgm_shade(ro: vec3f, rd: vec3f, tt: f32, m: f32, freed: f32, warm: f32, t: f32) -> vec3f {
  let p = ro + rd * tt;
  let n = tgm_nrm(p, freed, t);
  let ldir = normalize(vec3f(0.55, 0.62, 0.45));
  let key = mix(vec3f(0.72, 0.82, 1.0), vec3f(1.15, 0.62, 0.30), warm);
  let dif = max(dot(n, ldir), 0.0);
  let sh = tgm_shadow(p + n * 0.03, ldir, freed, t);
  let ao = tgm_ao(p, n, freed, t);
  let bnc = max(dot(n, vec3f(0.0, -1.0, 0.0)), 0.0) * 0.10;      // sea bounce
  var c = tgm_alb(m, p, warm) * (0.18 + 1.2 * dif * sh) * ao + key * bnc * ao * 0.4;
  c += key * 0.06 * ao;                                           // fill
  // speculars
  let hv = normalize(ldir - rd);
  let spec = pow(max(dot(n, hv), 0.0), 34.0) * sh;
  if (m > 1.5 && m < 2.5) {
    // the mirror face: sky reflection + fresnel — the wall that holds another world
    let rr = reflect(rd, n);
    let fres = pow(1.0 - max(dot(-rd, n), 0.0), 3.0);
    var sky = mix(vec3f(0.020, 0.030, 0.055), vec3f(0.055, 0.045, 0.075), clamp(rr.y, 0.0, 1.0));
    sky = mix(sky, vec3f(0.35, 0.25, 0.18), warm * 0.4);
    c += sky * (0.35 + 0.6 * fres);
    c += key * pow(max(dot(rr, ldir), 0.0), 60.0) * 0.8;          // moon glint in the wall
  } else if (m > 5.5 && m < 6.5) {
    let fres = pow(1.0 - max(dot(-rd, n), 0.0), 2.5);
    c += mix(vec3f(0.10, 0.16, 0.20), vec3f(0.30, 0.16, 0.10), warm) * (0.25 + 0.75 * fres);
    c += key * pow(fres, 5.0) * 1.1;   // the moonlit shard edge
    c += key * spec * 1.3;
  } else {
    c += key * spec * (0.25 + 0.3 * step(2.5, m));
  }
  // wet streak below an opened gate — the wall remembers the water
  if (freed > 0.5 && m < 2.5) {
    let wet = smoothstep(0.5, 0.15, abs(p.x - 1.83)) * smoothstep(1.15, 0.2, p.y);
    c *= 1.0 - 0.35 * wet;
  }
  return c;
}

fn mod_tg_mirror(pp: vec2f, px: vec2f, t: f32) -> vec3f {
  let mm = vec2f(uni(27), uni(28));
  let freed = uni(59);
  let held = uni(62);
  let warm = clamp(uni(82), 0.0, 1.0);        // eased element morph (fire)
  // ── camera: standing on the water before the wall ──
  let ro = vec3f(0.0, 1.7, 5.2);
  let uvx = px.x / 256.0 - 1.0;
  let uvy = px.y / 256.0 - 1.0;                // y DOWN in px
  let rd = normalize(vec3f(uvx, -uvy + 0.02, -1.0));
  // ── sky base: deep night, warms toward ember under the fire moon ──
  var c = mix(vec3f(0.012, 0.020, 0.042), vec3f(0.030, 0.022, 0.050), clamp(-uvy, 0.0, 1.0));
  c = mix(c, vec3f(0.075, 0.038, 0.030), warm * 0.5);
  let st = tgm_h(floor(px / 3.0));
  if (st > 0.9965) { c += vec3f(0.7, 0.8, 1.0) * (0.4 + 0.6 * sin(t * 1.3 + st * 90.0)) * 0.5; }
  // ── march: solids first; sea plane as the mirror floor ──
  let hit = tgm_march(ro, rd, freed, t);
  let seaT = select(1e5, (0.0 - ro.y) / rd.y, rd.y < -0.001);
  if (hit.x > 0.0 && hit.x < seaT) {
    c = tgm_shade(ro, rd, hit.x, hit.y, freed, warm, t);
    // depth mist toward the wall
    c = mix(c, vec3f(0.020, 0.030, 0.050), smoothstep(6.0, 16.0, hit.x) * 0.5);
  } else if (seaT < 1e4) {
    // ── the BLACK-GLASS SEA: planar reflection — the true mirror ──
    let sp = ro + rd * seaT;
    // gentle ripple normal (amplitude grows as the element turns to fire)
    let ramp = 0.012 + 0.03 * warm;
    let rip = vec2f(tgm_fbm(sp.xz * 2.6 + vec2f(t * 0.14, 0.0), 3) - 0.5,
                    tgm_fbm(sp.xz * 2.6 + vec2f(0.0, t * 0.11) + 31.7, 3) - 0.5) * ramp;
    let sn = normalize(vec3f(rip.x, 1.0, rip.y));
    let rr = normalize(reflect(rd, sn));
    // march the reflected ray against the solids — the world doubled, darker
    let rhit = tgm_march(sp + vec3f(0.0, 0.015, 0.0), rr, freed, t);
    var refl: vec3f;
    if (rhit.x > 0.0) {
      refl = tgm_shade(sp, rr, rhit.x, rhit.y, freed, warm, t) * 0.55;
      // the wrongness: the reflection breathes, slightly out of true
      refl *= 0.85 + 0.15 * sin(t * 0.7 + sp.x * 2.0);
    } else {
      refl = mix(vec3f(0.016, 0.024, 0.048), vec3f(0.06, 0.035, 0.03), warm * 0.6);
    }
    // deep water base + fresnel toward the reflection
    let fres = pow(1.0 - max(dot(-rd, sn), 0.0), 2.2);
    var sea = mix(vec3f(0.004, 0.008, 0.014), refl, 0.45 + 0.5 * fres);
    sea = mix(sea, vec3f(0.10, 0.045, 0.020), warm * 0.25);
    // moon lane on the water
    let ldir = normalize(vec3f(0.55, 0.62, 0.45));
    let lane = pow(max(dot(rr, ldir), 0.0), 130.0);
    sea += mix(vec3f(0.7, 0.8, 1.0), vec3f(1.2, 0.6, 0.3), warm) * lane * 1.6;
    // FOAM COLLAR — churn where solids break the glass (wall foot, shards)
    let dWall = tgm_map(vec3f(sp.x, 0.02, sp.z), freed, t).x;
    let foam = smoothstep(0.30, 0.04, dWall) * (0.5 + 0.5 * tgm_n2(sp.xz * 9.0 + vec2f(t * 0.9, 0.0)));
    sea += vec3f(0.55, 0.62, 0.70) * foam * 0.35;
    c = sea;
    // distance mist over far water
    c = mix(c, vec3f(0.018, 0.026, 0.048), smoothstep(8.0, 22.0, seaT) * 0.6);
  }
  // ── the freed sluice: a living thread of water from the gate to the sea ──
  if (uni(106) > 0.5) {   // the thread runs while the VALVE is open (draining)
    // screen-space stream: from the gate mouth (~px 346,352) down to the water
    let sx = 346.0 + 5.0 * sin(px.y * 0.05 + t * 2.2);
    let sw = smoothstep(7.0, 0.5, abs(px.x - sx)) * smoothstep(340.0, 360.0, px.y) * smoothstep(430.0, 408.0, px.y);
    let flow = 0.6 + 0.4 * sin(px.y * 0.35 - t * 9.0);
    c += vec3f(0.50, 0.72, 0.85) * sw * flow * 0.9;
    let splash = smoothstep(26.0, 4.0, length(px - vec2f(346.0, 424.0)));
    c += vec3f(0.55, 0.68, 0.78) * splash * (0.4 + 0.3 * sin(t * 6.0)) * 0.5;
  }
  // ── anchor affordances (screen-space light only) ──
  let wc2 = vec2f(346.0, 250.0);
  let hovW = smoothstep(70.0, 28.0, length(mm - wc2));
  // hover: the wheel answers with rim light
  c += vec3f(0.85, 0.70, 0.35) * tg_ring(px, wc2, 52.0, 47.0) * hovW * 0.35;
  // un-freed: a patient gold invitation; holding the spanner sharpens it
  if (freed < 0.5) {
    let ready = select(0.35, 0.85, held > 0.5 && held < 1.5);
    c += vec3f(0.95, 0.80, 0.40) * tg_ring(px, wc2, 60.0, 54.0) * (0.22 + 0.28 * sin(t * 2.2)) * ready;
  }
  // (no moon in the sluice — the moon puzzle lives on the shore) 
  return c;
}

module · tg_lib


// ── the four tide-glyphs: crescent, triad, star, ringed bar ──
// unit cell p in [-1,1], returns coverage 0..1
fn mod_tg_glyph(idx: i32, p: vec2f) -> f32 {
  var d = 1.0;
  if (idx == 0) {                    // crescent — the hooked tide
    let a = length(p) - 0.62;
    let b = length(p - vec2f(0.30, -0.12)) - 0.55;
    d = max(a, -b);
  } else if (idx == 1) {             // triad — three drops
    let d1 = length(p - vec2f(0.0, -0.38)) - 0.20;
    let d2 = length(p - vec2f(-0.38, 0.30)) - 0.20;
    let d3 = length(p - vec2f(0.38, 0.30)) - 0.20;
    d = min(d1, min(d2, d3));
  } else if (idx == 2) {             // four-point star — the vault
    d = sdStar(p, 0.72, 4, 2.6);
  } else {                           // ringed bar — the mooring
    let ring = abs(length(p) - 0.52) - 0.10;
    let bar = sdBox(p, vec2f(0.10, 0.72));
    d = min(ring, bar);
  }
  return smoothstep(0.05, -0.05, d);
}
fn mod_tg_gcol(idx: i32) -> vec3f {
  if (idx == 0) { return vec3f(1.00, 0.62, 0.22); }   // amber
  if (idx == 1) { return vec3f(0.25, 0.85, 0.75); }   // teal
  if (idx == 2) { return vec3f(0.66, 0.48, 1.00); }   // violet
  return vec3f(1.00, 0.38, 0.48);                     // rose
}
// ═══ ONE-DAY-grade 3D sky + ocean for the shore (ported technique) ═══
fn mod_tgo_suncol(el: f32) -> vec3f {
  return mix(vec3f(1.30, 0.45, 0.16), vec3f(1.15, 1.05, 0.90), smoothstep(0.02, 0.55, el));
}
fn mod_tgo_sky(rd: vec3f, sd: vec3f, md: vec3f, t: f32, vault: f32) -> vec3f {
  let el = sd.y;
  let y = max(rd.y, 0.0);
  let day = smoothstep(-0.10, 0.35, el);
  let night = smoothstep(0.05, -0.18, el);
  let zen = mix(vec3f(0.012, 0.018, 0.045), vec3f(0.15, 0.30, 0.58), day);
  let horDay = mix(vec3f(1.00, 0.42, 0.16), vec3f(0.46, 0.58, 0.72), smoothstep(0.10, 0.55, el));
  let hor = mix(vec3f(0.030, 0.032, 0.070), horDay, smoothstep(-0.16, 0.06, el));
  var c = mix(hor, zen, pow(y, 0.6));
  // the sun: wide bloom, tight halo, HDR core
  let sdot = clamp(dot(rd, sd), 0.0, 1.0);
  let sunUp = smoothstep(-0.14, 0.02, el);
  c += mod_tgo_suncol(el) * pow(sdot, 6.0) * 0.24 * sunUp;
  c += mod_tgo_suncol(el) * pow(sdot, 190.0) * 0.85 * sunUp;
  c += vec3f(6.0, 4.1, 2.2) * smoothstep(0.99988, 0.99997, sdot) * sunUp;
  // the vault-lamp moon of the finale night
  let mdot = clamp(dot(rd, md), 0.0, 1.0);
  let moonUp = night * smoothstep(-0.05, 0.10, md.y);
  c += vec3f(0.45, 0.72, 0.80) * pow(mdot, 90.0) * 0.5 * moonUp;
  c += vec3f(2.2, 3.0, 3.1) * smoothstep(0.99985, 0.99995, mdot) * moonUp;
  // stars + a faint galaxy band
  if (night > 0.02 && rd.y > 0.01) {
    let sp = rd.xz / (rd.y + 0.55) * 26.0;
    let cell = floor(sp);
    let h = hash21(cell);
    let tw = 0.55 + 0.45 * sin(t * (0.8 + h) + h * 50.0);
    c += vec3f(0.72, 0.78, 0.95) * step(0.990, h) * smoothstep(0.30, 0.05, length(fract(sp) - 0.5)) * night * tw;
    let band = pow(max(1.0 - abs(rd.x * 0.8 + rd.y * 0.5 - 0.15), 0.0), 3.5);
    c += vec3f(0.07, 0.07, 0.12) * band * night * (0.5 + 0.3 * vnoise(sp * 0.5));
  }
  // aurora curtains once the vault has risen
  if (vault > 0.01 && rd.y > 0.04) {
    let ax = rd.x / (rd.y + 0.35);
    let wave = sin(ax * 2.2 + t * 0.22) * 0.5 + sin(ax * 5.1 - t * 0.13) * 0.22;
    let curt = exp(-pow((rd.y - 0.34 - wave * 0.10) * 4.5, 2.0));
    let flick = 0.65 + 0.35 * vnoise(vec2f(ax * 3.0, t * 0.35));
    c += vec3f(0.10, 0.85, 0.45) * curt * flick * vault * night * 0.35;
    c += vec3f(0.35, 0.20, 0.75) * exp(-pow((rd.y - 0.52 - wave * 0.13) * 5.0, 2.0)) * flick * vault * night * 0.16;
  }
  // sparse dusk clouds, tinted by the hour
  if (rd.y > 0.015 && night < 0.9) {
    let cp = rd.xz / (rd.y + 0.14) * 1.4 + vec2f(t * 0.006, t * 0.002);
    var cl = fbm(cp * 0.55, 4);
    cl = smoothstep(0.46, 0.78, cl);
    let cloudLit = mix(vec3f(0.06, 0.06, 0.10), mix(vec3f(1.15, 0.50, 0.28), vec3f(0.85, 0.85, 0.90), smoothstep(0.12, 0.5, el)), max(day, night * 0.12));
    c = mix(c, cloudLit, cl * 0.7 * smoothstep(0.015, 0.10, rd.y) * (1.0 - night));
  }
  return c;
}
fn mod_tgo_ic() -> vec2f { return vec2f(6.5, 16.0); }   // the island's rock in sea space
fn mod_tgo_oct(uv0: vec2f, choppy: f32) -> f32 {
  let n = gnoise(uv0);
  let uv = uv0 + vec2f(n, n);
  var wv = 1.0 - abs(sin(uv));
  let swv = abs(cos(uv));
  wv = mix(wv, swv, wv);
  return pow(1.0 - pow(wv.x * wv.y, 0.65), choppy);
}
fn mod_tgo_surge(pxz: vec2f, st: f32) -> f32 {
  let fB2 = clamp(uni(82), 0.0, 1.0);
  let dr = length(pxz - mod_tgo_ic());
  return exp(-max(dr - 4.6, 0.0) * 0.4) * (sin(st * 1.3 - dr * 0.9) * 0.5 + 0.62) * 0.9 * mix(1.0, 0.4, fB2);
}
fn mod_tgo_map3(p: vec3f, st: f32) -> f32 {
  // FIRE MORPHS THE FORM (Galen Jul 30): magma is slow, heavy, unchopped — uni(82)
  let fB = clamp(uni(82), 0.0, 1.0);
  var freq = 0.16;   // freq CONSTANT — morphing it rescales the pattern mid-blend (the residual speed-up)
  var amp = mix(0.62, 0.80, fB);
  var choppy = mix(4.0, 1.15, fB);
  var uv = p.xz;
  uv.x = uv.x * 0.75;
  var h = 0.0;
  for (var i = 0; i < 3; i++) {
    var d = mod_tgo_oct((uv + vec2f(st)) * freq, choppy);
    d = d + mod_tgo_oct((uv - vec2f(st)) * freq, choppy);
    h = h + d * amp;
    uv = mat2x2f(1.6, 1.2, -1.2, 1.6) * uv;
    freq = freq * 1.9;
    amp = amp * mix(0.22, 0.11, fB);
    choppy = mix(choppy, 1.0, 0.2);
  }
  return p.y - h - mod_tgo_surge(p.xz, st);
}
fn mod_tgo_map5(p: vec3f, st: f32) -> f32 {
  // FIRE MORPHS THE FORM (Galen Jul 30): magma is slow, heavy, unchopped — uni(82)
  let fB = clamp(uni(82), 0.0, 1.0);
  var freq = 0.16;   // freq CONSTANT — morphing it rescales the pattern mid-blend (the residual speed-up)
  var amp = mix(0.62, 0.80, fB);
  var choppy = mix(4.0, 1.15, fB);
  var uv = p.xz;
  uv.x = uv.x * 0.75;
  var h = 0.0;
  for (var i = 0; i < 5; i++) {
    var d = mod_tgo_oct((uv + vec2f(st)) * freq, choppy);
    d = d + mod_tgo_oct((uv - vec2f(st)) * freq, choppy);
    h = h + d * amp;
    uv = mat2x2f(1.6, 1.2, -1.2, 1.6) * uv;
    freq = freq * 1.9;
    amp = amp * mix(0.22, 0.11, fB);
    choppy = mix(choppy, 1.0, 0.2);
  }
  return p.y - h - mod_tgo_surge(p.xz, st);
}
// cheap height at a point — the buoys ride the SAME sea (2 octaves + surge)
fn mod_tgo_h2(uv0: vec2f, st: f32) -> f32 {
  var uv = uv0;
  uv.x = uv.x * 0.75;
  // FIRE MORPHS THE FORM (Galen Jul 30): magma is slow, heavy, unchopped — uni(82)
  let fB = clamp(uni(82), 0.0, 1.0);
  var freq = 0.16;   // freq CONSTANT — morphing it rescales the pattern mid-blend (the residual speed-up)
  var amp = mix(0.62, 0.80, fB);
  var choppy = mix(4.0, 1.15, fB);
  var h = 0.0;
  for (var i = 0; i < 2; i++) {
    var d = mod_tgo_oct((uv + vec2f(st)) * freq, choppy);
    d = d + mod_tgo_oct((uv - vec2f(st)) * freq, choppy);
    h = h + d * amp;
    uv = mat2x2f(1.6, 1.2, -1.2, 1.6) * uv;
    freq = freq * 1.9;
    amp = amp * mix(0.22, 0.11, fB);
    choppy = mix(choppy, 1.0, 0.2);
  }
  return h + mod_tgo_surge(uv0, st);
}
// ═══ the bell-buoys: true marched bodies riding the swell ═══
// local frame: origin at the waterline, +y up. mats: 0 iron hull · 1 cage ·
// 2 the bell · 3 lantern head (emissive, glyph-colored)
fn mod_tgb_sdf(q0: vec3f, lean: f32) -> vec2f {
  let cl = cos(lean); let sl = sin(lean);
  let q = vec3f(cl * q0.x - sl * q0.y, sl * q0.x + cl * q0.y, q0.z);
  // squat cone float with a waterline skirt
  let ft = clamp((q.y + 0.40) / 0.60, 0.0, 1.0);
  let fd = vec2f(length(q.xz) - mix(0.34, 0.52, ft) * step(q.y, 0.20) - mix(0.52, 0.10, clamp((q.y - 0.20) / 0.25, 0.0, 1.0)) * step(0.20, q.y), 0.0);
  var d = max(length(q.xz) - mix(0.34, 0.52, ft), max(q.y - 0.42, -0.40 - q.y));
  d = min(d, length(vec2f(length(q.xz) - 0.50, q.y - 0.02)) - 0.055);        // skirt torus
  var m = 0.0;
  // tripod cage: three struts leaning to a collar
  let pr = mod_tg_polar3(q);
  let strut = mod_tg_seg3(pr, vec3f(0.34, 0.30, 0.0), vec3f(0.05, 1.30, 0.0)) - 0.030;
  if (strut < d) { d = strut; m = 1.0; }
  let collar = length(vec2f(length(q.xz) - 0.09, q.y - 1.30)) - 0.032;
  if (collar < d) { d = collar; m = 1.0; }
  // the bell hangs in the cage
  let bell = max(length((q - vec3f(0.0, 0.88, 0.0)) * vec3f(1.0, 0.80, 1.0)) - 0.17, 0.62 - q.y);
  if (bell < d) { d = bell; m = 2.0; }
  // lantern head above the collar
  let lant = mod_w3ish_box(q - vec3f(0.0, 1.46, 0.0), vec3f(0.062, 0.080, 0.062)) - 0.015;
  if (lant < d) { d = lant; m = 3.0; }
  return vec2f(d, m);
}
fn mod_tg_polar3(p: vec3f) -> vec3f {
  let ang = 2.0943951;
  let a = atan2(p.z, p.x);
  let r = length(p.xz);
  let a2 = (fract(a / ang + 0.5) - 0.5) * ang;
  return vec3f(cos(a2) * r, p.y, sin(a2) * r);
}
fn mod_tg_seg3(p: vec3f, a: vec3f, b: vec3f) -> f32 {
  let pa = p - a; let ba = b - a;
  let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h);
}
fn mod_w3ish_box(p: vec3f, b: vec3f) -> f32 {
  let d = abs(p) - b;
  return length(max(d, vec3f(0.0))) + min(max(d.x, max(d.y, d.z)), 0.0);
}
// ═══ the island observatory: a real building, raymarched (ONE DAY treatment) ═══
// materials: 0 rock · 1 masonry · 2 iron · 3 glass dome · 4 roof · 5 cottage · 6 chimney · 7 beacon
fn mod_tgi_sdf(pw: vec3f) -> vec2f {
  let q = pw - vec3f(6.5, 0.0, 16.0);
  // the rock: noised mass with a leveled court
  var d = length(q * vec3f(0.85, 1.45, 1.30)) - 2.85;
  d = d + (vnoise3(q * 1.1) - 0.5) * 0.85 + (vnoise3(q * 3.1) - 0.5) * 0.28;
  d = max(d, q.y - 2.08);                                   // the court
  var m = 0.0;
  // tapered masonry tower on the court's east rise
  let ty = clamp((q.y - 1.9) / 3.0, 0.0, 1.0);
  let tr = mix(0.60, 0.40, pow(ty, 0.8)) * (1.0 + 0.45 * smoothstep(2.7, 1.9, q.y));
  let dt = max(length(q.xz - vec2f(0.6, 0.0)) - tr, abs(q.y - 3.42) - 1.55);
  if (dt < d) { d = dt; m = 1.0; }
  // gallery deck + handrail ring + balusters
  let rxz = length(q.xz - vec2f(0.6, 0.0));
  let dDeck = max(rxz - 0.74, abs(q.y - 4.99) - 0.07);
  if (dDeck < d) { d = dDeck; m = 2.0; }
  let dRail = max(abs(rxz - 0.64) - 0.032, abs(q.y - 5.34) - 0.035);
  if (dRail < d) { d = dRail; m = 2.0; }
  let ph = atan2(q.z, q.x - 0.6);
  let seg = 0.3927;                                          // 2π/16
  let aph = (glsl_mod(ph + seg * 0.5, seg) - seg * 0.5) * 0.64;
  let dBal = max(length(vec2f(rxz - 0.64, aph)) - 0.028, abs(q.y - 5.16) - 0.17);
  if (dBal < d) { d = dBal; m = 2.0; }
  // the tide-glass itself: the great dome over the gallery
  let dGl = max(length(q - vec3f(0.6, 5.02, 0.0)) - 0.56, 5.04 - q.y);
  if (dGl < d) { d = dGl; m = 3.0; }
  // finial beacon
  let dFin = length(q - vec3f(0.6, 5.72, 0.0)) - 0.11;
  if (dFin < d) { d = dFin; m = 7.0; }
  // keeper's cottage on the west court: walls, gable roof, chimney
  let qc = q - vec3f(-1.35, 2.36, 0.25);
  let dW = sdBox(qc.xz, vec2f(0.78, 0.58));
  let dWall = max(dW, abs(qc.y + 0.1) - 0.52);
  if (dWall < d) { d = dWall; m = 5.0; }
  let dRoof = max(max(abs(qc.z) * 0.85 + (qc.y - 0.85), -(qc.y - 0.32)), max(abs(qc.x) - 0.92, abs(qc.z) - 0.70));
  if (dRoof < d) { d = dRoof; m = 4.0; }
  let dCh = max(sdBox(qc.xz - vec2f(0.38, 0.0), vec2f(0.13, 0.13)), abs(qc.y - 1.02) - 0.38);
  if (dCh < d) { d = dCh; m = 6.0; }
  return vec2f(d, m);
}
// soft shadow: march from a surface point toward the sun through the island
fn mod_tgi_shadow(ro2: vec3f, ld: vec3f) -> f32 {
  var t2 = 0.30;
  var sh = 1.0;
  for (var i = 0; i < 10; i++) {
    let d = mod_tgi_sdf(ro2 + ld * t2).x;
    sh = min(sh, 7.0 * d / t2);
    t2 = t2 + clamp(d, 0.18, 1.4);
    if (sh < 0.02 || t2 > 15.0) { break; }
  }
  return clamp(sh, 0.0, 1.0);
}
// bounded march for reflections: (t, material) or (-1,-1)
fn mod_tgi_march(ro2: vec3f, rd2: vec3f, tmax: f32) -> vec2f {
  var t2 = 0.4;
  for (var i = 0; i < 22; i++) {
    let dm = mod_tgi_sdf(ro2 + rd2 * t2);
    if (dm.x < 0.06) { return vec2f(t2, dm.y); }
    t2 = t2 + max(dm.x * 0.9, 0.05);
    if (t2 > tmax) { break; }
  }
  return vec2f(-1.0, -1.0);
}
fn mod_tgi_nrm(pw: vec3f) -> vec3f {
  let e = 0.025;
  let c = mod_tgi_sdf(pw).x;
  return normalize(vec3f(
    mod_tgi_sdf(pw + vec3f(e, 0.0, 0.0)).x - c,
    mod_tgi_sdf(pw + vec3f(0.0, e, 0.0)).x - c,
    mod_tgi_sdf(pw + vec3f(0.0, 0.0, e)).x - c));
}
fn mod_tgo_nrm(p: vec3f, eps: f32, st: f32) -> vec3f {
  let hy = mod_tgo_map5(p, st);
  let hx = mod_tgo_map5(p + vec3f(eps, 0.0, 0.0), st);
  let hz = mod_tgo_map5(p + vec3f(0.0, 0.0, eps), st);
  return normalize(vec3f(hx - hy, eps, hz - hy));
}
fn mod_tgo_spec(n: vec3f, l: vec3f, e: vec3f, s: f32) -> f32 {
  let nrm = (s + 8.0) / (3.14159 * 8.0);
  return pow(max(dot(reflect(e, n), l), 0.0), s) * nrm;
}
fn mod_tgo_seacol(p: vec3f, n: vec3f, sd: vec3f, md: vec3f, eye: vec3f, dist: vec3f, t: f32, st: f32, vault: f32) -> vec3f {
  let el = sd.y;
  let day = smoothstep(-0.10, 0.35, el);
  let night = smoothstep(0.05, -0.18, el);
  var fres = clamp(1.0 - dot(n, -eye), 0.0, 1.0);
  fres = pow(fres, 3.0) * 0.5;
  let reflected = mod_tgo_sky(reflect(eye, n), sd, md, t, vault);
  let base = mix(vec3f(0.004, 0.008, 0.016), vec3f(0.030, 0.050, 0.085), day);
  let waterCol = vec3f(0.16, 0.21, 0.20) * (0.25 + 0.75 * day);
  let refracted = base + pow(dot(n, sd) * 0.4 + 0.6, 80.0) * waterCol * 0.12 * day;
  var refl2 = reflected;
  // ── the island stands in the mirror: march the reflected ray. The dome and
  //    beacon burn in the waves at night; the dark mass breaks the sky by day.
  let rdir = reflect(eye, n);
  if (p.x > 0.5 && p.z < 15.0 && rdir.z > 0.04) {
    let hit = mod_tgi_march(p + vec3f(0.0, 0.03, 0.0), rdir, 17.0);
    if (hit.x > 0.0) {
      let hp2 = p + rdir * hit.x;
      let mi2 = i32(hit.y + 0.5);
      var rc = vec3f(0.016, 0.014, 0.018);
      if (mi2 == 3) { rc = vec3f(0.08, 0.42, 0.46) * (0.6 + night * 1.4); }
      else if (mi2 == 7) { rc = vec3f(0.30, 0.90, 0.95) * (1.3 + night * 2.0); }
      else {
        rc = rc * (0.8 + 0.4 * fbm(hp2.xy * 2.0, 2));
        rc += mod_tgo_suncol(el) * clamp(hp2.y * 0.25, 0.0, 0.8) * (1.0 - night) * 0.35;   // dusk-lit faces
        // the tower's lit windows smear in the swell
        let wy = smoothstep(0.25, 0.10, abs(hp2.y - 2.95)) + smoothstep(0.22, 0.09, abs(hp2.y - 3.95));
        rc += vec3f(1.3, 0.85, 0.35) * wy * smoothstep(0.9, 0.2, abs(hp2.x - 7.1)) * (0.35 + night * 0.5);
      }
      refl2 = mix(refl2, rc, 0.85);
    }
  }
  var col = mix(refracted, refl2, fres);
  // the island's long dusk shadow lies across the water
  if (p.x > 0.8 && p.z < 20.0 && sd.y > 0.01) {
    let sh2 = mod_tgi_shadow(vec3f(p.x, 0.25, p.z), sd);
    col *= 0.50 + 0.50 * sh2;
  }
  let atten = max(1.0 - dot(dist, dist) * 0.001, 0.0);
  col = col + waterCol * (p.y - 0.6) * 0.18 * atten;
  // sun glitter at dusk; at finale-night the vault-lamp takes the water
  col = col + mod_tgo_suncol(el) * vec3f(1.7, 1.05, 0.55) * mod_tgo_spec(n, sd, eye, 150.0) * smoothstep(-0.08, 0.05, el);
  col = col + vec3f(0.55, 1.05, 1.25) * mod_tgo_spec(n, md, eye, 140.0) * night * smoothstep(0.0, 0.15, md.y) * 1.7;
  // crest foam
  let foamN = vnoise(p.xz * 2.2 + vec2f(t * 0.5, -t * 0.35));
  let crest = smoothstep(1.00, 1.50, p.y) * smoothstep(0.42, 0.85, foamN);
  col = mix(col, vec3f(0.90, 0.82, 0.72) * (0.25 + 0.75 * day), crest * atten * 0.4);
  // ── the rock's waterline: foam that follows the actual SDF, lapping with the swell ──
  if (p.x > 2.2) {
    let islD = mod_tgi_sdf(vec3f(p.x, p.y * 0.4, p.z)).x;
    let lap = sin(st * 1.5 - islD * 5.5) * 0.5 + 0.5;
    var lapFoam = smoothstep(0.85, 0.06, islD) * (0.30 + 0.70 * lap);
    lapFoam *= 0.40 + 0.60 * vnoise(p.xz * 5.5 + vec2f(st * 0.9, -st * 0.6));
    // fine bubble speckle right at the contact
    let bub = step(0.965, hash21(floor(p.xz * 14.0 + vec2f(st * 1.2, 0.0))));
    lapFoam += smoothstep(0.30, 0.03, islD) * bub * 0.6;
    col = mix(col, vec3f(0.90, 0.89, 0.85) * (0.35 + 0.65 * day + 0.25 * night), clamp(lapFoam, 0.0, 1.0) * 0.85);
    // wet reflection band hugging the rock
    col += vec3f(0.25, 0.18, 0.10) * smoothstep(1.6, 0.2, islD) * (0.4 + 0.6 * day) * 0.25;
  }
  // ── buoy wakes: bob rings + worked collar, in the sea itself ──
  for (var bk = 0; bk < 4; bk++) {
    var bwx2 = -4.4;
    if (bk == 1) { bwx2 = -2.45; } else if (bk == 2) { bwx2 = -0.6; } else if (bk == 3) { bwx2 = 1.45; }
    let bd = length(p.xz - vec2f(bwx2, 11.0));
    if (bd > 3.2) { continue; }
    // expanding bob rings, born of the buoy's rhythm
    let ring = sin(bd * 7.5 - st * 2.6 - f32(bk) * 1.9);
    let rings = smoothstep(0.62, 0.97, ring) * exp(-bd * 1.35) * smoothstep(0.12, 0.35, bd) * 0.55;
    // churned collar at the hull
    let collar = smoothstep(0.34, 0.10, bd) * (0.45 + 0.55 * vnoise(p.xz * 9.0 + vec2f(st * 1.5, f32(bk) * 7.0)));
    let bfoam = clamp(rings * 0.7 + collar, 0.0, 1.0);
    col = mix(col, vec3f(0.88, 0.88, 0.86) * (0.35 + 0.65 * day + 0.22 * night), bfoam * 0.75);
    // the hull's shadow in its own water
    col *= 1.0 - smoothstep(0.30, 0.08, bd) * 0.14;
  }
  // ── THE FIRE SEA v2 (researched — Erta 'Ale lava-lake structure): a MOSAIC of
  //    dark crust plates drifting slowly, split by narrow INCANDESCENT SEAMS;
  //    the swell's folds crack the crust wider; sparse plates founder and
  //    fountain. Big fires flicker slow (~1.4Hz), per-plate phase. uni(82). ──
  let fireB = clamp(uni(82), 0.0, 1.0);
  if (fireB > 0.003) {
    let lp = p.xz * vec2f(0.55, 0.62) + vec2f(t * 0.035, t * 0.02);
    let vor = voronoi(lp);
    let seamW = vor.y - vor.x;
    let plateTone = 0.75 + 0.5 * vnoise(lp * 1.7 + vec2f(9.0));
    var mc2 = vec3f(0.055, 0.022, 0.012) * plateTone;
    mc2 *= 0.85 + 0.3 * fbm(p.xz * 3.0, 2);
    let crest2 = smoothstep(0.9, 1.5, p.y);
    let openW = 0.16 + crest2 * 0.30;
    let seam = smoothstep(openW, 0.015, seamW);
    let flick = 0.62 + 0.38 * sin(t * 8.5 + vor.x * 40.0);
    let heart = smoothstep(0.06, 0.0, seamW);
    mc2 += vec3f(1.3, 0.16, 0.02) * seam * 0.9;
    mc2 += vec3f(2.9, 0.85, 0.08) * seam * seam * flick;
    mc2 += vec3f(4.2, 2.3, 0.55) * heart * flick;
    let cell2 = floor(lp * 2.0) + vec2f(floor(t * 0.45));
    let burst = step(0.955, hash21(cell2)) * (0.5 + 0.5 * sin(t * 11.0 + hash21(cell2 + vec2f(3.0)) * 30.0));
    mc2 += vec3f(3.0, 1.1, 0.15) * burst * seam * 1.6;
    mc2 += vec3f(1.1, 0.42, 0.10) * fres * 1.2;
    mc2 += vec3f(2.0, 0.85, 0.22) * mod_tgo_spec(n, sd, eye, 60.0) * 0.5;
    col = mix(col, mc2, fireB);
  }
  return col;
}

// ═══ ancient machine-walls: height-field relief, engraved and fossiled ═══
// a toothed ring pressed into stone — the gear fossils of the tide-race
fn mod_tg_gearh(q: vec2f, r: f32, teeth: f32) -> f32 {
  let a = atan2(q.y, q.x);
  let tooth = smoothstep(0.30, 0.62, abs(fract(a * teeth * 0.15915) - 0.5) * 2.0);
  let rr2 = r * (1.0 + tooth * 0.09);
  let L = length(q);
  let ring = smoothstep(0.022, 0.007, abs(L - rr2));
  let spoke = smoothstep(0.030, 0.010, abs(fract(a * 0.9549 + 0.5) - 0.5) * max(L, 0.05)) * step(L, rr2 * 0.94) * step(rr2 * 0.30, L);
  let hub = smoothstep(0.045, 0.018, abs(L - rr2 * 0.22));
  return max(max(ring, spoke * 0.7), hub * 0.8);
}
// wall height by STYLE — every chamber speaks its own carved language:
//   0 GEARWORKS (the gate): fossil gear trains + conduit grooves
//   1 TIDE-SCRIPTORIUM (the record): engraved wave-charts + tally rows + strata
//   2 FLUTED WATERCOURSE (the hall): organ-rib fluting + band clamps + drip stains
fn mod_tg_wallh(p: vec2f, seed: f32, style: i32) -> f32 {
  var h = fbm(p * 3.0 + vec2f(seed), 3) * 0.35;
  h += fbm(p * 11.0 + vec2f(seed * 2.0), 2) * 0.15;
  if (style == 0) {
    h -= smoothstep(0.42, 0.5, abs(fract(p.y * 2.6 + fbm(p * 1.5, 2) * 0.25) - 0.5)) * 0.18;
    let cell = floor(p * 1.15 + vec2f(seed));
    let ch = hash21(cell);
    if (ch > 0.48) {
      let cq = (fract(p * 1.15 + vec2f(seed)) - 0.5) + (hash22(cell) - 0.5) * 0.26;
      h += mod_tg_gearh(cq, 0.15 + ch * 0.16, floor(6.0 + ch * 9.0)) * 0.5;
    }
    let colId = floor(p.x * 0.9 + seed * 0.7);
    let gx = abs(fract(p.x * 0.9 + seed * 0.7) - 0.5);
    h -= smoothstep(0.055, 0.030, gx) * 0.22 * step(0.55, hash11(colId));
  } else if (style == 1) {
    // sediment strata, close-set
    h -= smoothstep(0.40, 0.5, abs(fract(p.y * 4.2 + fbm(p * 1.2, 2) * 0.15) - 0.5)) * 0.14;
    // engraved wave-chart lines: rows of carved sines, each row its own sea
    let row = floor(p.y * 1.4 + seed);
    let rh = hash11(row);
    let wave = sin(p.x * (5.0 + rh * 7.0) + rh * 20.0) * (0.05 + rh * 0.05);
    let chart = smoothstep(0.030, 0.010, abs(fract(p.y * 1.4 + seed) - 0.5 - wave));
    h += chart * 0.42 * step(0.30, rh);
    // tally strokes beneath some charts — the counted tides
    let tq = vec2f(fract(p.x * 6.5 + rh * 5.0), fract(p.y * 1.4 + seed) - 0.72);
    let tally = smoothstep(0.10, 0.04, abs(tq.x - 0.5)) * smoothstep(0.10, 0.05, abs(tq.y));
    h -= tally * 0.30 * step(0.62, rh);
  } else {
    // vertical organ-rib fluting
    h += smoothstep(0.5, 0.0, abs(fract(p.x * 3.4 + seed) - 0.5)) * 0.34;
    // iron band clamps every few courses
    h += smoothstep(0.045, 0.015, abs(fract(p.y * 1.1 + seed * 0.3) - 0.5)) * 0.30;
    // drip stains running down from the bands
    let dcol = floor(p.x * 5.0 + seed);
    let dh2 = hash11(dcol);
    h -= smoothstep(0.06, 0.02, abs(fract(p.x * 5.0 + seed) - 0.5)) * 0.12 * step(0.6, dh2) * fract(p.y * 0.7 + dh2);
  }
  return h;
}
// relief: (height, directional slope toward the key light, ridge01)
fn mod_tg_relief(p: vec2f, seed: f32, lite: vec2f, style: i32) -> vec3f {
  let h0 = mod_tg_wallh(p, seed, style);
  let h1 = mod_tg_wallh(p + lite * 0.012, seed, style);
  return vec3f(h0, (h0 - h1) * 9.0, smoothstep(0.12, 0.55, h0));
}
// the full ancient-wall material: stone base, relief light, verdigris in the
// recesses, gold dust on the ridges — one call per wall pixel
fn mod_tg_ancient(p: vec2f, seed: f32, lite: vec2f, warm: f32, style: i32) -> vec3f {
  let rel = mod_tg_relief(p, seed, lite, style);
  var c = mix(vec3f(0.028, 0.025, 0.028), vec3f(0.080, 0.070, 0.062), rel.z);
  c *= clamp(1.0 + rel.y * 0.6, 0.35, 1.8);
  c = mix(c, vec3f(0.050, 0.082, 0.066), (1.0 - rel.z) * 0.40 * smoothstep(0.45, 0.15, rel.x));   // verdigris pools
  c += vec3f(0.55, 0.42, 0.16) * smoothstep(0.60, 0.80, rel.x) * 0.20 * warm;                     // gilt ridges
  c += vec3f(0.85, 0.55, 0.25) * max(rel.y, 0.0) * 0.10 * warm;                                   // warm catch-light
  return c;
}
// ── dusk→night sky, shared by shore + lens ──
fn mod_tg_sky(p: vec2f, t: f32, night: f32) -> vec3f {
  let h = clamp(-p.y * 0.5 + 0.5, 0.0, 1.0);          // 0 horizon → 1 zenith
  let duskLo = vec3f(1.05, 0.52, 0.24);
  let duskHi = vec3f(0.10, 0.14, 0.32);
  let nightLo = vec3f(0.05, 0.08, 0.17);
  let nightHi = vec3f(0.010, 0.016, 0.045);
  var c = mix(mix(duskLo, duskHi, pow(h, 0.65)), mix(nightLo, nightHi, pow(h, 0.8)), night);
  // banded clouds, advected slowly
  let cl = fbm(vec2f(p.x * 1.6 - t * 0.008, p.y * 4.2), 3);
  let band = smoothstep(0.45, 0.75, cl) * smoothstep(0.9, 0.2, h) * (1.0 - night * 0.75);
  c = mix(c, vec3f(0.55, 0.26, 0.24) * (1.4 - h), band * 0.55);
  // stars grow with night
  let sc = floor(p * 190.0);
  let sh = hash21(sc);
  let tw = 0.6 + 0.4 * sin(t * (1.5 + sh * 3.0) + sh * 40.0);
  c += vec3f(0.9, 0.95, 1.1) * step(0.9955, sh) * tw * (0.25 + night) * smoothstep(0.12, 0.5, h);
  return c;
}
// ── the sea band, shared by shore views ──
fn mod_tg_sea(p: vec2f, t: f32, night: f32) -> vec3f {
  let depth = clamp((p.y - 0.17) / 0.83, 0.0, 1.0);   // 0 horizon → 1 near
  var c = mix(mix(vec3f(0.35, 0.20, 0.20), vec3f(0.05, 0.09, 0.14), night),
              mix(vec3f(0.05, 0.06, 0.10), vec3f(0.010, 0.022, 0.040), night), depth);
  // swell lines compress toward the horizon
  let sw = sin(p.x * 14.0 + t * 0.5 + sin(depth * 30.0) * 2.0) * sin(depth * 46.0 - t * 0.9);
  c *= 1.0 + sw * 0.05 * (0.3 + depth);
  // glitter path under the light (sun at dusk, risen vault at night)
  let gx = p.x - mix(0.22, -0.15, night);   // under the dusk sun; under the risen vault at night
  let cell = floor(vec2f(p.x * 90.0, depth * 160.0 - t * 2.2));
  let gh = hash21(cell);
  let path = exp(-gx * gx * mix(22.0, 26.0, night)) * smoothstep(0.85, 0.25, depth);
  let glit = step(0.976, gh) * path * max(0.3 + 0.7 * sin(t * 3.0 + gh * 50.0), 0.0);
  c += mix(vec3f(1.3, 0.65, 0.25), vec3f(0.45, 0.85, 1.15), night) * glit * 0.7;
  return c;
}
// ── carved rock body ──
fn mod_tg_rock(p: vec2f, tint: vec3f) -> vec3f {
  var c = tint * (0.75 + 0.5 * fbm(p * 5.0, 3));
  c *= 0.80 + 0.35 * fbm(p * 17.0 + vec2f(4.0), 2);
  // damp streaks
  c *= 1.0 - 0.22 * smoothstep(0.4, 0.9, fbm(vec2f(p.x * 9.0, p.y * 1.8), 2));
  return c;
}
// ── nav chevron: dir 0 right, 1 left, 2 up, 3 down; hover glows ──
fn mod_tg_chev(px: vec2f, at: vec2f, dir: i32, hover: f32, t: f32) -> vec4f {
  var q = (px - at) / 26.0;
  if (dir == 0) { q = vec2f(-q.x, q.y); }
  if (dir == 2) { q = vec2f(q.y, q.x); }
  if (dir == 3) { q = vec2f(-q.y, q.x); }
  // two nested arrows pointing -x after remap
  let a1 = abs(abs(q.y) * 0.8 - q.x - 0.35) - 0.11;
  let a2 = abs(abs(q.y) * 0.8 - q.x + 0.25) - 0.11;
  let lim = step(abs(q.y), 0.75);
  let m = max(smoothstep(0.06, -0.02, a1), smoothstep(0.06, -0.02, a2)) * lim;
  let pulse = 0.55 + 0.25 * sin(t * 2.2) + hover * 0.9;
  let col = mix(vec3f(0.85, 0.80, 0.62), vec3f(1.3, 1.15, 0.7), hover);
  let halo = exp(-dot(q, q) * 1.4) * (0.10 + hover * 0.30);
  return vec4f(col * (m * pulse + halo), max(m * 0.85, halo));
}
// ── small pixel-space SDF helpers, shared by the act-1 art modules (deck, engine,
//    mirror, thicket) and the shore/gate retrofits. Kept in tg_lib so every later
//    module can call them (modules concatenate in define-order). ──
fn tg_disc(px: vec2f, c: vec2f, r: f32) -> f32 { return smoothstep(r, r - 5.0, length(px - c)); }
fn tg_ring(px: vec2f, c: vec2f, ro: f32, ri: f32) -> f32 { return max(tg_disc(px, c, ro) - tg_disc(px, c, ri), 0.0); }
fn tg_seg(p: vec2f, a: vec2f, b: vec2f) -> f32 {
  let pa = p - a; let ba = b - a;
  let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
  return length(pa - ba * h);
}
fn tg_backchev(px: vec2f, c: vec2f) -> f32 {
  let a = tg_seg(px, c + vec2f(9.0, -12.0), c + vec2f(-7.0, 0.0));
  let b = tg_seg(px, c + vec2f(-7.0, 0.0), c + vec2f(9.0, 12.0));
  return smoothstep(3.0, 0.0, min(a, b));
}

// ═══ ARCANE SUN TECHNOLOGY — a caged miniature star (Galen: "gorgeous…
//     arcane floating sun technology"). el: 0 water · 1 fire. ruling opens the
//     gimbal cage wide and crowns the core; alt fades the whole machine in as
//     the sun rises. Cheap by construction: 3 ring SDFs, one 2-octave fbm,
//     fixed-count bead loops. Everything additive is built ≥ 0. ═══
fn px_to_r(px: vec2f, ctr: vec2f, R: f32) -> vec2f { return (px - ctr) / max(R, 1.0); }
fn tgsun_ring(d: vec2f, tilt: f32, spin: f32, R: f32, w: f32) -> f32 {
  // an elliptical gimbal ring: rotate by spin, squash by tilt (precession =
  // both animate). Returns a 0..1 stroke mask.
  let q = rot2(spin) * d;
  let e = vec2f(q.x, q.y / max(tilt, 0.14));
  return smoothstep(w, w * 0.35, abs(length(e) - R));
}
fn tgsun_core(r: f32, a: f32, el: f32, t: f32) -> vec3f {
  // the star itself. water: aqua-white, caustic shimmer. fire: furnace white-
  // orange, short prominence licks breathing on the rim.
  let licks = 0.5 + 0.5 * sin(a * 7.0 + t * (1.4 + el * 1.3) + fbm(vec2f(a * 2.2, t * 0.35), 2) * 4.0);
  let rim = 1.0 + licks * mix(0.06, 0.22, el);                  // fire licks reach further
  let body = smoothstep(rim, rim * 0.22, r);
  let hot = smoothstep(0.55, 0.0, r);
  var col = mix(vec3f(0.55, 0.95, 1.05), vec3f(1.25, 0.62, 0.20), el);  // element voice
  var white = mix(vec3f(0.95, 1.05, 1.15), vec3f(1.35, 1.15, 0.85), el);
  // water caustic shimmer — light dapples crawling on the core
  let dap = fbm(vec2f(r * 5.0 - t * 0.6, a * 3.0), 2) * (1.0 - el);
  col += vec3f(0.10, 0.22, 0.26) * max(dap, 0.0);
  return (col * body + white * hot * hot) * (0.55 + 0.45 * (0.5 + 0.5 * sin(t * (0.9 + el * 0.9))));
}
fn mod_tg_suntech(pxIn: vec2f, ctr: vec2f, Rbase: f32, el: f32, t: f32, ruling: f32, alt: f32, hov: f32) -> vec3f {
  let d = px_to_r(pxIn, ctr, Rbase);           // px_to_r hoisted above
  let r = length(d);
  if (r > 3.4) { return vec3f(0.0); }          // cheap early-out beyond the halo
  let a = atan2(d.y, d.x);
  var c = vec3f(0.0);
  let vis = clamp(alt, 0.0, 1.0);
  // ── refraction ripple: faint concentric lensing of the sky around the machine ──
  c += vec3f(0.06, 0.08, 0.10) * max(sin(r * 9.0 - t * 1.1), 0.0) * exp(-r * 1.4) * 0.35 * vis;
  // ── god-ray shafts: soft angular spokes, slowly wheeling ──
  let spokes = max(sin(a * 5.0 - t * 0.21), 0.0);
  c += mix(vec3f(0.16, 0.30, 0.38), vec3f(0.55, 0.26, 0.10), el)
       * spokes * spokes * exp(-max(r - 0.9, 0.0) * 2.6) * (0.35 + 0.45 * ruling) * vis;
  // ── the caged star ──
  c += tgsun_core(r, a, el, t) * vis;
  // ── gimbal rings: brass-glass, precessing. ruling opens them wide; the
  //    lesser sun folds its cage tight around the core ──
  let open = mix(0.55, 1.0, ruling);
  let brass = mix(vec3f(0.72, 0.58, 0.30), vec3f(0.85, 0.55, 0.25), el);
  let glass = vec3f(0.55, 0.75, 0.85);
  let r1 = tgsun_ring(d, 0.34 + 0.22 * sin(t * 0.23), t * 0.11, 1.55 * open, 0.10);
  let r2 = tgsun_ring(d, 0.30 + 0.24 * sin(t * 0.31 + 2.1), -t * 0.07 + 1.1, 1.95 * open, 0.085);
  let r3 = tgsun_ring(d, 0.85, t * 0.045 + 0.6, 2.35 * open, 0.06) * ruling;   // crown ring, ruling only
  c += brass * r1 * (0.55 + 0.30 * max(sin(a * 2.0 + t * 0.7), 0.0)) * vis;
  c += glass * r2 * 0.40 * vis;
  c += mix(glass, brass, 0.5) * r3 * 0.50 * vis;
  // ── bead satellites riding the outer ring ──
  for (var i = 0; i < 3; i = i + 1) {
    let ph = t * 0.33 + f32(i) * 2.094;
    let bp = vec2f(cos(ph), sin(ph) * (0.30 + 0.24 * sin(t * 0.31 + 2.1)));
    let bd = length(d - rot2(-t * 0.07 + 1.1) * bp * 1.95 * open);
    c += mix(vec3f(0.5, 0.8, 0.9), vec3f(1.0, 0.6, 0.25), el) * exp(-bd * bd * 60.0) * 0.5 * vis;
  }
  // ── element rain: water sheds falling light-beads; fire sheds rising sparks ──
  for (var i = 0; i < 3; i = i + 1) {
    let fi = f32(i);
    let cyc = glsl_mod(t * (0.16 + fi * 0.05) + fi * 0.37, 1.0);
    let dir = mix(1.0, -1.0, el);                                  // water falls, fire rises
    let sp = vec2f((fi - 1.0) * 0.55 + 0.18 * sin(t * 0.5 + fi * 4.0), dir * (0.7 + cyc * 1.6));
    let sd = length(d - sp);
    c += mix(vec3f(0.35, 0.65, 0.80), vec3f(1.0, 0.5, 0.2), el)
         * exp(-sd * sd * 90.0) * (1.0 - cyc) * 0.45 * vis;
  }
  // ── crown flare + hover invite (keeps the existing click affordance) ──
  c += mix(vec3f(0.4, 0.7, 0.9), vec3f(1.1, 0.6, 0.2), el) * exp(-r * r * 0.8) * (0.18 * ruling + 0.30 * hov) * vis;
  return max(c, vec3f(0.0));
}

module · tg_views


// px = pixel in 0..512 grid coords (y down), p = uv (-1..1, y down)

// ── THE AIRSHIP (top-left, emerges with dawn). PLACEHOLDER STUB — replaced by the
//    graphics-swarm's synthesized mod_tg_airship. Defined before mod_tg_shore so
//    the shore can call it. ──
fn mod_tg_shore(p: vec2f, px: vec2f, t: f32) -> vec3f {
  let rawNight = uni(23);
  let dawn = uni(44);                 // the orrery's gift: the night burns off, a new sun climbs
  let night = rawNight * (1.0 - dawn * 0.92);   // used for sky tint, buoy colour, glitter
  let vault = uni(26);
  let horizon = 0.17;                 // uv.y of the sea line (matches rd.y = 0)
  // ── a real camera over a real sea ──
  let pv = vec2f(p.x, -p.y);
  let st = uni(83);   // integrated sea clock from the hook — rate morphs WITHOUT phase-whip (Galen: the speed-up bug)
  let ro = vec3f(0.0, 3.2 + sin(t * 0.4) * 0.05, 0.0);
  let rd = normalize(vec3f(pv.x, pv.y * 0.72 + 0.122, 1.75));
  // sun: sinks straight down over the sea at dusk, then at dawn CLIMBS straight
  // back up the SAME azimuth — a clean vertical sunrise, no sideways drift. Only
  // the elevation animates; the bearing is fixed so it never rises "at an angle".
  let sel = mix(mix(0.055, -0.22, rawNight), 0.22, dawn);
  let saz = 1.446;                    // the sun's bearing over the sea — set and rise in place
  let sunv = normalize(vec3f(cos(saz) * cos(sel), sin(sel), sin(saz) * cos(sel)));
  // the vault-lamp moon rises with the finale, then sets again as the dawn takes the sky
  let mel = mix(-0.25, 0.50, rawNight) * (1.0 - dawn * 0.9);
  let maz = 1.917;                    // the vault-lamp rises where the vault breaches
  let mdv = normalize(vec3f(cos(maz) * cos(mel), sin(mel), sin(maz) * cos(mel)));
  var c = mod_tgo_sky(rd, sunv, mdv, t, vault);
  var seaT = 100000.0;
  var seaHit = 0.0;
  // THE TIDE (valve-driven, uni 60): at low water the whole sea SINKS — the
  // beach widens, the island stands taller. High tide (0.65) = the shipped look.
  let tdrop = vec3f(0.0, (0.65 - clamp(uni(60), 0.0, 1.0)) * 0.5, 0.0);
  // ── the ocean: bisection-marched heightfield, analytic normals ──
  if (rd.y < -0.003) {
    var tm = 0.0;
    var tx = 1000.0;
    var hx = mod_tgo_map3(ro + rd * tx + tdrop, st);
    if (hx < 0.0) {
      var hm = mod_tgo_map3(ro + tdrop, st);
      var tmid = 0.0;
      for (var i = 0; i < 8; i++) {
        tmid = mix(tm, tx, hm / (hm - hx));
        let pm = ro + rd * tmid;
        let hmid = mod_tgo_map3(pm + tdrop, st);
        if (hmid < 0.0) { tx = tmid; hx = hmid; } else { tm = tmid; hm = hmid; }
      }
      let pt = ro + rd * tmid;
      let dist = pt - ro;
      let eps = max(dot(dist, dist) * 0.0002, 0.002);
      let n = mod_tgo_nrm(pt + tdrop, eps, st);
      let seaCol = mod_tgo_seacol(pt + tdrop, n, sunv, mdv, rd, dist, t, st, vault);
      c = mix(c, seaCol, pow(1.0 - smoothstep(-0.02, 0.0, rd.y), 0.2));
      seaT = tmid;
      seaHit = 1.0;
    }
  }
  // ── THE RISEN VAULT (finale): a glass dome breaching the sea ──
  if (vault > 0.001) {
    let vy = horizon + 0.02 - vault * 0.16;         // rises out of the water
    let vp = (p - vec2f(-0.15, vy + 0.16)) / 0.20;
    let dome = length(vp * vec2f(1.0, 1.6)) - 1.0;
    let waterY = vy + 0.115;                          // where the sea cuts across the dome
    // ── SUBMERGED FOUNDATION: a widening base + caustics seen down through water ──
    // (drawn first, so the emerged shell overlaps it at the breach line)
    let subDepth = p.y - waterY;                      // >0 = below the surface
    if (subDepth > 0.0 && subDepth < 0.30) {
      let fx = abs(p.x - (-0.15));
      let w = mix(0.075, 0.20, subDepth / 0.30);      // the foundation flares as it descends
      let inside = smoothstep(w, w * 0.55, fx);
      let fade = smoothstep(0.30, 0.0, subDepth);     // murkier the deeper it sinks
      let caust = 0.55 + 0.45 * sin(p.x * 44.0 + t * 2.1) * sin(p.y * 30.0 - t * 1.6);
      var bc = vec3f(0.05, 0.17, 0.21) * (0.4 + 0.7 * caust);
      bc += vec3f(0.16, 0.46, 0.52) * exp(-fx * fx * 130.0) * (0.4 + 0.6 * fade);   // lit central shaft
      bc += vec3f(0.30, 0.72, 0.80) * pow(inside, 3.0) * fade * 0.5;                // rim of the base
      c = mix(c, bc, vault * inside * (0.55 + 0.35 * fade));
    }
    // ── the emerged glass shell — full dome, murked below the waterline ──
    if (dome < 0.0) {
      let nrm = clamp(-dome * 2.0, 0.0, 1.0);
      var vc = mix(vec3f(0.045, 0.10, 0.14), vec3f(0.16, 0.34, 0.42), nrm);
      vc += vec3f(0.55, 0.95, 1.05) * pow(1.0 - nrm, 2.5) * 1.3;               // rim light
      vc += vec3f(0.30, 0.75, 0.85) * exp(-dot(vp, vp) * 1.6) * (0.5 + 0.3 * sin(t * 0.9)); // inner lamp
      vc += vec3f(1.15, 1.0, 0.62) * mod_tg_glyph(2, vp * 2.4) * (0.8 + 0.5 * sin(t * 1.7));
      vc *= 1.0 - 0.22 * step(0.42, abs(fract(vp.y * 3.0) - 0.5));             // latitude ribs
      vc *= 1.0 - 0.16 * step(0.44, abs(fract(vp.x * 2.6 + 0.5) - 0.5));         // meridian ribs — a built vault, not a bubble
      // crown beacon: a hot point at the apex, throwing a soft upward shaft
      let crown = exp(-dot(vp - vec2f(0.0, -1.02), vp - vec2f(0.0, -1.02)) * 90.0);
      vc += vec3f(0.85, 1.05, 1.10) * crown * (1.2 + 0.5 * sin(t * 2.3));
      // water SHEETS down the emerged shell while it rises — thin bright runnels
      let emerged = smoothstep(waterY + 0.01, waterY - 0.06, p.y);
      let runnel = pow(0.5 + 0.5 * sin(vp.x * 34.0 + sin(vp.y * 7.0 + t * 0.8) * 2.0), 6.0);
      let sheetFall = fract(vp.y * 2.2 + t * 1.3);
      vc += vec3f(0.55, 0.85, 0.92) * runnel * emerged * smoothstep(1.0, 0.55, sheetFall) * (0.55 * (1.0 - vault) + 0.10);
      // below the waterline the shell dims, tints deep-teal, and its light wobbles
      let below = smoothstep(waterY - 0.004, waterY + 0.03, p.y);
      let caust2 = 0.5 + 0.5 * sin(vp.x * 8.0 + t * 2.0) * sin(vp.y * 6.0 - t * 1.5);
      vc = mix(vc, mix(vc, vec3f(0.04, 0.15, 0.19), 0.62) * (0.42 + 0.4 * caust2), below);
      c = mix(c, vc * (0.4 + vault), vault * smoothstep(0.03, -0.05, dome));
    }
    // near halo where it breaches
    let lx = p.x + 0.15;
    c += vec3f(0.30, 0.70, 0.90) * exp(-lx * lx * 22.0) * smoothstep(horizon, horizon + 0.22, p.y) * smoothstep(horizon + 0.45, horizon + 0.1, p.y) * vault * 0.32;
    // water DISPLACED by the rising dome — stronger rings, a real push
    let seaMask = smoothstep(horizon, horizon + 0.30, p.y) * smoothstep(horizon + 0.52, horizon + 0.06, p.y);
    let db = length((p - vec2f(-0.15, waterY + 0.02)) * vec2f(1.0, 2.0));
    let ring = sin(db * 13.0 - t * 3.4 - vault * 7.0) * exp(-db * 2.5);
    let disp = ring * (vault * (1.0 - vault) * 5.0 + vault * 0.5);
    c += vec3f(0.46, 0.86, 0.96) * max(disp, 0.0) * seaMask;              // lifted crest
    c -= vec3f(0.13, 0.21, 0.23) * max(-disp, 0.0) * seaMask;             // trough behind it
    // FOAM COLLAR — churning white right where the shell breaks the sea. This is
    // what makes the displacement READ against a bright sea: hard, bright, alive.
    let fxr = abs(p.x + 0.15);
    let collarW = 0.075 + vault * 0.055;
    let collar = exp(-pow((p.y - waterY) * 34.0, 2.0)) * smoothstep(collarW * 1.9, collarW * 0.4, fxr);
    let churn = 0.55 + 0.45 * sin(p.x * 120.0 + t * 6.0) * sin(p.y * 90.0 - t * 4.2);
    let boil = 0.5 + 0.5 * sin(fxr * 70.0 - t * 7.5);
    c += vec3f(0.92, 1.0, 1.0) * collar * churn * (0.55 + 0.75 * vault * (1.0 - vault) * 4.0);
    c += vec3f(0.55, 0.9, 0.95) * exp(-pow((p.y - waterY - 0.02) * 22.0, 2.0)) * smoothstep(collarW * 3.0, collarW, fxr) * boil * 0.35;
  }
  // ── RISING FLAMES (Galen): tongues lick UP off the burning sea — screen-space
  //    layer, upward-advected warped noise, sharp tips, fast flicker over slow magma ──
  let fireS = clamp(uni(82), 0.0, 1.0);
  if (fireS > 0.01 && p.y > horizon - 0.22) {
    var fq = vec2f(p.x * 3.0, p.y * 5.0 + t * 2.1);
    var fl = fbm(fq, 3);
    fl = fl + 0.55 * fbm(fq * 2.3 + vec2f(fl * 1.7, t * 1.4), 2);
    let nearness = smoothstep(horizon, 0.9, p.y);                      // near field burns taller
    let above = smoothstep(horizon - 0.20, horizon + 0.05, p.y);       // licks reach above the sea line
    let flick2 = 0.55 + 0.45 * sin(t * 9.0 + p.x * 13.0 + fl * 8.0);
    let tongue = pow(smoothstep(0.62, 1.05, fl + nearness * 0.22), 2.0) * above * flick2 * fireS;
    // blackbody tongue: red skirt → orange body → yellow-white tip
    c += vec3f(1.4, 0.18, 0.02) * tongue * 0.9;
    c += vec3f(2.6, 0.95, 0.10) * tongue * tongue * 1.1;
    c += vec3f(3.6, 2.4, 0.7) * pow(tongue, 3.0) * 0.9;
    // heat haze: the sky just above the sea shivers warm
    c += vec3f(0.45, 0.16, 0.04) * exp(-abs(p.y - horizon) * 9.0) * fireS * (0.5 + 0.3 * sin(t * 5.0 + p.x * 20.0));
  }
  // ── the island observatory: raymarched against the same sky and sun ──
  if (p.x > 0.20 && p.y < 0.90 && rd.z > 0.0) {
    var tI = 9.5;
    var hitM = -1.0;
    var hitP = vec3f(0.0);
    for (var st2 = 0; st2 < 56; st2++) {
      let pos = ro + rd * tI;
      let dm = mod_tgi_sdf(pos);
      if (dm.x < 0.02) { hitM = dm.y; hitP = pos; break; }
      tI = tI + max(dm.x * 0.85, 0.015);
      if (tI > 22.5) { break; }
    }
    if (hitM > -0.5 && (seaHit < 0.5 || tI < seaT)) {
      let n = mod_tgi_nrm(hitP);
      let q = hitP - vec3f(6.5, 0.0, 16.0);
      let sunD = clamp(dot(n, sunv), 0.0, 1.0);
      let skyA = 0.22 + 0.30 * clamp(n.y, 0.0, 1.0);              // sky ambient
      let suncol = mod_tgo_suncol(sel);
      var alb = vec3f(0.05);
      var emis = vec3f(0.0);
      let mi = i32(hitM + 0.5);
      if (mi == 0) {                                              // rock
        alb = vec3f(0.062, 0.055, 0.058) * (0.7 + 0.6 * fbm(hitP.xy * 2.2 + hitP.zz * 1.3, 3));
        alb *= 1.0 - 0.3 * smoothstep(0.4, 0.0, hitP.y);          // tide-dark base
      } else if (mi == 1) {                                       // masonry: weathered courses
        alb = vec3f(0.135, 0.120, 0.105) * (0.75 + 0.4 * fbm(vec2f(atan2(q.z, q.x - 0.6) * 4.0, hitP.y * 7.0), 2));
        alb *= 1.0 - 0.22 * smoothstep(0.35, 0.5, abs(fract(hitP.y * 3.6) - 0.5));
        alb *= 1.0 - 0.25 * smoothstep(2.4, 1.9, hitP.y);         // damp foot
      } else if (mi == 2) { alb = vec3f(0.045, 0.040, 0.042);     // ironwork
      } else if (mi == 3) {                                       // the tide-glass dome
        let fres = pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 2.0);
        alb = vec3f(0.035, 0.075, 0.085);
        emis = vec3f(0.10, 0.55, 0.60) * (0.30 + night * 0.9 + fres * 0.5);
        emis *= 1.0 - 0.35 * step(0.42, abs(fract(atan2(q.z, q.x - 0.6) * 1.273) - 0.5));  // ribs
        emis += vec3f(0.9, 0.95, 1.0) * fres * 0.25;
      } else if (mi == 4) { alb = vec3f(0.055, 0.085, 0.075) * (0.8 + 0.3 * fbm(hitP.xz * 6.0, 2));  // verdigris roof
      } else if (mi == 5) {                                       // cottage clapboard
        alb = vec3f(0.140, 0.115, 0.090) * (0.8 + 0.3 * fbm(vec2f(hitP.x * 3.0, hitP.y * 14.0), 2));
        alb *= 1.0 - 0.18 * smoothstep(0.35, 0.5, abs(fract(hitP.y * 7.0) - 0.5));
      } else if (mi == 6) { alb = vec3f(0.110, 0.062, 0.048) * (0.8 + 0.35 * fbm(hitP.xy * 9.0, 2)); // brick
      } else if (mi == 7) {                                       // the beacon
        alb = vec3f(0.03);
        emis = vec3f(0.30, 0.90, 0.95) * (1.2 + night * 1.8) * (0.85 + 0.15 * sin(t * 2.1));
      }
      let selfSh = mod_tgi_shadow(hitP + n * 0.12, sunv);
      var ic = alb * (skyA * mix(vec3f(0.55, 0.45, 0.55), vec3f(0.25, 0.30, 0.50), night) * 2.0 + suncol * sunD * selfSh * (1.15 - night * 0.75));
      // dusk rim from the sunward edge
      let rim = pow(1.0 - clamp(dot(n, -rd), 0.0, 1.0), 3.0) * clamp(dot(n, sunv) + 0.4, 0.0, 1.0);
      ic += suncol * rim * (0.5 - night * 0.3);
      // lit windows: tower (two) + cottage (one), camera face
      if (mi == 1 && q.z < 0.0) {
        let wa = abs(atan2(q.z, q.x - 0.6) + 1.5708);
        let wy1 = smoothstep(0.16, 0.10, abs(hitP.y - 2.95)) * smoothstep(0.30, 0.18, wa);
        let wy2 = smoothstep(0.14, 0.09, abs(hitP.y - 3.95)) * smoothstep(0.30, 0.18, wa);
        emis += vec3f(1.5, 0.95, 0.40) * (wy1 + wy2) * (0.5 + night * 0.9);
      }
      if (mi == 5 && q.z < 0.25) {
        let wc = smoothstep(0.14, 0.08, abs(q.x + 1.05)) * smoothstep(0.16, 0.09, abs(hitP.y - 2.38));
        emis += vec3f(1.5, 0.9, 0.35) * wc * (0.5 + night * 0.9);
      }
      ic += emis;
      // aerial perspective into the dusk
      let fogF = clamp((tI - 11.0) / 24.0, 0.0, 1.0);
      ic = mix(ic, mod_tgo_sky(rd, sunv, mdv, t, vault) * 0.9, fogF * 0.35);
      c = ic;
    }
  }
  // ── a low mist bank drifts before the island, breathing with the hour ──
  if (rd.z > 0.05) {
    let mistT = 11.2 / rd.z;
    let mp = ro + rd * mistT;
    let seaBlocks = seaHit > 0.5 && seaT < mistT;
    if (!seaBlocks && mp.y > -0.3 && mp.y < 3.4 && mp.x > -6.0 && mp.x < 12.0) {
      var md = fbm(vec2f(mp.x * 0.30 - t * 0.035, mp.y * 0.75 + t * 0.008), 3);
      md = md * (0.75 + 0.25 * vnoise(vec2f(mp.x * 1.3 + t * 0.05, mp.y * 2.0)));
      let band = smoothstep(3.2, 0.5, mp.y) * smoothstep(0.40, 0.72, md);
      let mistCol = mix(vec3f(0.80, 0.50, 0.34), vec3f(0.16, 0.24, 0.34), night)
                  + mod_tgo_suncol(sel) * exp(-abs(mp.x - 4.0) * 0.25) * (1.0 - night) * 0.22;
      c = mix(c, mistCol, band * 0.34);
    }
  }
  // beacon halo + its wobbling light on the water (screen-space, projected)
  {
    let bs = vec2f(1.75 * 7.1 / 16.0, -((1.75 * 2.52 / 16.0) - 0.122) / 0.72);
    let bd2 = p - bs;
    c += vec3f(0.30, 0.85, 0.90) * exp(-dot(bd2, bd2) * 240.0) * (0.35 + night * 0.9);
    if (p.y > horizon + 0.01) {
      let sx2 = p.x - bs.x;
      let wob = 0.72 + 0.28 * sin(p.y * 52.0 + t * 1.35);
      c += vec3f(0.22, 0.60, 0.62) * exp(-sx2 * sx2 * 1600.0) * smoothstep(horizon + 0.42, horizon, p.y) * 0.20 * wob * (0.4 + night);
      c += vec3f(0.85, 0.55, 0.22) * exp(-pow(p.x - 0.71, 2.0) * 900.0) * smoothstep(horizon + 0.30, horizon, p.y) * 0.14 * wob * (1.0 - night * 0.4);
    }
  }
  // the CRYSTAL WORKS on the far left — drawn BEFORE the buoys so they pass in front
  c = tg_cavehouse(c, p, t, night, dawn);
  // ── four bell-buoys: real bodies riding the marched sea ──
  for (var k = 0; k < 4; k++) {
    let fk = f32(k);
    var bwx = -4.4;
    if (k == 1) { bwx = -2.45; } else if (k == 2) { bwx = -0.6; } else if (k == 3) { bwx = 1.45; }
    let axk = 1.75 * bwx / 11.0;
    if (abs(p.x - axk) > 0.34) { continue; }
    // anchor on the actual wave field
    let bh = mod_tgo_h2(vec2f(bwx, 11.0), st);
    let base = vec3f(bwx, bh, 11.0);
    let dirB = base - ro;
    let bDist = length(dirB);
    let occl = step(0.5, seaHit) * step(seaT, bDist - 0.45);   // a crest stands in front
    if (occl > 0.5) { continue; }
    let su = 1.75 / dirB.z;
    let sx = 1.75 * dirB.x / dirB.z;
    let sy = -((1.75 * dirB.y / dirB.z - 0.122) / 0.72);
    let lean = sin(t * 0.6 + fk * 1.9) * 0.10 + sin(t * 1.1 + fk * 3.1) * 0.03;
    let gcol = mod_tg_gcol(k);
    let flash = uni(19 + k);
    // ── the buoy as it was: slim keeper of its lamp — hull, post, and light ──
    {
      var q0 = p - vec2f(sx, sy);
      q0.x = q0.x + q0.y * lean;
      // hull: half-sunk iron, a touch of dusk rim, seated in the swell
      let hd = length((q0 - vec2f(0.0, -0.10 * su)) * vec2f(1.0, 2.2)) - 0.50 * su;
      if (hd < 0.0 && q0.y < 0.06 * su) {
        var bc = vec3f(0.034, 0.030, 0.032) * (0.85 + 0.4 * fbm(q0 * 30.0 + vec2f(fk * 7.0), 2));
        bc += mix(vec3f(0.75, 0.34, 0.12), vec3f(0.10, 0.13, 0.22), night) * smoothstep(0.02 * su, -0.06 * su, hd) * clamp(0.5 - q0.x * 6.0, 0.0, 1.0) * 0.5;
        bc += gcol * exp(-q0.y * q0.y * 300.0) * flash * 0.3;
        // wet sheen at the waterline
        bc += vec3f(0.4, 0.45, 0.5) * exp(-pow((q0.y - 0.02 * su) / (0.03 * su), 2.0)) * 0.25;
        c = mix(c, bc, smoothstep(0.004, -0.004, hd));
      }
      // post: slender, warm-rimmed toward the sun
      let pd2 = max(abs(q0.x) - 0.045 * su, max(q0.y - (-0.12 * su), -1.62 * su - q0.y));
      if (pd2 < 0.0) {
        var pc2 = vec3f(0.036, 0.032, 0.032) * (0.9 + 0.3 * fbm(vec2f(q0.x * 60.0, q0.y * 8.0), 2));
        pc2 += mix(vec3f(0.7, 0.32, 0.12), vec3f(0.12, 0.15, 0.24), night) * smoothstep(0.0, -0.03 * su, abs(q0.x) - 0.02 * su) * 0.35;
        // cross-brace collar where the lamp mounts
        pc2 += vec3f(0.30, 0.24, 0.14) * smoothstep(0.016 * su, 0.006 * su, abs(q0.y + 1.52 * su)) * 0.8;
        c = mix(c, pc2, smoothstep(0.003, -0.003, pd2));
      }
      // waterline seat shadow
      let fq2 = q0 - vec2f(0.0, 0.035 * su);
      c *= 1.0 - exp(-pow(fq2.y / (0.030 * su), 2.0)) * smoothstep(0.55 * su, 0.20 * su, abs(fq2.x)) * 0.30;
    }
    // the lantern-glyph, alive when its bell speaks (billboard over the head)
    var q = p - vec2f(sx, sy);
    q.x = q.x + q.y * lean;
    let lp2 = (q - vec2f(0.0, -1.80 * su)) / (0.44 * su);
    if (abs(lp2.x) < 1.6 && abs(lp2.y) < 1.6) {
      // a dark lamp-glass backing makes the glyph legible against bright sky
      let back = smoothstep(1.35, 0.95, length(lp2));
      c = mix(c, vec3f(0.015, 0.014, 0.018), back * 0.72);
      let g = mod_tg_glyph(k, lp2);
      c = mix(c, gcol * (1.15 + flash * 2.6), g);
      c += gcol * g * 0.4;                                 // let bloom catch it
    }
    c += gcol * exp(-dot(q - vec2f(0.0, -1.78 * su), q - vec2f(0.0, -1.78 * su)) * (170.0 / (su * su))) * (0.10 + flash * 1.9) * su * 4.0;
    // waterline seat: the hull settles INTO the sea (foam now lives in the sea shader)
    let fq2 = q - vec2f(0.0, 0.035 * su);
    c *= 1.0 - exp(-pow(fq2.y / (0.030 * su), 2.0)) * smoothstep(0.55 * su, 0.20 * su, abs(fq2.x)) * 0.30;
    // its light wobbling down the water
    if (q.y > 0.05 * su) {
      let wob4 = 0.6 + 0.4 * sin(p.y * 46.0 + t * 1.6 + fk);
      c += gcol * exp(-pow(q.x / (0.10 * su), 2.0)) * exp(-q.y * 2.2) * (0.12 + flash * 0.55) * wob4;
    }
  }
  // ── the airship drifts in at the TOP-LEFT once the orrery is solved (uni(44)=
  //    dawn) — the world answering that the work is done. (built by the swarm) ──
// SUMMONED, not dawn-born (Galen Aug 10): the ship exists once the bloom has
// called it. During the cine it slides out of the portal while its own fade
// brings it up — the portal draws after, so its rim overlaps the emerging bow.
let smn = uni(115);
if (uni(116) > 0.5 && smn > 0.98) {
  c = mod_tg_airship(c, p, t, 1.0, sel, night);
} else if (smn > 0.28) {
  // THE SHIP COMES OUT OF THE HOLE (Galen): visible only INSIDE the aperture
  // (seen against the elsewhere-sky) or on the EXIT side of the portal plane —
  // never hanging in our sky behind the ring.
  let em = smoothstep(0.30, 0.80, smn);
  let PC = vec2f(-0.24, -0.66);
  let slide = (PC - vec2f(-0.58, -0.50)) * (1.0 - em);   // portal → mooring
  let before = c;
  c = mod_tg_airship(c, p - slide, t, em, sel, night);
  let exitDir = normalize(vec2f(-0.58, -0.50) - PC);
  let inHole = smoothstep(0.015, -0.015, length(p - PC) - tg_portalR(smn));
  let outSide = smoothstep(-0.01, 0.03, dot(p - PC, exitDir));
  c = mix(before, c, max(inHole, outSide));
}
if (smn > 0.0 && smn < 0.98) { c = tg_skybolt(c, p, t, smn); c = tg_portal(c, p, t, smn); }
  return c;
}

// ═══ THE TIDE RECORD — the stele's own chamber, right of the gate disc ═══
fn mod_tg_stele(p: vec2f, px: vec2f, t: f32) -> vec3f {
  // carved alcove: candle-lit ancient wall — gear fossils and tide-strata
  let vign2 = 0.26 + 0.62 * exp(-dot(p * vec2f(0.9, 0.75), p * vec2f(0.9, 0.75)) * 0.9);
  var c = mod_tg_ancient(p * 1.7 + vec2f(31.0), 7.0, vec2f(0.6, -0.7), vign2, 1) * vign2;
  // archive pigeonholes flank the chamber: rows of scroll-filled recesses
  if (abs(p.x) > 0.70 && p.y > -0.55 && p.y < 0.60) {
    let hq = vec2f(fract(abs(p.x) * 7.0), fract((p.y + 0.55) * 5.2));
    let hole = step(0.16, hq.x) * step(hq.x, 0.86) * step(0.14, hq.y) * step(hq.y, 0.84);
    if (hole > 0.5) {
      let hid = floor(vec2f(abs(p.x) * 7.0, (p.y + 0.55) * 5.2)) + vec2f(sign(p.x) * 9.0, 0.0);
      let hh = hash21(hid);
      var hc = vec3f(0.012, 0.010, 0.010);                          // deep recess
      if (hh > 0.35) {
        // a rolled scroll catches the candlelight — round end, wax seal fleck
        let sq = (hq - vec2f(0.51, 0.49)) * vec2f(1.35, 1.8);
        let scroll = smoothstep(0.30, 0.26, length(sq));
        let paper = mix(vec3f(0.28, 0.22, 0.14), vec3f(0.42, 0.34, 0.22), hash21(hid + 4.0));
        hc = mix(hc, paper * (0.35 + 0.65 * vign2) * (0.7 + 0.6 * smoothstep(0.3, -0.3, sq.x)), scroll);
        hc += vec3f(0.55, 0.12, 0.08) * step(0.82, hh) * smoothstep(0.10, 0.05, length(sq - vec2f(0.12, 0.0))) * 0.8;
      }
      c = mix(c, hc, 0.92);
      // worn sill light under each hole
      c += vec3f(0.55, 0.34, 0.14) * smoothstep(0.14, 0.10, hq.y) * hole * vign2 * 0.15;
    }
  }
  // floor line + flags
  if (p.y > 0.66) {
    let fv = p.y - 0.66;
    let rc2 = log(1.0 + fv * 5.0) * 7.0;
    let cc2 = p.x * (2.4 / (0.30 + fv)) + hash11(floor(rc2)) * 2.0;
    var fc = vec3f(0.075, 0.066, 0.062) * (0.75 + 0.4 * hash21(vec2f(floor(cc2), floor(rc2))));
    fc *= 1.0 - 0.4 * max(smoothstep(0.38, 0.5, abs(fract(rc2) - 0.5)), smoothstep(0.42, 0.5, abs(fract(cc2) - 0.5)));
    c = fc * (0.5 + 0.6 * exp(-p.x * p.x * 2.0));
  }
  // two sconce lamps flanking the slab: iron, glass, a true flame
  for (var sN = 0; sN < 2; sN++) {
    let sxx = select(-0.62, 0.62, sN == 1);
    let sp0 = p - vec2f(sxx, -0.02);
    let fs = f32(sN);
    let flick = 0.82 + 0.13 * sin(t * (6.3 + fs * 1.4) + fs * 9.0) + 0.05 * sin(t * 17.0 + fs * 5.0);
    // the sconce: iron stem up the wall, a shallow dish, a wax candle
    let stemD = sdBox(sp0 - vec2f(0.0, 0.235), vec2f(0.011, 0.085));
    let dishD = max(length((sp0 - vec2f(0.0, 0.150)) * vec2f(1.0, 2.6)) - 0.058, -(sp0.y - 0.128));
    let iron = min(stemD, dishD);
    if (iron < 0.0) {
      var ic2 = vec3f(0.050, 0.040, 0.032) * (0.85 + 0.5 * clamp(-(sp0.y - 0.15) * 8.0 + 0.4, 0.0, 1.0));
      ic2 *= 0.8 + 0.4 * fbm(sp0 * 60.0 + vec2f(fs * 3.0), 2);                 // forged texture
      ic2 += vec3f(1.0, 0.60, 0.22) * smoothstep(0.006, 0.0, abs(dishD + 0.008)) * 0.45 * flick;  // rim catches the flame
      c = mix(c, ic2, smoothstep(0.004, -0.004, iron));
    }
    // the candle: warm wax, dripped edges, a dark wick
    let waxD = sdBox(sp0 - vec2f(0.0, 0.096), vec2f(0.021 + 0.004 * sin(sp0.y * 90.0 + fs * 5.0), 0.036));
    if (waxD < 0.0) {
      var wc2 = vec3f(0.62, 0.50, 0.34) * (0.55 + 0.45 * clamp(-(sp0.x) * 8.0 + 0.5, 0.0, 1.0));
      wc2 += vec3f(1.0, 0.65, 0.28) * exp(-pow((sp0.y - 0.062) * 30.0, 2.0)) * 0.55 * flick;   // translucent glow at the lip
      c = mix(c, wc2, smoothstep(0.003, -0.003, waxD));
    }
    let wickD = sdBox(sp0 - vec2f(0.0, 0.052), vec2f(0.0035, 0.012));
    c = mix(c, vec3f(0.05, 0.03, 0.02), smoothstep(0.003, -0.002, wickD));
    // the flame: a living tongue — fbm-torn edge, layered heat, licking tip
    {
      var fq = sp0 - vec2f(0.0, -0.008);
      // rising turbulence tears the profile; stronger toward the tip
      let rise = clamp(-fq.y * 8.0 + 0.4, 0.0, 1.6);
      let torn = fbm(vec2f(fq.x * 26.0, fq.y * 15.0 - t * 4.2 + fs * 9.0), 3) - 0.5;
      fq.x = fq.x + torn * 0.030 * rise;
      fq.x = fq.x + sin(t * 3.1 + fs * 2.2) * 0.006 * rise;             // slow sway
      // teardrop: wide at the wick, drawn to a licking point
      let neck = 1.0 + clamp(fq.y + 0.035, 0.0, 0.4) * 5.5;
      let tipFlick = 1.0 + 0.22 * sin(t * 11.0 + fs * 4.0) * smoothstep(0.0, -0.06, fq.y);
      let flameD = length(vec2f(fq.x * 2.6 * neck, (fq.y + 0.008) * 1.30 / tipFlick)) - 0.075;
      if (flameD < 0.012) {
        let heat = clamp(-flameD * 11.0, 0.0, 1.0);
        // stratified: transparent skin → orange → yellow → white heart
        var fc2 = mix(vec3f(0.95, 0.30, 0.05), vec3f(1.55, 0.95, 0.25), smoothstep(0.15, 0.55, heat));
        fc2 = mix(fc2, vec3f(2.0, 1.75, 1.25), smoothstep(0.62, 0.95, heat));
        // the blue-violet root at the wick
        fc2 = mix(fc2, vec3f(0.35, 0.25, 0.75), smoothstep(0.035, 0.075, fq.y) * smoothstep(0.9, 0.4, heat) * 0.7);
        // inner darker vein above the wick (real candles hollow there)
        fc2 *= 1.0 - 0.35 * exp(-dot(fq - vec2f(0.0, 0.030), fq - vec2f(0.0, 0.030)) * 900.0);
        let edge = smoothstep(0.010, -0.004, flameD);
        c = mix(c, fc2 * flick * 1.3, edge);
        // heat shimmer just above the tip
        c += vec3f(0.9, 0.55, 0.2) * exp(-pow((fq.y + 0.10) * 22.0, 2.0)) * exp(-fq.x * fq.x * 500.0) * 0.12 * flick;
      }
    }
    // its light: near halo, wall gradient, pool on the floor
    c += vec3f(1.0, 0.55, 0.18) * exp(-dot(sp0, sp0) * 5.0) * 0.36 * flick;
    c += vec3f(0.75, 0.42, 0.14) * exp(-dot(sp0, sp0) * 1.1) * 0.14 * flick;
    let poolD = (p - vec2f(sxx * 0.85, 0.86)) * vec2f(2.2, 8.0);
    c += vec3f(0.85, 0.48, 0.16) * exp(-dot(poolD, poolD) * 1.2) * 0.16 * flick;
  }
  // ── the slab: large, enthroned, readable ──
  let sp2 = p - vec2f(0.0, 0.10);
  let steleD = sdRoundedBox(sp2, vec2f(0.34, 0.58), 0.05);
  // plinth beneath
  c = mix(c, vec3f(0.085, 0.075, 0.068) * (0.7 + 0.3 * fbm(p * 8.0, 2)), smoothstep(0.01, -0.01, sdBox(p - vec2f(0.0, 0.78), vec2f(0.42, 0.10))));
  if (steleD < 0.0) {
    var sc2 = mod_tg_rock(p * 2.6 + vec2f(9.0), vec3f(0.135, 0.122, 0.112));
    sc2 *= 0.80 + 0.35 * smoothstep(0.0, -0.2, steleD);
    // beveled edge catches candlelight
    sc2 += vec3f(0.95, 0.60, 0.25) * smoothstep(0.030, 0.0, abs(steleD + 0.020)) * 0.35;
    // carve: columns k=0..4, ticks = answer[k]+1  (answer 3,1,4,2,0)
    for (var k = 0; k < 5; k++) {
      var ticks = 1;
      if (k == 0) { ticks = 4; } else if (k == 1) { ticks = 2; } else if (k == 2) { ticks = 5; } else if (k == 3) { ticks = 3; }
      let cx = -0.232 + f32(k) * 0.116;
      // faint column channel
      sc2 *= 1.0 - 0.10 * smoothstep(0.040, 0.020, abs(sp2.x - cx)) * step(-0.30, sp2.y) * step(sp2.y, 0.50);
      for (var j = 0; j < 5; j++) {
        if (j >= ticks) { continue; }
        let tickP = sp2 - vec2f(cx, 0.42 - f32(j) * 0.165);
        let td = sdBox(tickP, vec2f(0.036, 0.026));
        let carve = smoothstep(0.014, -0.006, td);
        let inner = smoothstep(0.0, -0.014, td);
        sc2 = mix(sc2, vec3f(0.040, 0.046, 0.048), carve * 0.9);
        sc2 += vec3f(0.22, 0.80, 0.68) * inner * (0.30 + 0.12 * max(sin(t * 0.9 + f32(k)), -0.9));
      }
    }
    // header: THE DROWNED-GATE COMBO (Galen Jul 30) — the top row IS the clue:
    // four glyphs in dialing order + the notch-down ring. No decorative key row.
    var cbh = array<i32, 4>(1, 3, 0, 2);
    for (var k = 0; k < 4; k++) {
      let gp2 = (sp2 - vec2f(-0.194 + f32(k) * 0.106, -0.470)) / 0.050;
      if (abs(gp2.x) < 1.3 && abs(gp2.y) < 1.3) {
        let g = mod_tg_glyph(cbh[k], gp2);
        sc2 = mix(sc2, vec3f(0.045, 0.05, 0.05), g * 0.7);
        sc2 += mod_tg_gcol(cbh[k]) * g * 0.55;
      }
    }
    let nph = sp2 - vec2f(0.235, -0.470);
    sc2 += vec3f(0.85, 0.66, 0.30) * smoothstep(0.006, 0.0, abs(length(nph) - 0.030)) * 0.7;
    sc2 += vec3f(0.95, 0.75, 0.35) * smoothstep(0.009, 0.0, sdSegment(nph, vec2f(0.0, 0.030), vec2f(0.0, 0.052))) * 0.9;
    // a graven wave-line beneath the header
    sc2 *= 1.0 - 0.25 * smoothstep(0.012, 0.004, abs(sp2.y + 0.40 - sin(sp2.x * 9.0) * 0.012));
    c = mix(c, sc2, smoothstep(0.006, -0.006, steleD));
  }
  // drifting dust in the candlelight
  let dcell = hash21(floor(vec2f(p.x * 130.0, p.y * 80.0 - t * 1.1)));
  c += vec3f(1.0, 0.8, 0.5) * step(0.9955, dcell) * exp(-dot(p, p) * 1.4) * 0.25;
  return c;
}

// aged bronze plate: layered patina over near-black metal, lit from above-left
fn mod_tg_plate(p: vec2f, key: f32) -> vec3f {
  var m = vec3f(0.030, 0.026, 0.022);
  let pat1 = fbm(p * 5.0 + vec2f(7.0), 3);
  let pat2 = fbm(p * 16.0 + vec2f(2.0), 2);
  m = mix(m, vec3f(0.085, 0.065, 0.038), smoothstep(0.45, 0.75, pat1) * 0.8);   // bronze bloom
  m = mix(m, vec3f(0.040, 0.055, 0.045), smoothstep(0.55, 0.85, pat2) * 0.5);   // verdigris fleck
  m *= 0.65 + 0.6 * pat2 * pat1;
  m *= 0.55 + key;                                                              // key light
  return m;
}

fn mod_tg_gate(p: vec2f, px: vec2f, t: f32) -> vec3f {
  let open = max(uni(8), uni(52));   // the gate opens for EITHER combo — it IS the portal (Galen Jul 30)
  // the cliff is a MACHINE fossil: strata, gear imprints, conduit grooves —
  // the observatory's dead works, pressed into the rock around the living door
  let vign = 0.28 + 0.50 * exp(-dot(p - vec2f(0.0, -0.1), p - vec2f(0.0, -0.1)) * 1.0);
  var c = mod_tg_ancient(p * 1.5, 3.0, vec2f(-0.7, -0.7), vign, 0) * vign;
  c += vec3f(0.30, 0.16, 0.07) * exp(-(p.x + 0.9) * (p.x + 0.9) * 1.0) * exp(-(p.y + 0.9) * (p.y + 0.9) * 1.1) * 0.4;
  // one great half-buried flywheel arcs behind the portal's upper right
  {
    let fq = (p - vec2f(0.94, -0.86)) / 0.52;
    let fg = mod_tg_gearh(fq, 0.72, 14.0);
    c = mix(c, vec3f(0.062, 0.052, 0.040) * (0.8 + vign), fg * 0.55 * smoothstep(1.6, 0.9, length(fq)));
    c += vec3f(0.45, 0.32, 0.12) * fg * 0.20 * vign;
  }
  // ── the plate frame: aged bronze slab enthroning the disc ──
  let fp = p - vec2f(0.0, 0.02);
  let fr1 = abs(sdRoundedBox(fp, vec2f(0.80, 0.80), 0.10)) - 0.030;
  let fr2 = abs(sdRoundedBox(fp, vec2f(0.90, 0.90), 0.12)) - 0.022;
  let frame = min(fr1, fr2);
  if (frame < 0.0) {
    let key = clamp(0.5 - fp.x * 0.6 - fp.y * 0.8, 0.0, 1.1);
    var kc = mod_tg_plate(p * 2.0, key);
    // engraved thin border line inside each band
    kc *= 1.0 - 0.35 * smoothstep(0.006, 0.002, abs(frame + 0.015));
    kc += vec3f(0.75, 0.52, 0.22) * smoothstep(0.008, 0.0, abs(frame + 0.006)) * key * 0.35;
    c = mix(c, kc, smoothstep(0.006, -0.006, frame));
  }
  // between the bands: dark plate with faint scattered etchings
  let between = max(sdRoundedBox(fp, vec2f(0.90, 0.90), 0.12), -sdRoundedBox(fp, vec2f(0.80, 0.80), 0.10));
  if (between < 0.0 && frame > 0.0) {
    var bc2 = mod_tg_plate(p * 2.0 + vec2f(4.0), clamp(0.4 - fp.x * 0.5 - fp.y * 0.6, 0.0, 1.0) * 0.7);
    let ecell = floor((fp + vec2f(2.0)) * 11.0);
    let eh = hash21(ecell);
    if (eh > 0.87) {
      let ep = (fract((fp + vec2f(2.0)) * 11.0) - 0.5) / 0.30;
      let gi2 = i32(eh * 37.0) % 4;
      bc2 += vec3f(0.55, 0.42, 0.18) * mod_tg_glyph(gi2, ep) * 0.16;   // ghost etchings
    }
    c = mix(c, bc2, smoothstep(0.006, -0.006, between));
  }
  // ── the threshold: worn steps and a puddle that remembers the door ──
  if (p.y > 0.86) {
    let sv = p.y - 0.86;
    var gc2 = vec3f(0.070, 0.062, 0.058) * (0.75 + 0.4 * fbm(vec2f(p.x * 7.0, p.y * 18.0), 2));
    gc2 *= 1.0 - 0.35 * smoothstep(0.35, 0.5, abs(fract(sv * 22.0) - 0.5));   // step treads
    // puddle mirrors the door's warmth
    let pd = length((p - vec2f(0.18, 0.965)) * vec2f(2.6, 9.0)) - 0.55;
    if (pd < 0.0) {
      let wob3 = 1.0 + sin(p.x * 60.0 + t * 2.0) * 0.15;
      gc2 = mix(gc2, vec3f(0.05, 0.055, 0.075) + vec3f(0.55, 0.30, 0.10) * (0.25 + open * 0.9) * wob3, smoothstep(0.0, -0.15, pd));
    }
    c = mix(c, gc2 * (0.5 + 0.5 * exp(-p.x * p.x * 1.5)), smoothstep(0.0, 0.05, sv));
  }
  // drips
  let dcol = floor(px.x / 24.0);
  let dh = hash11(dcol);
  if (dh > 0.55) {
    let dy = fract(p.y * 0.5 - t * (0.05 + dh * 0.1) + dh * 7.0);
    c += vec3f(0.35, 0.45, 0.5) * exp(-abs(fract(px.x / 24.0) - 0.5) * 14.0) * smoothstep(0.94, 1.0, dy) * 0.35;
  }
  // ── the great round door ──
  let dp = p - vec2f(0.0, 0.02);
  let R = 0.62;
  let dd = length(dp) - R;
  if (dd < 0.02) {
    // opening: an iris — the door's inner disc pulls back and darkens
    let irisR = R * (1.0 - open);          // iris retracts FULLY — the 6% nub was 'the dot in the middle' (Galen's click found it)
    let ird = length(dp) - irisR;
    let rr = length(dp);
    if (ird > 0.0) {
      // ── the revealed passage: measured from the plate (zone read Jul 19) ──
      // radial law: near-black rim, long amber falloff, small hot heart at the orb
      let rn = rr / R;
      let oc = vec2f(0.0, -0.28);                          // the ornament seat (upper-center)
      // ── THE GATE IS A PORTAL (Galen Jul 30): the opened door SHOWS the zone it
      //    leads to — combo B reveals the drowned quarter, combo A the organ hall ──
      let pp = dp / R;
      // ── legible SKETCH previews (Galen: full scenes at this scale become blobs —
      //    the flywheel read as a black-edged dot, the water as a blue semicircle) ──
      var pc = vec3f(0.005, 0.004, 0.004);
      if (uni(52) > 0.5) {
        // → the drowned quarter: dark flooded hall, three pillars, a living waterline
        pc = vec3f(0.015, 0.035, 0.045) * (1.0 - pp.y * 0.3);
        for (var pi = 0; pi < 3; pi++) {
          let cx3 = -0.5 + f32(pi) * 0.5;
          pc += vec3f(0.03, 0.06, 0.07) * smoothstep(0.10, 0.04, abs(pp.x - cx3));
        }
        let wl = 0.25 + 0.02 * sin(pp.x * 5.0 + t * 0.8);
        pc += vec3f(0.10, 0.50, 0.55) * exp(-abs(pp.y - wl) * 18.0) * 0.5;
        if (pp.y > wl) { pc = pc * vec3f(0.5, 0.8, 0.85) + vec3f(0.02, 0.08, 0.09); }
      } else {
        // → the organ hall, THE REAL ROOM through the door (Galen: the pipes must
        //   show as THEMSELVES) — the actual hall shader, zoomed onto the pipe row
        let hp2 = pp * 0.55;
        pc = mod_tg_hall(hp2, hp2 * 256.0 + 256.0, t);
        pc = pc * 1.35 + vec3f(0.030, 0.022, 0.014);   // lift the room out of shadow — the dim
        // unsolved-hall level + the flywheel's dark center read as a blot at this size
      }
      pc *= open;                                          // fades in as the iris pulls back
      pc *= smoothstep(1.0, 0.82, rn);                     // rim to true black
      // ── the ornament: chevrons and orb, the door's own enter-button ──
      if (open > 0.5) {
        let mm2 = vec2f(uni(27), uni(28));
        let hov = smoothstep(95.0, 34.0, length(mm2 - vec2f(256.0, 184.0)));
        let oq = dp - oc;
        for (var ch = 0; ch < 2; ch++) {
          let oy = select(0.056, -0.034, ch == 1);
          let vd = abs(oq.y - oy - abs(oq.x) * 0.72) - 0.031;
          let band = max(vd, abs(oq.x) - 0.165);
          if (band < 0.0) {
            // top-lit metal: bright upper face, bronze under-face, dark seam
            let face = clamp(-(oq.y - oy - abs(oq.x) * 0.72) * 11.0 + 0.55, 0.0, 1.0);
            var mc = mix(vec3f(0.34, 0.235, 0.095), vec3f(1.30, 1.06, 0.56), face);
            mc *= 1.0 - 0.5 * smoothstep(0.004, 0.0, abs(band + 0.026));   // dark inner seam
            mc += vec3f(1.3, 1.1, 0.65) * smoothstep(0.005, 0.0, abs(band + 0.006)) * 0.55;
            pc = mix(pc, mc * (0.85 + hov * 0.45), smoothstep(0.004, -0.004, band));
          }
        }
        // (orb removed — read as an artifact dot over the portal, Galen Jul 30)
        pc += vec3f(1.0, 0.78, 0.42) * exp(-dot(oq, oq) * 44.0) * (0.22 + hov * 0.45);
      }
      // shadowed lip where the iris withdrew
      pc *= smoothstep(0.0, 0.07, ird);
      c = mix(c, pc, smoothstep(0.01, -0.01, dd));
    } else {
      // deep bronze: brushed rings, patina patches, one key light
      let ang = atan2(dp.y, dp.x);
      var bc = mix(vec3f(0.225, 0.140, 0.055), vec3f(0.085, 0.110, 0.095),
                   smoothstep(0.38, 0.78, fbm(dp * 6.0 + vec2f(3.0), 3)));
      bc *= 0.72 + 0.48 * fbm(vec2f(ang * 6.0, rr * 22.0), 2);   // brushed metal
      bc *= 1.0 - 0.38 * smoothstep(0.32, 0.5, abs(fract(rr * 9.0) - 0.5)); // ring grooves
      // engraved glyph ring
      bc *= 1.0 - 0.30 * step(0.46, abs(fract(ang * 3.8197) - 0.5)) * step(abs(rr - 0.485), 0.030);
      // eight spokes
      bc *= 1.0 - 0.26 * smoothstep(0.035, 0.0, abs(fract(ang * 1.2732 + 0.5) - 0.5) * rr) * step(0.17, rr);
      // key light upper-left, ambient falls off to near-black at the rim
      bc *= 0.30 + 0.80 * clamp(0.55 - dp.x * 0.8 - dp.y * 0.8, 0.0, 1.2);
      // center medallion: the vault star, waiting
      if (rr < 0.135) {
        bc = mix(vec3f(0.15, 0.092, 0.038), vec3f(0.26, 0.165, 0.07), clamp(1.0 - rr * 9.0, 0.0, 1.0));
        bc += vec3f(0.85, 0.60, 0.28) * mod_tg_glyph(2, dp / 0.095) * 0.40;
        bc *= 0.85 + 0.3 * clamp(-dp.y * 4.0, 0.0, 1.0);
      }
      // rim bevel light
      bc += vec3f(1.0, 0.62, 0.3) * smoothstep(0.02, -0.03, abs(dd + 0.03)) * clamp(-dp.y - dp.x + 0.3, 0.0, 1.0) * 0.30;
      c = mix(c, bc, smoothstep(0.01, -0.01, dd));
    }
  }
  // carved arch ring around the door
  c += vec3f(0.55, 0.4, 0.22) * smoothstep(0.025, 0.0, abs(length(dp) - (R + 0.05))) * 0.35;
  // ── the four dials, low across the door ──
  for (var k = 0; k < 4; k++) {
    let dc = vec2f(-0.39 + f32(k) * 0.26, 0.60);
    let q = p - dc;
    let r = length(q);
    if (r < 0.118) {
      let mm = vec2f(uni(27), uni(28));
      let mh = smoothstep(60.0, 22.0, length(mm - (dc * 0.5 + 0.5) * 512.0));
      var kc: vec3f;
      if (r > 0.082) {
        // bezel: dark bronze, one worn top-arc highlight (zone read Jul 19)
        kc = mod_tg_plate(q * 8.0 + vec2f(f32(k) * 5.0), 0.35);
        let bang = atan2(q.y, q.x);
        let topArc = exp(-pow((bang + 1.5708) * 1.1, 2.0));
        kc += vec3f(0.55, 0.42, 0.20) * topArc * smoothstep(0.115, 0.095, r) * 0.7;
        kc += vec3f(0.9, 0.68, 0.30) * smoothstep(0.009, 0.0, abs(r - 0.0855)) * (0.22 + topArc * 0.3);
        kc *= 0.8 + 0.25 * fbm(q * 40.0 + vec2f(f32(k) * 9.0), 2);
        kc *= 1.0 + mh * 0.4;
      } else {
        // the well: near-black, the glyph burning quiet inside
        kc = vec3f(0.012, 0.011, 0.012) + vec3f(0.045, 0.032, 0.018) * clamp(1.0 - r * 10.0, 0.0, 1.0);
        let gi = i32(uni(4 + k) + 0.5);
        let gp = q / 0.055;
        let g = mod_tg_glyph(gi, gp);
        let gcol = mod_tg_gcol(gi);
        kc = mix(kc, gcol * (1.05 + mh * 0.4), g * 0.95);
        kc += gcol * exp(-r * r * 260.0) * (0.13 + mh * 0.12);
        kc *= 0.55 + 0.45 * smoothstep(0.082, 0.055, r);   // inner shadow at the lip
      }
      c = mix(c, kc, smoothstep(0.005, -0.005, r - 0.116));
    }
  }
  // solved: gold seam light around everything
  c += vec3f(1.2, 0.85, 0.35) * smoothstep(0.03, 0.0, abs(length(dp) - R * (1.0 - open * 0.94))) * open * (1.0 - open) * 2.2;
  return c;
}

fn mod_tg_hall(p: vec2f, px: vec2f, t: f32) -> vec3f {
  let lit = uni(31);            // 0 dim → 1 solved blaze
  let solved = uni(14);
  // stone interior — the machine-wall again, and behind the organ a COLOSSAL
  // flywheel entombed in the masonry, its teeth just breaking the surface
  var c = mod_tg_ancient(p * 1.8 + vec2f(20.0), 13.0, vec2f(0.0, -0.9), 0.5 + lit * 0.5, 2) * (0.55 + 0.30 * lit);
  {
    let fq = (p - vec2f(0.0, 0.05)) / 1.15;
    let fg = mod_tg_gearh(fq, 0.86, 18.0);
    c = mix(c, vec3f(0.052, 0.045, 0.038) * (0.9 + lit * 0.8), fg * 0.45);
    c += vec3f(0.50, 0.36, 0.14) * fg * (0.10 + lit * 0.22);
    // its axle-boss behind the center pipe
    let ax = length(p - vec2f(0.0, 0.05)) - 0.10;
    c = mix(c, vec3f(0.070, 0.055, 0.038) * (1.0 + lit), smoothstep(0.015, -0.01, ax) * 0.8);
  }
  // back-wall arcade: shadowed arches between the pipes
  for (var a = 0; a < 4; a++) {
    let ax = -0.375 + f32(a) * 0.25;
    let aq = p - vec2f(ax, 0.10);
    var ad2 = 1.0;
    if (aq.y > -0.22) { ad2 = max(abs(aq.x) - 0.072, aq.y - 0.40); }
    else { ad2 = length(vec2f(aq.x, (aq.y + 0.22) * 1.1)) - 0.072; }
    if (ad2 < 0.0) {
      var av = vec3f(0.030, 0.028, 0.034) * (0.8 + 0.4 * fbm(aq * 9.0, 2));
      av += vec3f(0.25, 0.16, 0.08) * exp(-aq.y * aq.y * 3.0) * lit * 0.4;   // borrowed light
      c = mix(c, av, smoothstep(0.008, -0.008, ad2) * 0.85);
    }
    // arch surround highlight
    c += vec3f(0.30, 0.22, 0.13) * smoothstep(0.014, 0.0, abs(ad2 - 0.012)) * 0.35;
  }
  // vaulted ceiling ribs converging on the oculus
  if (p.y < -0.45) {
    let rib = abs(fract(atan2(p.x, -p.y - 0.30) * 2.546) - 0.5);
    c *= 1.0 - 0.30 * smoothstep(0.10, 0.02, rib) * smoothstep(-0.45, -0.75, p.y);
    c += vec3f(0.55, 0.42, 0.25) * smoothstep(0.05, 0.0, rib) * smoothstep(-0.5, -0.9, p.y) * (0.10 + lit * 0.35);
  }
  // oculus aperture ring
  let oring = abs(length((p - vec2f(0.0, -0.98)) * vec2f(1.0, 2.2)) - 0.34) - 0.035;
  c = mix(c, vec3f(0.14, 0.11, 0.06) * (0.8 + lit * 1.6), smoothstep(0.010, -0.010, oring));
  // wall sconces between pipes: small ember lamps
  for (var s = 0; s < 4; s++) {
    let scx = -0.375 + f32(s) * 0.25;
    let sq = p - vec2f(scx, -0.10);
    let flick2 = 0.8 + 0.2 * sin(t * (6.0 + f32(s)) + f32(s) * 7.0);
    let lamp = length(sq * vec2f(1.3, 1.0)) - 0.020;
    if (lamp < 0.0) { c = vec3f(1.5, 0.85, 0.30) * flick2; }
    c += vec3f(0.9, 0.50, 0.16) * exp(-dot(sq, sq) * 30.0) * 0.22 * flick2;
  }
  if (p.y > 0.62) {
    c = mod_tg_rock(vec2f(p.x * 2.0, p.y * 6.0), vec3f(0.09, 0.082, 0.078)) * (1.1 - (p.y - 0.62) * 0.8);
    // wet sheen strip where the pipes drip
    c *= 1.0 + 0.25 * smoothstep(0.68, 0.63, p.y) * (0.5 + 0.5 * sin(p.x * 40.0 + t));
  }
  c *= 0.45 + 0.4 * exp(-dot(p, p) * 0.7);
  // ── oculus shaft ──
  let shaft = exp(-p.x * p.x * mix(14.0, 6.0, lit));
  c += vec3f(0.9, 0.75, 0.5) * shaft * smoothstep(1.0, -1.0, p.y) * (0.09 + lit * 0.42);
  let mcell = hash21(floor(vec2f(p.x * 160.0, p.y * 95.0 - t * 2.4)));
  c += vec3f(1.1, 0.95, 0.7) * step(0.9945, mcell) * shaft * (0.05 + lit * 0.28);
  // ── star-glyph mural, high center — ignites when the organ is solved ──
  let mg = (p - vec2f(0.0, -0.72)) / 0.11;
  if (abs(mg.x) < 1.4 && abs(mg.y) < 1.4) {
    let g = mod_tg_glyph(2, mg);
    let reveal = uni(29);
    c = mix(c, mix(vec3f(0.10, 0.09, 0.10), mod_tg_gcol(2) * (1.1 + 1.3 * sin(t * 1.4) * 0.3), reveal), g * (0.35 + reveal * 0.65));
    c += mod_tg_gcol(2) * exp(-dot(mg, mg) * 0.8) * reveal * 0.5;
  }
  // ── five organ pipes ──
  for (var k = 0; k < 5; k++) {
    let cx = -0.49 + f32(k) * 0.245;
    let q = p - vec2f(cx, 0.14);
    let bodyD = sdRoundedBox(q, vec2f(0.062, 0.46), 0.05);
    if (bodyD < 0.012) {
      // brass shell
      var kc = mix(vec3f(0.34, 0.24, 0.11), vec3f(0.15, 0.19, 0.17), fbm(q * 12.0 + vec2f(f32(k) * 3.0), 2) * 0.7);
      kc *= 0.75 + 0.5 * exp(-q.x * q.x * 160.0);            // cylinder highlight
      // glass gauge window
      let gw = sdRoundedBox(q, vec2f(0.037, 0.40), 0.03);
      if (gw < 0.0) {
        let lvl = uni(9 + k);                                 // 0..1 (level/4)
        let fillTop = 0.40 - (lvl * 0.8 + 0.12) * 0.72;       // liquid surface (y down)
        var gc = vec3f(0.016, 0.022, 0.028);
        if (q.y > fillTop) {
          let glow = 0.55 + 0.45 * sin(q.y * 40.0 - t * 2.0);
          gc = vec3f(0.05, 0.35, 0.33) * (0.8 + lit * 0.8);
          gc += vec3f(0.10, 0.65, 0.58) * glow * 0.35;
          let bub = hash21(floor(vec2f(q.x * 90.0, q.y * 60.0 - t * (1.0 + f32(k) * 0.2))));
          gc += vec3f(0.3, 0.9, 0.8) * step(0.976, bub) * 0.5;
        }
        // liquid surface line
        gc += vec3f(0.5, 1.1, 1.0) * exp(-abs(q.y - fillTop) * 140.0) * 0.8;
        // tick marks — five stations
        let tickY = fract((q.y + 0.40) / 0.144);
        gc += vec3f(0.5, 0.45, 0.3) * smoothstep(0.06, 0.0, abs(tickY - 0.5)) * 0.14;
        kc = mix(kc, gc, smoothstep(0.004, -0.004, gw));
      }
      // hover: the pipe knows the hand is near
      let mm = vec2f(uni(27), uni(28));
      let hd = abs(mm.x - (cx * 0.5 + 0.5) * 512.0);
      kc += vec3f(0.9, 0.75, 0.4) * smoothstep(0.014, 0.0, abs(bodyD + 0.01)) * smoothstep(45.0, 12.0, hd) * 0.5;
      c = mix(c, kc, smoothstep(0.006, -0.006, bodyD));
      // floor reflection
      let fy = 0.62 + (0.62 - p.y) * 0.85;
      if (p.y > 0.62 && fy > -0.34 && fy < 0.60) {
        let rq = vec2f(p.x - cx, fy - 0.14);
        let rd = sdRoundedBox(rq, vec2f(0.062, 0.46), 0.05);
        c = mix(c, vec3f(0.10, 0.16, 0.16) * (0.4 + lit * 0.5), smoothstep(0.01, -0.01, rd) * 0.30 * (1.0 - (p.y - 0.62) * 1.6));
      }
    }
  }
  // solved wash
  c += vec3f(0.9, 0.8, 0.55) * solved * exp(-dot(p - vec2f(0.0, -0.4), p - vec2f(0.0, -0.4)) * 1.2) * 0.35;
  return c;
}

fn mod_tg_lens(p: vec2f, px: vec2f, t: f32) -> vec3f {
  // open night — the observatory crown, under the same physical sky as the shore
  let lrd = normalize(vec3f(p.x, -p.y * 0.85 + 0.30, 1.45));
  let lsd = normalize(vec3f(0.06, -0.30, 0.99));            // the sunk sun — deep night
  let lmd = normalize(vec3f(-0.4, -0.5, 0.8));              // no moon before the finale
  var c = mod_tgo_sky(lrd, lsd, lmd, t, uni(17));
  // ── four constellations, each tagged with a glyph ──
  // az from vertical: -0.90, -0.35, 0.25, 0.80 · glyphs 1,3,2,0 (answer: az 0.25 = glyph 2)
  for (var k = 0; k < 4; k++) {
    var az = -0.90; var gi = 1;
    if (k == 1) { az = -0.35; gi = 3; } else if (k == 2) { az = 0.25; gi = 2; } else if (k == 3) { az = 0.80; gi = 0; }
    let center = vec2f(sin(az), -cos(az)) * 0.72 + vec2f(0.0, 0.85);
    // five stars per figure, deterministic offsets, joined by faint lines
    var prev = vec2f(0.0);
    for (var j = 0; j < 5; j++) {
      let off = (hash22(vec2f(f32(k) * 7.0 + 2.0, f32(j) * 13.0 + 1.0)) - 0.5) * 0.30;
      let spos = center + off;
      let d = length(p - spos);
      let tw = 0.7 + 0.3 * sin(t * 2.0 + f32(j * 3 + k) * 2.1);
      c += vec3f(0.95, 1.0, 1.15) * exp(-d * d * 5200.0) * (1.6 + tw);
      if (j > 0) {
        let sd2 = sdSegment(p, prev, spos);
        c += vec3f(0.35, 0.45, 0.6) * smoothstep(0.004, 0.0, sd2) * 0.30;
      }
      prev = spos;
    }
    // the tag-glyph beneath the figure
    let gp = (p - (center + vec2f(0.0, 0.20))) / 0.045;
    if (abs(gp.x) < 1.4 && abs(gp.y) < 1.4) {
      c += mod_tg_gcol(gi) * mod_tg_glyph(gi, gp) * (0.30 + uni(29) * select(0.0, 0.55, gi == 2));
    }
  }
  // ── the observatory crown: parapet arc, flanking pylons ──
  // curved parapet — a dark arc the platform stands behind
  let par = p.y - (0.64 + 0.06 * p.x * p.x);
  if (par > 0.0) {
    var pc2 = vec3f(0.028, 0.026, 0.034) * (0.8 + 0.4 * fbm(p * 7.0 + vec2f(40.0), 2));
    // crenel notches along the rim
    pc2 *= 1.0 - 0.30 * step(abs(fract(p.x * 6.0) - 0.5), 0.10) * smoothstep(0.05, 0.0, par);
    // rim catch-light from the beam
    pc2 += vec3f(0.45, 0.40, 0.55) * smoothstep(0.020, 0.0, par) * (0.25 + uni(16) * 0.5);
    // engraved star-chart arcs + a rivet course along the parapet face
    let arc = abs(length(vec2f(p.x, (p.y - 1.7) * 1.6)) - 1.30);
    pc2 *= 1.0 - 0.25 * smoothstep(0.012, 0.004, min(arc, abs(arc - 0.09)));
    let riv = length(vec2f(fract(p.x * 9.0) - 0.5, fract((par + 0.05) * 11.0) - 0.5));
    pc2 += vec3f(0.40, 0.34, 0.28) * smoothstep(0.10, 0.04, riv) * smoothstep(0.10, 0.03, par) * 0.5;
    c = mix(c, pc2, smoothstep(0.0, 0.015, par));
  }
  // two glyph-lamp pylons at the platform's edge
  for (var s = 0; s < 2; s++) {
    let sx = select(-0.80, 0.80, s == 1);
    let pq = p - vec2f(sx, 0.72);
    let pyl = sdBox(pq, vec2f(0.045, 0.26));
    if (pyl < 0.0) {
      var yc = vec3f(0.055, 0.048, 0.055) * (0.8 + 0.4 * fbm(pq * 11.0, 2));
      yc += vec3f(0.5, 0.42, 0.28) * smoothstep(0.012, 0.0, abs(pq.x + 0.035)) * 0.4;
      c = mix(c, yc, smoothstep(0.006, -0.006, pyl));
    }
    // lamp head: the star-glyph burns atop each pylon
    let lg = (pq - vec2f(0.0, -0.315)) / 0.042;
    if (abs(lg.x) < 1.3 && abs(lg.y) < 1.3) {
      c += mod_tg_gcol(2) * mod_tg_glyph(2, lg) * (0.7 + 0.3 * sin(t * 1.3 + f32(s) * 3.0));
    }
    c += mod_tg_gcol(2) * exp(-dot(pq - vec2f(0.0, -0.315), pq - vec2f(0.0, -0.315)) * 160.0) * 0.35;
  }
  // ── the great lens, bottom center ──
  let lc = vec2f(0.0, 0.84);
  let lq = p - lc;
  let ang = uni(15);
  let dirv = vec2f(sin(ang), -cos(ang));
  // beam — a cone from the lens along dir
  let along = dot(lq, dirv);
  let across = abs(dot(lq, vec2f(-dirv.y, dirv.x)));
  if (along > 0.0) {
    let width = 0.012 + along * 0.055;
    let beam = exp(-pow(across / width, 2.0)) * exp(-along * 0.55);
    let hold = uni(16);
    c += mix(vec3f(1.0, 0.82, 0.45), vec3f(0.8, 0.62, 1.3), hold) * beam * (0.5 + hold * 1.5);
  }
  // mount pillar (behind the glass)
  let mp = sdBox(p - vec2f(0.0, 1.06), vec2f(0.045, 0.20));
  c = mix(c, vec3f(0.045, 0.040, 0.045), smoothstep(0.01, -0.01, mp));
  c += vec3f(0.5, 0.35, 0.15) * smoothstep(0.012, 0.0, abs(mp)) * 0.4;
  // lens body
  let ld = length(lq) - 0.17;
  if (ld < 0.06) {
    var kc = mix(vec3f(0.30, 0.22, 0.11), vec3f(0.14, 0.18, 0.16), fbm(lq * 20.0, 2) * 0.6);
    if (ld < -0.035) {
      // the glass: sky refracted + inner glow
      kc = mod_tg_sky(vec2f(p.x * 1.4, p.y * 1.4 - 0.9), t, 1.0) * 0.7 + vec3f(0.10, 0.14, 0.20);
      kc += vec3f(0.7, 0.65, 1.0) * exp(-dot(lq, lq) * 60.0) * (0.4 + uni(16) * 1.2);
    }
    kc += vec3f(1.0, 0.9, 0.6) * smoothstep(0.012, 0.0, abs(ld + 0.035)) * 0.5;
    c = mix(c, kc, smoothstep(0.008, -0.008, ld - 0.055));
  }
  // brass yoke: two arms cradle the glass, turning with the aim
  for (var ya = 0; ya < 2; ya++) {
    let side = select(-1.0, 1.0, ya == 1);
    let yang = ang + side * 1.35;
    let ydir = vec2f(sin(yang), -cos(yang));
    let yd = sdSegment(lq, ydir * 0.16, ydir * 0.26) - 0.020;
    if (yd < 0.0) {
      var yc2 = vec3f(0.24, 0.165, 0.075) * (0.8 + 0.5 * clamp(-lq.y * 3.0, 0.0, 1.0));
      yc2 += vec3f(0.9, 0.7, 0.35) * smoothstep(0.008, 0.0, abs(yd + 0.010)) * 0.4;
      c = mix(c, yc2, smoothstep(0.005, -0.005, yd));
    }
  }
  // hold-progress ring
  let hold = uni(16);
  if (hold > 0.001) {
    let pr = polar(lq / 0.22);
    let sweep = step(pr.y / 6.28318 + 0.5, hold);
    c += vec3f(0.8, 0.7, 1.3) * smoothstep(0.05, 0.0, abs(pr.x - 1.0)) * sweep * 1.4;
  }
  return c;
}

// ── tg_views ends here ──

module · tg_views2


fn mod_tg_lattice(p: vec2f, px: vec2f, t: f32) -> vec3f {
  // the observatory interior at night — weave the constellation the island keeps.
  // whiteboard: uni(32)=bloom, uni(33..37)=the five anchor charges, uni(27),uni(28)=cursor px.
  let bloom = uni(32);
  let gold = uni(38);                                               // locked in → the constellation turns gold
  let cLine = mix(vec3f(0.30, 0.75, 0.85), vec3f(1.05, 0.72, 0.22), gold);
  let cGlow = mix(vec3f(0.30, 0.70, 0.80), vec3f(1.05, 0.74, 0.26), gold);
  let cCore = mix(vec3f(0.85, 0.95, 0.90), vec3f(1.0, 0.93, 0.62), gold);
  let neb = fbm4(p * 1.5 + vec2f(t * 0.05, -t * 0.04));
  var col = vec3f(0.015, 0.028, 0.05);
  col = col + vec3f(0.04, 0.10, 0.16) * pow(neb, 2.0);
  let s = 0.6;
  var A = array<vec2f, 5>(vec2f(0.0, 0.0), vec2f(-s, 0.0), vec2f(s, 0.0), vec2f(0.0, -s), vec2f(0.0, s));
  for (var i = 1; i < 5; i = i + 1) {
    let pa = p - A[0]; let ba = A[i] - A[0];
    let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
    let d = length(pa - ba * h);
    col = col + vec3f(0.06, 0.10, 0.15) * exp(-d * d * 70.0) * (0.6 + 0.4 * sin(t * 3.0 + f32(i)));
  }
  for (var i = 0; i < 5; i = i + 1) {
    let ci = uni(33 + i);
    for (var j = i + 1; j < 5; j = j + 1) {
      let cj = uni(33 + j);
      let sm = min(ci, cj);
      if (sm > 0.55) {
        let pa = p - A[i]; let ba = A[j] - A[i];
        let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
        let d = length(pa - ba * h);
        col = col + cLine * exp(-d * d * 90.0) * (sm - 0.5) * 2.0;
      }
    }
  }
  for (var i = 0; i < 5; i = i + 1) {
    let c = uni(33 + i);
    let d = p - A[i]; let r = dot(d, d);
    col = col + cGlow * exp(-r * 40.0) * (0.10 + c * 1.4);
    col = col + cCore * exp(-r * 300.0) * (0.25 + c * 1.1);
  }
  let cur = (vec2f(uni(27), uni(28)) - 256.0) / 256.0;
  let dc = p - cur;
  col = col + vec3f(0.80, 0.95, 1.0) * exp(-dot(dc, dc) * 420.0) * 0.9;
  let bloomCol = mix(vec3f(0.5, 0.85, 0.9), vec3f(1.0, 0.78, 0.34), gold);
  col = mix(col, col + bloomCol * 0.7 + vec3f(0.12, 0.13, 0.16) + vec3f(0.08, 0.05, 0.0) * gold, bloom * 0.7);
  return col;
}

fn mod_tg_core(p: vec2f, px: vec2f, t: f32) -> vec3f {
  // INSIDE THE ORB — a deep well with three rings of light. Each ring carries a
  // notch (uni(40..42), radians, 0 = up); the player turns them to the top gate.
  // uni(39)=solved, uni(43)=ignition glow, uni(27),uni(28)=cursor px.
  let solved = uni(39);
  let glow = uni(43);
  let rr = length(p);
  var col = vec3f(0.015, 0.02, 0.035);
  let neb = fbm4(p * 2.0 + vec2f(t * 0.03, t * 0.02));
  col = col + vec3f(0.02, 0.05, 0.09) * pow(neb, 2.0) * (1.0 - smoothstep(0.4, 1.15, rr));
  let warm = max(solved, glow);
  let ringCol = mix(vec3f(0.30, 0.72, 0.85), vec3f(1.05, 0.72, 0.22), warm);
  let notchCol = mix(vec3f(0.75, 0.95, 1.0), vec3f(1.0, 0.90, 0.55), warm);
  // the gate at 12 o'clock — where notches must land
  let ang = atan2(p.x, -p.y);
  let gate = exp(-ang * ang * 24.0) * smoothstep(0.34, 0.9, rr) * smoothstep(1.05, 0.86, rr);
  col = col + vec3f(0.95, 0.88, 0.52) * gate * 0.4;
  var radii = array<f32, 3>(0.30, 0.55, 0.80);
  for (var i = 0; i < 3; i = i + 1) {
    let R = radii[i];
    let band = exp(-(rr - R) * (rr - R) * 320.0);
    col = col + ringCol * band * 0.16;
    let a = uni(40 + i);
    let nd = vec2f(sin(a), -cos(a));
    let np = nd * R;
    let dn = length(p - np);
    let al = exp(-a * a * 20.0);                       // 1 when this notch is at the top
    let bright = 0.8 + al * 1.0;
    col = col + notchCol * exp(-dn * dn * 1500.0) * bright;
    col = col + notchCol * exp(-dn * dn * 240.0) * 0.22 * bright;
  }
  // the heart of the orb — dim until woken, then a small sun
  let heart = exp(-rr * rr * 24.0);
  col = col + mix(vec3f(0.09, 0.15, 0.21), vec3f(2.3, 1.75, 0.95), glow) * heart * (0.22 + glow * 1.7);
  col = col + vec3f(1.7, 1.25, 0.65) * exp(-rr * rr * 230.0) * glow;
  if (glow > 0.001 && glow < 0.999) {                  // ignition ring blooming outward
    let fr = glow * 1.15;
    col = col + vec3f(1.0, 0.8, 0.4) * exp(-(rr - fr) * (rr - fr) * 110.0) * (1.0 - glow) * 0.85;
  }
  let cur = (vec2f(uni(27), uni(28)) - 256.0) / 256.0;
  let dc = p - cur;
  col = col + vec3f(0.8, 0.95, 1.0) * exp(-dot(dc, dc) * 520.0) * 0.6;
  col = col * smoothstep(1.3, 0.5, rr);
  return col;
}

// ═══ THE TIDE-ORRERY — the island's keystone, reached through the woken core ═══
// A brass armillary in the deep sanctum: a sun-bead on the outer ring, a moon-bead
// on the inner, the island fixed at the hub. An engraved horizon cuts the sphere —
// above is risen sky, below is the drowned. Set the sun BELOW and the moon ABOVE
// (the finale sky) and the tide-gauge surges to full: the spring tide the island
// remembers. Holding that alignment wakes the dawn.
//   uni(45) sunA  uni(46) moonA  uni(47) tide  uni(48) solved  uni(49) glow  uni(50) hold
fn mod_tg_orrery(p: vec2f, px: vec2f, t: f32) -> vec3f {
  let sunA = uni(45);
  let moonA = uni(46);
  let tide = uni(47);
  let solved = uni(48);
  let glow = uni(49);
  let hold = uni(50);
  let warm = max(solved, glow);
  let rr = length(p);
  let mm = vec2f(uni(27), uni(28));
  // ── the sanctum void: nebula + far stars, like the core it opens from ──
  var c = vec3f(0.010, 0.016, 0.030);
  let neb = fbm4(p * 1.7 + vec2f(t * 0.02, -t * 0.015));
  c += vec3f(0.03, 0.06, 0.12) * pow(neb, 2.2) * (1.0 - smoothstep(0.45, 1.30, rr));
  let sc = floor((p * 0.5 + 0.5) * 250.0);
  let sh = hash21(sc);
  c += vec3f(0.72, 0.80, 1.0) * step(0.9955, sh) * (0.35 + 0.65 * sin(t * 2.0 + sh * 40.0)) * 0.6 * smoothstep(1.25, 0.4, rr);
  // ── the engraved horizon sea-line: sky above, drowned below ──
  c += mix(vec3f(0.10, 0.16, 0.22), vec3f(0.55, 0.44, 0.22), warm) * smoothstep(0.006, 0.0, abs(p.y)) * (0.35 + 0.25 * sin(p.x * 22.0));
  // a faint drowned wash below the line
  c += vec3f(0.010, 0.030, 0.045) * smoothstep(0.0, 0.9, p.y) * (1.0 - warm * 0.5);
  // BONES (Galen Jul 30): compass tide-glyphs DELETED — symbols with no puzzle meaning.
  // ── the two armillary rings: engraved brass circles with graticule ticks ──
  let sunR = 0.78;
  let moonR = 0.52;
  let ang = atan2(p.x, -p.y);                                 // 0 at top, +cw
  // outer sun ring
  let dS = abs(rr - sunR);
  var ringS = mix(vec3f(0.30, 0.22, 0.10), vec3f(0.95, 0.72, 0.34), warm);
  let tickS = smoothstep(0.35, 0.5, abs(fract(ang * 3.8197) - 0.5));   // 24 ticks
  c += ringS * smoothstep(0.016, 0.0, dS) * (0.45 + 0.35 * tickS + warm * 0.4);
  c += ringS * smoothstep(0.006, 0.0, dS) * 0.5;             // bright rail
  // inner moon ring
  let dM = abs(rr - moonR);
  var ringM = mix(vec3f(0.24, 0.28, 0.36), vec3f(0.72, 0.82, 0.96), warm);
  let tickM = smoothstep(0.35, 0.5, abs(fract(ang * 2.8648) - 0.5));   // 18 ticks
  c += ringM * smoothstep(0.014, 0.0, dM) * (0.45 + 0.35 * tickM + warm * 0.3);
  c += ringM * smoothstep(0.005, 0.0, dM) * 0.5;
  // ── the island at the hub: a small dark rock with its beacon, the still center ──
  {
    let iq = p / 0.14;
    let rock = length(iq * vec2f(1.0, 1.35)) - 0.7 - (vnoise(iq * 3.0) - 0.5) * 0.25;
    if (rock < 0.0) {
      var icol = vec3f(0.050, 0.046, 0.050) * (0.7 + 0.5 * fbm(iq * 3.0, 2));
      icol += vec3f(0.30, 0.90, 0.95) * exp(-dot(iq - vec2f(0.0, -0.45), iq - vec2f(0.0, -0.45)) * 30.0) * (0.4 + warm); // beacon
      c = mix(c, icol, smoothstep(0.05, -0.05, rock));
    }
    c += vec3f(0.20, 0.55, 0.60) * exp(-dot(p, p) * 26.0) * (0.10 + warm * 0.35);   // hub halo
  }
  // ── the sun-bead on the outer ring ──
  let sunPos = vec2f(sin(sunA), -cos(sunA)) * sunR;
  let ds = length(p - sunPos);
  // a hot disc with a shaded south face, a corona, and eight rays
  let sunDisc = ds - 0.058;
  if (sunDisc < 0.02) {
    let sph = clamp(-sunDisc * 24.0, 0.0, 1.0);
    var scol = mix(vec3f(0.55, 0.22, 0.06), vec3f(1.7, 1.05, 0.45), sph);
    scol *= 0.7 + 0.3 * clamp(-(p.y - sunPos.y) * 10.0 + 0.5, 0.0, 1.0);
    c = mix(c, scol, smoothstep(0.006, -0.006, sunDisc));
  }
  c += vec3f(1.5, 0.85, 0.35) * exp(-ds * ds * 130.0);                 // corona
  c += vec3f(1.1, 0.55, 0.20) * exp(-ds * ds * 16.0) * 0.35;
  let sray = abs(fract(atan2(p.y - sunPos.y, p.x - sunPos.x) * 1.2732) - 0.5);
  c += vec3f(1.3, 0.75, 0.28) * smoothstep(0.42, 0.5, sray) * exp(-ds * 7.0) * smoothstep(0.05, 0.12, ds) * 0.5;
  // ── the moon-bead on the inner ring, with a crescent terminator ──
  let moonPos = vec2f(sin(moonA), -cos(moonA)) * moonR;
  let dm = length(p - moonPos);
  let moonDisc = dm - 0.046;
  if (moonDisc < 0.014) {
    let sph = clamp(-moonDisc * 30.0, 0.0, 1.0);
    var mcol = mix(vec3f(0.12, 0.16, 0.24), vec3f(0.78, 0.86, 1.02), sph);
    let term = smoothstep(-0.01, 0.02, (p.x - moonPos.x) + (p.y - moonPos.y) * 0.4);  // lit limb
    mcol *= 0.35 + 0.65 * term;
    mcol += vec3f(0.05, 0.07, 0.10) * (1.0 - term);
    c = mix(c, mcol, smoothstep(0.005, -0.005, moonDisc));
  }
  c += vec3f(0.55, 0.68, 0.95) * exp(-dm * dm * 220.0) * 0.8;
  // ── THE TIDE ITSELF: a luminous sea flooding the drowned lower half, rising to
  //    the horizon as the alignment nears. The gauge is not a dial but the water —
  //    the sun sets INTO it, the moon lifts ABOVE it, and at the true syzygy the
  //    whole sea brims gold: the spring tide the island remembers. ──
  {
    let surf = 0.86 - tide * 0.86 + sin(p.x * 7.0 + t * 0.7) * 0.010 + sin(p.x * 17.0 - t * 1.1) * 0.005;  // a gently waving waterline
    if (p.y > surf) {
      let depth = p.y - surf;
      let caust = 0.5 + 0.5 * sin(p.x * 24.0 + t * 1.6 + sin(p.y * 9.0) * 1.5) * sin(p.y * 20.0 - t * 1.1);
      var wc = mix(vec3f(0.04, 0.20, 0.24), vec3f(0.85, 0.68, 0.30), warm);   // teal by night, gold once woken
      wc *= 0.55 + 0.45 * caust;
      wc += mix(vec3f(0.10, 0.55, 0.55), vec3f(1.0, 0.80, 0.40), tide) * exp(-depth * 3.0) * 0.6;   // lit surface band
      wc += vec3f(1.0, 0.85, 0.45) * smoothstep(0.78, 1.0, tide) * 0.5;        // the spring-tide surge
      // soft round glints drifting on the sea (not blocky — a jittered point per cell)
      let cell = vec2f(p.x * 52.0, p.y * 52.0 - t * 1.1);
      let ci = floor(cell);
      let jit = (hash22(ci) - 0.5) * 0.6;
      let glint = step(0.975, hash21(ci)) * smoothstep(0.30, 0.0, length(fract(cell) - 0.5 - jit));
      wc += mix(vec3f(0.45, 0.95, 0.9), vec3f(1.0, 0.92, 0.55), tide) * glint * (0.5 + 0.6 * tide);
      let a = smoothstep(0.0, 0.05, depth) * (0.32 + 0.55 * tide);
      c = mix(c, wc, a);
    }
    // the meniscus — a bright waterline that warms from sea-glass to gold as it fills
    c += mix(vec3f(0.4, 1.0, 1.0), vec3f(1.0, 0.85, 0.42), tide) * exp(-abs(p.y - surf) * 55.0) * (0.22 + 0.6 * tide);
  }
  // ── hold-progress arc sweeping the outer ring ──
  if (hold > 0.001) {
    let sweep = step(fract(ang * 0.15915 + 0.5), hold);
    c += mix(vec3f(0.5, 0.85, 0.9), vec3f(1.0, 0.85, 0.4), hold) * smoothstep(0.03, 0.0, abs(rr - (sunR + 0.10))) * sweep * (0.7 + hold);
  }
  // ── ignition + solved bloom ──
  if (glow > 0.001 && glow < 0.999) {
    let fr = glow * 1.2;
    c += vec3f(1.0, 0.82, 0.42) * exp(-(rr - fr) * (rr - fr) * 90.0) * (1.0 - glow) * 0.9;
  }
  c += vec3f(1.15, 0.92, 0.55) * solved * exp(-rr * rr * 1.3) * 0.55;
  // cursor firefly
  let cur = (mm - 256.0) / 256.0;
  c += vec3f(0.8, 0.95, 1.0) * exp(-dot(p - cur, p - cur) * 460.0) * 0.6;
  c *= smoothstep(1.45, 0.5, rr);
  return c;
}

// ═══ THE SEA CAVE — a bioluminescent grotto in the rock, LEFT off the shore. Wet
// stone, a bright mouth open to the sea, a tide-pool that glows teal, glinting
// motes. Cool counterpoint to the warm puzzle rooms. Reuses rock/sky/sea/glyphs.
// ══ THE SEA-CAVE, raymarched (view 8). A wet grotto open to the ocean through a
//    mouth in the back wall — the sky and the ruling moon show through it. At its
//    heart a carved plinth with a tideglass SOCKET; seat the pearl and it blazes,
//    firing a beam out the mouth to the crystal under the airship. Author: Fable 5.
const TGS_SOCK  = vec3f(0.0, 0.96, 0.85);    // socket bowl centre → projects ≈ px(256,300)
const TGS_MOUTH = vec3f(0.0, 1.85, -3.6);    // the sea-mouth's heart (the one light) → px(256,224)
const TGS_STAL = array<vec3f, 5>(            // (x, z, hang) — precomputed, map() is hot
  vec3f(-1.800, -1.000, 0.500), vec3f(-0.748, -0.067, 1.048), vec3f(0.198, 1.036, 1.046),
  vec3f(1.288, -0.495, 0.933), vec3f(2.256, -0.736, 0.516));

fn tgs_h1(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgs_hs(x: f32) -> f32 { return fract(sin(x * 91.3) * 43758.5453); }
fn tgs_n2(p: vec2f) -> f32 {
  let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
  return mix(mix(tgs_h1(i), tgs_h1(i + vec2f(1.0, 0.0)), u.x),
             mix(tgs_h1(i + vec2f(0.0, 1.0)), tgs_h1(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgs_fbm(p: vec2f, oct: i32) -> f32 {
  var v = 0.0; var a = 0.5; var q = p;
  for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgs_n2(q); q = q * 2.03 + vec2f(5.0, 9.0); a *= 0.5; }
  return v;
}
fn tgs_box(p: vec3f, b: vec3f) -> f32 { let q = abs(p) - b; return length(max(q, vec3f(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0); }
fn tgs_cyl(p: vec3f, h: f32, r: f32) -> f32 { let d = vec2f(length(p.xz) - r, abs(p.y) - h); return min(max(d.x, d.y), 0.0) + length(max(d, vec2f(0.0))); }
fn tgs_smin(a: f32, b: f32, k: f32) -> f32 { let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); }
// a warm→cool hue wheel for the ruling-moon tint (hue in ~[0,1))
fn tgs_hue(x: f32) -> vec3f {
  let h = fract(x) * 6.28318;
  return vec3f(0.6 + 0.4 * cos(h), 0.6 + 0.4 * cos(h - 2.094), 0.6 + 0.4 * cos(h - 4.188));
}

// SDF → (dist, matId). 1 rock · 2 wet floor · 3 plinth bronze · 4 pearl · 5 socket glass
fn tgs_map(p: vec3f, t: f32) -> vec2f {
  var d = 1e5; var m = 1.0;
  // ── the grotto, built from explicit walls so the back is genuinely OPEN to
  //    the sea (an inside-out box makes a closed shaft — this doesn't). ──
  let ceil = (3.6 - p.y) + 0.12 * tgs_fbm(p.xz * 1.5, 2);          // ceiling
  d = ceil; m = 1.0;
  let lw = (p.x + 2.9) + 0.14 * tgs_fbm(p.yz * 1.4, 2);            // left wall
  if (lw < d) { d = lw; m = 1.0; }
  let rw = (2.9 - p.x) + 0.14 * tgs_fbm(p.yz * 1.4 + vec2f(4.0), 2); // right wall
  if (rw < d) { d = rw; m = 1.0; }
  // the back wall — a finite slab with the MOUTH punched out; rays through the
  // hole pass the thin slab and escape to the open ocean beyond.
  let back = tgs_box(p - vec3f(0.0, 1.5, -3.5), vec3f(3.2, 2.4, 0.3)) + 0.08 * tgs_fbm(p.xy * 1.4, 2);
  let hole = tgs_box(p - vec3f(0.0, 1.85, -3.5), vec3f(1.2, 1.05, 1.2));
  let wallH = max(back, -hole);
  if (wallH < d) { d = wallH; m = 1.0; }
  // ── the wet floor, silt-mounded — STOPS before the mouth so it never blocks
  //    the sea view (no infinite plane across the opening) ──
  if (p.z > -2.2) {
    let floorY = p.y + 0.28 + 0.06 * tgs_fbm(p.xz * 2.4, 2) - 0.06 * smoothstep(2.0, 0.0, length(p.xz - vec2f(0.4, 1.6)));
    if (floorY < d) { d = floorY; m = 2.0; }
  }
  // ── stalactites down from the ceiling (positions precomputed — perf) ──
  for (var k = 0; k < 5; k++) {
    let st = TGS_STAL[k];
    let sx = st.x; let sz = st.y; let hh = st.z;
    let tq = p - vec3f(sx, 3.5, sz);
    let taper = clamp(-tq.y / hh, 0.0, 1.0);
    let stal = length(tq.xz) - mix(0.12, 0.01, taper) + max(0.0, tq.y) + max(0.0, -tq.y - hh);
    if (stal < d) { d = stal; m = 1.0; }
  }
  // ── THE PLINTH: a carved bronze pedestal rising from the floor to the socket ──
  let plc = TGS_SOCK - vec3f(0.0, 0.62, 0.0);
  var plinth = tgs_cyl(p - plc, 0.60, 0.30);
  plinth = tgs_smin(plinth, tgs_cyl(p - (TGS_SOCK - vec3f(0.0, 0.12, 0.0)), 0.05, 0.34), 0.06);   // rim under the bowl
  plinth = min(plinth, tgs_box(p - vec3f(0.0, -0.16, 0.85), vec3f(0.40, 0.12, 0.40)));            // base on the floor
  if (plinth < d) { d = plinth; m = 3.0; }
  // the SOCKET bowl on top: a hemispherical hollow (glass-lined)
  let bowl = max(length(p - (TGS_SOCK + vec3f(0.0, 0.10, 0.0))) - 0.20, -(p.y - (TGS_SOCK.y + 0.10)));
  if (bowl < d) { d = bowl; m = 5.0; }
  // ── the PEARL seated in the bowl (present once the socket is powered) ──
  if (uni(109) > 0.5) {
    let pd = length(p - (TGS_SOCK + vec3f(0.0, 0.10, 0.0))) - 0.13;
    if (pd < d) { d = pd; m = 4.0; }
  }
  return vec2f(d, m);
}
fn tgs_nrm(p: vec3f, t: f32) -> vec3f {
  let e = vec2f(0.006, 0.0);
  return normalize(vec3f(
    tgs_map(p + e.xyy, t).x - tgs_map(p - e.xyy, t).x,
    tgs_map(p + e.yxy, t).x - tgs_map(p - e.yxy, t).x,
    tgs_map(p + e.yyx, t).x - tgs_map(p - e.yyx, t).x));
}
fn tgs_march(ro: vec3f, rd: vec3f, t: f32) -> vec2f {
  var tt = 0.02;
  for (var i = 0; i < 64; i++) {
    let h = tgs_map(ro + rd * tt, t);
    if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
    tt += h.x * 0.8; if (tt > 24.0) { break; }
  }
  return vec2f(-1.0, 0.0);
}
fn tgs_shadow(ro: vec3f, rd: vec3f, t: f32) -> f32 {
  var res = 1.0; var tt = 0.04;
  for (var i = 0; i < 12; i++) {
    let h = tgs_map(ro + rd * tt, t).x;
    if (h < 0.002) { return 0.15; }
    res = min(res, 12.0 * h / tt); tt += clamp(h, 0.06, 0.5); if (tt > 6.0) { break; }
  }
  return clamp(res, 0.15, 1.0);
}
fn tgs_ao(p: vec3f, n: vec3f, t: f32) -> f32 {
  var occ = 0.0; var w = 1.0;
  for (var i = 1; i <= 3; i++) { let hh = 0.07 * f32(i); occ += w * (hh - tgs_map(p + n * hh, t).x); w *= 0.7; }
  return clamp(1.0 - 1.9 * occ, 0.0, 1.0);
}
fn tgs_alb(m: f32, p: vec3f) -> vec3f {
  if (m < 1.5) {          // wet rock — cool, mineral-veined
    var a = vec3f(0.055, 0.072, 0.082) * (0.7 + 0.6 * tgs_fbm(p.xy * 3.0 + p.zz, 3));
    a = mix(a, vec3f(0.04, 0.09, 0.10), smoothstep(0.4, 0.8, tgs_fbm(p.xz * 5.0, 2)) * 0.5);
    return a;
  } else if (m < 2.5) {   // wet silt floor
    return vec3f(0.045, 0.06, 0.065) * (0.7 + 0.7 * tgs_fbm(p.xz * 4.0, 3));
  } else if (m < 3.5) {   // plinth bronze, sea-worn
    var a = vec3f(0.16, 0.12, 0.06);
    a = mix(a, vec3f(0.06, 0.13, 0.11), smoothstep(0.4, 0.75, tgs_fbm(p.xy * 6.0 + p.zz * 3.0, 3)));
    return a;
  } else if (m < 4.5) {   // pearl (lit specially)
    return vec3f(0.75, 0.82, 0.85);
  }
  return vec3f(0.10, 0.14, 0.16);   // socket glass rim
}
// the view through the mouth: sky graded to sea, sun/moon by world state
fn tgs_ocean(rd: vec3f, t: f32, night: f32, dawn: f32, moonHue: f32, spill: vec3f) -> vec3f {
  let up = clamp(rd.y * 0.5 + 0.5, 0.0, 1.0);
  // sky: night indigo → day/dawn
  let daySky = mix(vec3f(0.45, 0.62, 0.85), vec3f(1.0, 0.85, 0.6), dawn);
  let nightSky = mix(vec3f(0.04, 0.07, 0.14), vec3f(0.10, 0.14, 0.26), up);
  var col = mix(daySky, nightSky, night * (1.0 - dawn));
  // horizon band
  col = mix(mix(vec3f(0.10, 0.20, 0.30), vec3f(0.9, 0.7, 0.5), dawn), col, smoothstep(-0.02, 0.25, rd.y));
  // the RULING MOON — a disc in the mouth's sky, tinted to its element
  let mdir = normalize(vec3f(-0.55, 0.55, -1.0));
  let md = distance(normalize(rd), mdir);
  let moonDisc = smoothstep(0.11, 0.09, md);
  let moonHalo = exp(-md * md * 20.0);
  let moonTint = mix(vec3f(0.9, 0.93, 1.0), tgs_hue(moonHue), 0.7);
  col = mix(col, moonTint, moonDisc * (0.5 + 0.5 * night));
  col += moonTint * moonHalo * (0.25 + 0.35 * night);
  // the sea below the horizon, catching the moon
  if (rd.y < -0.01) {
    let w = tgs_fbm(vec2f(rd.x * 6.0, rd.z * 6.0 - t * 0.3), 3);
    var sea = mix(vec3f(0.03, 0.10, 0.15), vec3f(0.08, 0.20, 0.26), w);
    sea += moonTint * smoothstep(0.15, 0.0, abs(rd.x + 0.4)) * (0.2 + 0.3 * night) * (0.5 + 0.5 * w);   // moonpath
    col = mix(col, sea, smoothstep(-0.01, -0.06, rd.y));
  }
  col += spill * 0.12;   // the daylight that fills the cave leaks from here
  return col;
}
// nearest distance from a ray to a segment [a,b] (for the volumetric beam)
fn tgs_segDist(ro: vec3f, rd: vec3f, a: vec3f, b: vec3f) -> f32 {
  let ab = b - a; let ao = ro - a;
  let rdd = dot(rd, rd); let abd = dot(ab, ab); let abrd = dot(ab, rd);
  let aord = dot(ao, rd); let aoab = dot(ao, ab);
  let den = rdd * abd - abrd * abrd;
  var s = 0.0; var u = 0.0;
  if (abs(den) > 1e-5) { s = (abrd * aoab - abd * aord) / den; }
  u = (abrd * s + aoab) / abd;
  s = max(s, 0.0); u = clamp(u, 0.0, 1.0);
  let pr = ro + rd * s; let pl = a + ab * u;
  return length(pr - pl);
}

fn mod_tg_seacave(p: vec2f, px: vec2f, t: f32) -> vec3f {
  let mm = vec2f(uni(27), uni(28));
  let pearlSet = uni(109);          // the pearl is seated → socket glows, beam fires
  let night = clamp(uni(23), 0.0, 1.0);
  let dawn = clamp(uni(44), 0.0, 1.0);
  let moonHue = uni(111);           // ruling moon → the ocean view matches the sky
  // ── camera: standing on the wet floor, the sea-mouth ahead-left ──
  let uvx = px.x / 256.0 - 1.0;
  let uvy = px.y / 256.0 - 1.0;
  let ro = vec3f(0.0, 1.5, 4.0);
  let rd = normalize(vec3f(uvx, -uvy - 0.06, -1.35));
  // the daylight/moonlight spilling from the mouth, tinted by the ruling moon
  let dayCol = mix(vec3f(0.55, 0.78, 0.92), vec3f(1.05, 0.95, 0.82), dawn);
  let moonCol = mix(vec3f(0.40, 0.62, 0.85), tgs_hue(moonHue), 0.6);
  let spill = mix(moonCol, dayCol, 1.0 - night * (1.0 - dawn));
  let hit = tgs_march(ro, rd, t);
  var c: vec3f;
  if (hit.x > 0.0) {
    let pw = ro + rd * hit.x;
    let n = tgs_nrm(pw, t);
    let lmouth = normalize(TGS_MOUTH - pw);                    // the mouth is the cave's one light
    let dif = max(dot(n, lmouth), 0.0);
    let sh = tgs_shadow(pw + n * 0.02, lmouth, t);
    let ao = tgs_ao(pw, n, t);
    var col = tgs_alb(hit.y, pw) * (0.16 + 1.05 * dif * sh) * ao;
    col += spill * ao * (0.22 + 0.35 * dif) * 0.7;             // cool ambient bounce from the mouth
    // the tide-pool glow lifts everything near the floor
    col += vec3f(0.06, 0.34, 0.40) * exp(-abs(pw.y + 0.18) * 2.2) * 0.5;
    // ── THE SOCKET: a carved bowl; empty it's dark, seated it blazes ──
    if (hit.y > 4.5 && hit.y < 5.5) {
      col += tgs_hue(moonHue) * 0.10;                          // faint tideglass in the stone
      if (pearlSet > 0.5) {
        let fres = pow(1.0 - max(dot(-rd, n), 0.0), 2.0);
        col = mix(vec3f(0.6, 0.75, 0.85), vec3f(1.2, 1.15, 1.0), fres);
        col += vec3f(0.5, 0.8, 0.95) * (0.6 + 0.4 * sin(t * 3.0));
      }
    }
    // pearl resonance on the plinth bronze after seating
    if (pearlSet > 0.5 && hit.y > 3.5 && hit.y < 4.5) {
      col += vec3f(0.3, 0.45, 0.55) * (0.4 + 0.4 * sin(t * 6.0 + pw.y * 12.0)) * 0.3;
    }
    c = col;
    c = mix(c, vec3f(0.02, 0.045, 0.06), smoothstep(6.0, 14.0, hit.x) * 0.5);   // depth haze
  } else {
    // ── the ray escaped through the MOUTH: the sea, the sky, the ruling moon ──
    c = tgs_ocean(rd, t, night, dawn, moonHue, spill);
  }
  // ── the LASER: once the pearl is seated, a bright beam climbs from the socket
  //    and out the mouth — the line that powers the airship's crystal ──
  if (pearlSet > 0.5) {
    let a = TGS_SOCK + vec3f(0.0, 0.14, 0.0);
    let b = TGS_MOUTH;
    let bd = tgs_segDist(ro, rd, a, b);
    let core = exp(-bd * bd * 900.0);
    let glow = exp(-bd * bd * 60.0);
    let beam = tgs_hue(moonHue);
    let pulse = 0.7 + 0.3 * sin(t * 18.0 - px.y * 0.05);       // energy climbing the beam
    c += (vec3f(1.2) * core + (beam * 0.6 + vec3f(0.3, 0.6, 0.8)) * glow) * pulse;
  }
  // ── screen-space affordances ──
  // the socket: a ring inviting the pearl (only while you carry it, before seating)
  if (uni(108) > 0.5 && pearlSet < 0.5) {
    let sc = vec2f(256.0, 300.0);
    c += vec3f(0.7, 0.85, 0.95) * tg_ring(px, sc, 40.0, 33.0) * (0.3 + 0.3 * sin(t * 2.6)) * (0.5 + 0.5 * smoothstep(56.0, 24.0, length(mm - sc)));
  }
  // the 16t gear under the loose plank (unchanged clue — reads invMask bit 8)
  {
    let m8 = uni(114);   // 1 once the 8t gear was EVER taken (seating it must not resurrect the plank gear)
    c += vec3f(0.95, 0.80, 0.42) * mod_tg_icon_gear(px, vec2f(150.0, 420.0), 15.0, 8.0, t) * (1.0 - m8) * (0.75 + 0.25 * sin(t * 2.5));
    c += vec3f(0.9, 0.75, 0.4) * tg_ring(px, vec2f(150.0, 420.0), 24.0, 21.0) * (1.0 - m8) * (0.18 + 0.18 * sin(t * 2.5));
  }
  // drifting spores + the cursor's cool glow
  let mote = hash21(floor(vec2f(px.x * 0.28, px.y * 0.28 - t * 8.0)));
  c += vec3f(0.4, 0.8, 0.9) * step(0.9965, mote) * (0.4 + 0.6 * sin(t * 2.0 + mote * 40.0)) * 0.35;
  let cur = (mm - 256.0) / 256.0;
  c += vec3f(0.3, 0.7, 0.8) * exp(-dot(p - cur, p - cur) * 12.0) * 0.10;
  // click-confirm ripple
  let cfx = uni(84);
  if (cfx > 0.003) { let cd = length(px - vec2f(uni(85), uni(86))); c += vec3f(0.8, 0.9, 0.95) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4; }
  c = tgs_obelisk(c, px, t);
  return max(c, vec3f(0.0));
}

fn mod_tg_scene(view: i32, p: vec2f, px: vec2f, t: f32) -> vec3f {
  if (uni(113) > 0.0005) { return mod_tg_flycine(p, px, t); }   // THE CROSSING — Act I finale owns the frame
  if (view == 1) { return mod_tg_gate(p, px, t); }
  if (view == 2) { return mod_tg_hall(p, px, t); }
  if (view == 3) { return mod_tg_lens(p, px, t); }
  if (view == 4) { return mod_tg_stele(p, px, t); }
  if (view == 5) { return mod_tg_lattice(p, px, t); }
  if (view == 6) { return mod_tg_core(p, px, t); }
  if (view == 7) { return mod_tg_orrery(p, px, t); }
  if (view == 8) { return mod_tg_seacave(p, px, t); }
  if (view == 9) { return mod_tg_deck(p, px, t); }
  if (view == 10) { return mod_tg_engine(p, px, t); }
  if (view == 11) { return mod_tg_mirror(p, px, t); }
  if (view == 12) { return mod_tg_thicket(p, px, t); }
  if (view == 13) { return mod_tg_drowned(p, px, t); }
  if (view == 14) { return mod_tg_kiln(p, px, t); }
  return mod_tg_shore(p, px, t);
}

// ═══ THE OBELISK — a slim glass-stone monolith in the grotto, px(430,300),
//     tide-ring engravings lit by the cave's own bioluminescence. uni(117)
//     irises the slot; the SPANNER waits inside until uni(53) says taken.
//     (Self-contained spanner SDF — the satchel's icon fns are visualType-side
//     and unreachable from modules.) ═══
fn tgs_spannerSdf(d: vec2f, s: f32) -> f32 {
  let h = sdSegment(d, vec2f(-s, s * 0.55), vec2f(s * 0.35, -s * 0.30));
  var m = smoothstep(0.20 * s, 0.12 * s, h);
  let hc = d - vec2f(0.62 * s, -0.50 * s);
  m = max(m, smoothstep(0.13 * s, 0.06 * s, abs(length(hc) - 0.33 * s)) * step(-hc.y, 0.30 * s));
  return clamp(m, 0.0, 1.0);
}
fn tgs_obelisk(cIn: vec3f, px: vec2f, t: f32) -> vec3f {
  var c = cIn;
  let O = vec2f(430.0, 300.0);
  let d = px - O;
  if (abs(d.x) > 46.0 || d.y < -118.0 || d.y > 96.0) { return c; }
  let openA = clamp(uni(117), 0.0, 1.0);
  let taken = uni(53);
  // the stone: a tall tapered slab, glassy-dark, strata catching the grotto light
  let hw = 20.0 - d.y * 0.055;                        // taper toward the crown
  let slab = max(abs(d.x) - hw, max(d.y - 88.0, -d.y - 112.0));
  if (slab < 0.0) {
    var oc = vec3f(0.030, 0.042, 0.052) * (0.8 + 0.45 * fbm(d * 0.16 + vec2f(11.0), 2));
    // tide-ring engravings: pale horizontal bands, worn at the edges
    let ring = max(sin(d.y * 0.30 + 1.3), 0.0);
    oc += vec3f(0.10, 0.20, 0.24) * ring * ring * smoothstep(hw, hw - 7.0, abs(d.x)) * 0.35;
    // bioluminescent rim light along the left edge (the water side)
    oc += vec3f(0.08, 0.30, 0.30) * smoothstep(4.0, 0.0, abs(d.x + hw)) * 0.5;
    c = mix(c, oc, smoothstep(1.5, -1.5, slab));
  }
  // THE SLOT — a vertical lens aperture at the heart; stone leaves withdraw as
  // openA rises. Interior glows; the spanner rests on a pedestal of light.
  let sd = vec2f(d.x, (d.y + 14.0) / 2.1);
  let mouth = length(sd) - 15.0;
  let leaves = abs(d.x) - 15.0 * openA;               // the leaves' retreating inner edge
  let slot = max(mouth, -leaves);                     // open where leaves have withdrawn
  if (mouth < 0.0 && leaves < 0.0) {
    var ic = vec3f(0.012, 0.020, 0.026);
    ic += vec3f(0.10, 0.30, 0.34) * (0.5 + 0.5 * sin(t * 1.7)) * 0.5;         // breathing inner light
    c = mix(c, ic, smoothstep(1.0, -1.0, mouth));
    if (taken < 0.5) {
      let sm = tgs_spannerSdf(d - vec2f(0.0, -14.0), 11.0);
      c = mix(c, vec3f(0.68, 0.66, 0.60), sm * 0.9);                            // old bright steel
      c += vec3f(0.35, 0.55, 0.60) * sm * (0.4 + 0.3 * sin(t * 2.3));
      c += vec3f(0.12, 0.30, 0.34) * exp(-dot(d - vec2f(0.0, -2.0), d - vec2f(0.0, -2.0)) * 0.004) * 0.6;  // pedestal glow
    } else {
      c += vec3f(0.30, 0.14, 0.06) * exp(-dot(sd, sd) * 0.02) * 0.35;          // the ember it leaves behind
    }
  }
  // the unlock flare: while the leaves are mid-travel the seam burns
  c += vec3f(0.55, 0.85, 0.85) * smoothstep(3.0, 0.0, abs(leaves)) * step(mouth, 0.0)
       * openA * (1.0 - openA) * 4.0 * (0.6 + 0.4 * sin(t * 6.0));
  return c;
}

module · tg_thicket

// ═══════════════════════════════════════════════════════════════════════════
// mod_tg_thicket  —  VIEW 12, THE THICKET  (synthesized module)
// Tideglass Act 1, design doc sec 12 "THE THICKET — an eastward room, fire's
// first home". Composed by the thicket synthesizer from the five hand-built
// aspect parts in tg-build/thicket/ (backlight, beyond_shape, roots, tangle,
// wet_green_readout), following the airship/ parts->synthesized pattern.
//
// THE SCENE: a dense overgrown tangle choking an eastward passage. A shape
// (an arched stone threshold) is visible but unreachable deep beyond the wet
// branches, backlit from somewhere past the canopy. Wet green everywhere that
// reads as NOT-yet-burnable (glossy, sodden, beaded). Ground roots grip the
// soil. In Act 1 the thicket is an impassable "not yet"; once the fire moon
// exists and BURN is chosen it chars away and the passage opens.
//
// EXPOSED FN (matches the cartridge view-fn signature, cf. mod_tg_shore):
//   fn mod_tg_thicket(p: vec2f, px: vec2f, t: f32) -> vec3f
//     p  : view uv in [-1,1], y-DOWN (top of screen = negative y).
//     px : pixel coords 0..512 (unused here; nav chevrons are drawn by the
//          cartridge's separate nav pass, not inside the view fn).
//     t  : scene seconds.
//
// UNIFORMS THIS MODULE READS (documented per the shader/hook-consistency law):
//   uni(76)  thicketBurn01  in [0,1]. 0 = wet, living, impassable Act-1 thicket
//            (cool green backlight, wet-green material, breathing sway). ->1 the
//            fire moon has dried and charred the tangle: green desaturates to
//            charcoal, ember cracks flare then die, roots/branches thin so the
//            way past clears, the backlight floods warm and open. The step hook
//            MUST write U[76]; until it does, uni() returns 0 (the correct
//            Act-1 default), so the module is safe before the hook is wired.
//   (uni(76) is the ONLY uniform read here. All burn behaviour is forwarded to
//    the aspect parts as their `burn` argument.)
//
// SHARED HELPERS RELIED ON (from the shaders.ts frame preamble + cartridge lib,
//   in scope when this module is concatenated): fbm, rot2, hash21, vnoise,
//   sdSegment, mod_tgo_suncol. No baked pixel data / no const arrays
//   (freeze-quarantine law); pure procedural SDF + noise throughout.
//
// COMPOSITION (back -> front), each part in its documented role:
//   0. base        : a dim wet-forest depth gradient so gaps read as depth.
//   1. beyond_shape : the arched threshold silhouette + its glow/haze (backdrop).
//   2. backlight    : the breach halo, god-ray shafts and drifting motes leaking
//                     from beyond — drawn BEFORE the branches so they occlude it
//                     into slivers (its header asks to be composited early).
//   3. roots        : the detailed ground-root tangle gripping the soil, before
//                     the tall branch weave overhangs it.
//   4. tangle       : the dominant branch weave, wet foliage clumps, burn front,
//                     embers and smoke — composited over the lit backdrop.
//   5. wet_green    : a wet-green material enrichment (beads, sheen, sick-green)
//                     masked to the darker on-branch pixels, held out of the
//                     bright breach and receding as the burn takes over so the
//                     tangle part owns the char.
// ═══════════════════════════════════════════════════════════════════════════


// ── aspect: backlight ──────────────────────────────────────────────────────
// The light from BEYOND the tangle: a wet luminous glow leaking through a gap in
// the canopy, god-ray shafts raking down, drifting dust motes. Light ONLY (no
// branches / silhouette) so it is drawn early and later occluded into slivers.
// Additive over cIn. Reads no uniform directly; `burn` is passed in from uni(76).
fn mod_tg_thicket_backlight(cIn: vec3f, p: vec2f, t: f32, burn: f32) -> vec3f {
  var c = cIn;

  // ── the gap in the canopy: the one bright breach the light pours from ──
  // it breathes a touch (wind stirring the far leaves) so the beam is never dead-still.
  let sx = 0.02 * sin(t * 0.23) + 0.015 * sin(t * 0.61 + 1.3);
  let s  = vec2f(sx, -0.30);                 // high-centre (y-down: negative = up)
  let d  = p - s;
  let r  = length(d);
  let ang = atan2(d.y, d.x);                  // angle around the breach for the shafts

  // ── burn palette: wet green-gold daylight → hot ember flood ──
  // beyond the wet thicket the light reads as a cool, sunless forest glow;
  // once fire has taken the tangle the same breach roars warm and much brighter.
  let coolBeyond = mix(vec3f(0.34, 0.62, 0.40), vec3f(0.86, 0.92, 0.66), 0.35); // green-gold
  let hotBeyond  = mod_tgo_suncol(0.10);       // low warm ember key (kin to the world sun)
  let beamCol = mix(coolBeyond, hotBeyond, burn);
  let gain    = mix(1.0, 2.6, burn);           // the passage floods as it chars open

  // ── (1) the breach halo: soft radial bloom from the gap, warm core ──
  let halo = exp(-r * 2.4) + 0.55 * exp(-r * 6.5);
  let core = exp(-r * 11.0);                    // near-white heart of the opening
  c += beamCol * halo * (0.28 * gain);
  c += mix(vec3f(0.82, 0.95, 0.78), vec3f(1.10, 0.86, 0.52), burn) * core * (0.55 * gain);

  // ── (2) god-ray shafts: streaks raking out of the breach, down through the tangle ──
  let drift = t * 0.06;
  let streak = fbm(vec2f(ang * 3.2 + drift, r * 1.4 - drift * 0.5), 3);
  var shafts = smoothstep(0.42, 0.86, streak);
  // bias the shafts downward (light falls INTO the room, y-down = +y is down)
  let downBias = smoothstep(-0.55, 0.65, p.y);
  shafts *= downBias;
  let reach = exp(-r * 1.7);                    // fade with distance from the breach
  c += beamCol * shafts * reach * (0.34 * gain);

  // ── (3) dust motes adrift in the beam: sparse, slow, only where light reaches ──
  {
    let mp = p * vec2f(9.0, 9.0) + vec2f(-drift * 1.5, drift * 3.0);
    let cell = floor(mp);
    let h = hash21(cell);
    let twinkle = 0.5 + 0.5 * sin(t * (0.9 + h * 1.4) + h * 40.0);
    let mote = step(0.965, h) * smoothstep(0.34, 0.03, length(fract(mp) - 0.5));
    c += beamCol * mote * twinkle * reach * downBias * (0.5 + 0.6 * burn);
  }

  // ── (4) a faint vertical breath of light down the throat of the passage ──
  let throat = exp(-(p.x - sx) * (p.x - sx) / (0.010 + burn * 0.05));
  c += beamCol * throat * downBias * (0.06 + 0.20 * burn);

  return c;
}


// ── aspect: beyond_shape ───────────────────────────────────────────────────
// The arched threshold / gateway deep past the tangle: the "shape visible but
// unreachable". Composited EARLY as the far backdrop; the branches draw over it.
// Act 1: wet green-grey silhouette against a cold hazed glow. Burn: clears/warms.
// Self-contained hash so it survives any synthesis order.
fn mod_tg_thicket_bs_hash(q: vec2f) -> f32 {
  return fract(sin(dot(q, vec2f(41.317, 289.113))) * 43758.5453);
}

fn mod_tg_thicket_beyond_shape(cIn: vec3f, p: vec2f, t: f32, burn01: f32) -> vec3f {
  var c = cIn;
  let burn = clamp(burn01, 0.0, 1.0);

  // ── local arch coords: O at frame centre, a.y points UP (p.y is screen-down) ──
  let O = vec2f(0.0, 0.0);
  let a = vec2f(p.x - O.x, O.y - p.y);

  // arch opening = rounded-top gateway (legs box unioned with a semicircle cap)
  let hw   = 0.15;                       // half width of the passage
  let base = -0.30;                      // bottom of the opening (a.y)
  let cap  = 0.14;                       // where the legs stop and the arch turns
  // box SDF for the straight legs
  let bc = vec2f(0.0, (base + cap) * 0.5);
  let bh = vec2f(hw, (cap - base) * 0.5);
  let bd = abs(a - bc) - bh;
  let dLegs = length(max(bd, vec2f(0.0))) + min(max(bd.x, bd.y), 0.0);
  // circle SDF for the arch cap
  let dCap = length(a - vec2f(0.0, cap)) - hw;
  let dOp  = min(dLegs, dCap);           // <0 inside the passage, >0 in the stone / outside

  // ── the light from beyond: a soft warm shaft climbing out of the opening ──
  let coldGlow = vec3f(0.14, 0.30, 0.28);                 // dim verdigris-teal cast
  let warmGlow = vec3f(1.15, 0.62, 0.24);                 // ember / amber from beyond
  let glowCol  = mix(coldGlow, warmGlow, burn);
  let colX  = smoothstep(hw + 0.10, 0.0, abs(a.x));       // narrows the beam to the gap
  let colY  = smoothstep(0.55, -0.20, a.y);               // fades upward into depth
  let breathe = 0.85 + 0.15 * sin(t * 0.6);
  let shaft = colX * colY * breathe;
  c += glowCol * shaft * (0.10 + 0.55 * burn);

  // bright core seen THROUGH the opening (the "somewhere it leads")
  let coreMask = smoothstep(0.012, -0.05, dOp);
  let coreCol  = mix(vec3f(0.20, 0.42, 0.40), vec3f(1.30, 0.78, 0.34), burn);
  let shim = mod_tg_thicket_bs_hash(floor(vec2f(a.x * 40.0, a.y * 22.0 - t * 1.3)));
  c = mix(c, coreCol * (0.55 + 0.45 * shim), coreMask * (0.35 + 0.45 * burn));

  // ── the stone threshold frame: a wet, mossy silhouette (the shape you can't reach) ──
  let wall = 0.085;                                       // frame thickness
  let frameMask = smoothstep(-0.010, 0.006, dOp)
                * smoothstep(wall + 0.012, wall - 0.012, dOp);
  let stoneWet = vec3f(0.070, 0.115, 0.090);
  let stoneDry = vec3f(0.150, 0.120, 0.088);
  var stone = mix(stoneWet, stoneDry, burn);
  let rim = smoothstep(wall, wall - 0.030, dOp) * smoothstep(-0.010, 0.010, dOp);
  stone += glowCol * rim * (0.20 + 0.40 * burn);
  c = mix(c, stone, frameMask * 0.92);

  // ── atmospheric depth haze: the beyond sits far back behind the tangle ──
  let hazeCol = mix(vec3f(0.10, 0.17, 0.16), vec3f(0.34, 0.22, 0.15), burn);
  let haze = (0.42 - 0.34 * burn) * smoothstep(0.9, -0.4, a.y);
  c = mix(c, hazeCol, clamp(haze, 0.0, 1.0));

  return c;
}


// ── aspect: roots ──────────────────────────────────────────────────────────
// A ground-root tangle gripping the soil at the base of the thicket. Composited
// after the backlit backdrop and before the tall branch tangle. Ground horizon
// yG = 0.42; roots sprawl at and below it. burn passed from uni(76).
fn mod_tg_thicket_roots(cIn: vec3f, p: vec2f, t: f32, burn: f32) -> vec3f {
  var c = cIn;
  let yG = 0.42;                           // ground horizon for view 12

  // GNARL: warp the sample point so every root reads hand-twisted, not a clean capsule.
  let warp = vec2f(
    vnoise(p * 3.1 + vec2f(11.0,  3.0)),
    vnoise(p * 3.4 + vec2f(-5.0,  7.0))) - vec2f(0.5);
  let pw = p + warp * 0.045;

  var cov  = 0.0;    // root coverage (0..1)
  var lit  = 0.0;    // top / backlit edge accumulation (moss + wet sheen)
  var deep = 0.0;    // crevice overlap -> occlusion darkening

  // Fan of gnarled roots emerging from the ground line and crawling outward.
  let N = 7;
  for (var i = 0; i < N; i = i + 1) {
    let fi = f32(i);
    let s  = hash21(vec2f(fi, 4.0));       // per-root jitter
    let s2 = hash21(vec2f(fi, 9.0));

    // base anchored along the ground line, spread across the frame
    let bx   = mix(-0.95, 0.95, fi / f32(N - 1)) + (s - 0.5) * 0.10;
    let base = vec2f(bx, yG + 0.02 + s2 * 0.04);
    let dir  = sign(bx + 1e-3);            // crawl away from centre

    // knee + tip: root reaches sideways, then dives into the soil (y down)
    let knee = base + vec2f(dir * (0.10 + s  * 0.14), 0.14 + s2 * 0.12);
    let tip  = knee + vec2f(dir * (0.06 + s2 * 0.20), 0.20 + s  * 0.16);

    // slow breathing sway, the WET tangle is alive; killed as it chars
    let sway = (1.0 - burn) * 0.010 * sin(t * 0.6 + fi * 1.7);
    let kA = knee + vec2f(sway,       0.0);
    let tB = tip  + vec2f(sway * 1.8, 0.0);

    // two tapered segments per root
    let d = min(sdSegment(pw, base, kA), sdSegment(pw, kA, tB));

    // taper: thick where it emerges, thinning as it spreads; thins more as it burns.
    let alongY = clamp((pw.y - (yG - 0.10)) / 0.50, 0.0, 1.0);
    let wid = (0.028 + 0.020 * (1.0 - alongY)) * (1.0 - 0.35 * s) * (1.0 - 0.45 * burn);
    let rc  = smoothstep(wid, wid * 0.40, d);
    cov = max(cov, rc);

    // lit upper edge, light from beyond catches the top of each root
    lit = max(lit, smoothstep(wid * 1.4, 0.0, d) * smoothstep(0.0, 0.02, kA.y - pw.y));
    deep += rc;                            // count overlaps for occlusion
  }

  if (cov <= 0.001) { return c; }          // let backlit gaps pass through

  // ---- WET, LIVING colour ---------------------------------------------
  let woodDk  = vec3f(0.055, 0.070, 0.050);  // near-black damp bark
  let woodMid = vec3f(0.110, 0.140, 0.100);
  let moss    = vec3f(0.160, 0.340, 0.170);  // wet tideglass green on lit edges
  let grain   = 0.5 + 0.5 * vnoise(pw * 22.0 + vec2f(0.0, t * 0.05));
  var rootCol = mix(woodDk, woodMid, grain);
  rootCol = mix(rootCol, moss, clamp(lit, 0.0, 1.0) * 0.60 * (1.0 - burn));

  // cold wet sheen along the lit edges, reads as damp, un-burnable
  let sheen = pow(clamp(lit, 0.0, 1.0), 3.0) * (1.0 - burn);
  rootCol += vec3f(0.20, 0.30, 0.26) * sheen * 0.50;

  // crevice occlusion where roots pile over one another
  rootCol *= 1.0 - 0.35 * smoothstep(1.2, 2.4, deep);

  // ---- CHAR / EMBER (burn state) --------------------------------------
  if (burn > 0.001) {
    rootCol = mix(rootCol, vec3f(0.030, 0.028, 0.030), smoothstep(0.0, 0.70, burn));
    let crack     = smoothstep(0.62, 0.72, fbm(pw * 30.0 + vec2f(t * 0.6, 0.0), 3));
    let emberLife = smoothstep(0.05, 0.35, burn) * (1.0 - smoothstep(0.60, 1.00, burn));
    rootCol += vec3f(1.20, 0.42, 0.10) * crack * emberLife * cov;
  }

  c = mix(c, rootCol, cov);
  return c;
}


// ── aspect: tangle ─────────────────────────────────────────────────────────
// The overgrown branch weave that chokes the passage: layered wavy strands, wet
// foliage clumps, a burn front that chars then ashes to open the way, embers and
// smoke. Composites over cIn and lets the backlight leak through the gaps.
// burn passed from uni(76).
fn mod_tg_thicket_tangle(cIn: vec3f, p: vec2f, t: f32, burn: f32) -> vec3f {
  // ── (1) BACKLIGHT FROM BEYOND ─────────────────────────────────────────────
  // A pale, faintly warm glow rises from the unreachable space past the branches.
  // Sits BEHIND the tangle; revealed as the branches burn away (alpha drops below).
  let gap   = p - vec2f(0.0, -0.12);
  let beyond = exp(-dot(gap * vec2f(1.05, 1.35), gap * vec2f(1.05, 1.35)) * 1.9);
  let arch  = smoothstep(0.34, 0.0, abs(p.x)) * smoothstep(0.55, -0.35, p.y);
  let backCol = mix(vec3f(0.30, 0.42, 0.44), vec3f(0.78, 0.72, 0.55), 0.35 + beyond * 0.4);
  var back = mix(cIn, backCol, clamp(beyond * 0.72 + arch * 0.18, 0.0, 0.9));

  // ── (2) THE TANGLE — layered wavy strands, far→near ───────────────────────
  var T = vec3f(0.0);
  var A = 0.0;
  let breathe = 0.012 * sin(t * 0.23);   // barely-there sway; the wet green holds
  for (var i = 0; i < 3; i = i + 1) {
    let fi = f32(i);
    let ang = 0.62 - fi * 0.66;                       // each layer crosses the last
    let u   = rot2(ang) * (p + vec2f(breathe * (fi - 1.0), 0.0));
    let sp  = 0.150 + fi * 0.046;                     // strand spacing (near = looser)
    let wob = 0.085 * sin(u.x * 2.7 + fi * 2.1) + (fbm(u * 1.5 + vec2f(fi * 6.3), 3) - 0.5) * 0.28;
    let uy  = (u.y + wob) / sp;
    let cell = fract(uy) - 0.5;                        // signed across the strand
    let thick = 0.30 + 0.16 * sin(u.x * 5.3 + fi * 1.7);   // knotty thickening
    let cov = smoothstep(thick, thick * 0.34, abs(cell)) * step(0.06, thick);
    if (cov > 0.001) {
      let lit  = 0.34 + 0.66 * (fi / 2.0);            // nearer layers catch more light
      let moss = fbm(u * 5.5 + vec2f(fi * 3.1), 2);
      var bcol = mix(vec3f(0.020, 0.030, 0.022), vec3f(0.060, 0.100, 0.058), moss) * lit;
      bcol = mix(bcol, vec3f(0.050, 0.082, 0.066), 0.35);   // cartridge verdigris key
      let sheen = smoothstep(0.0, thick * 0.9, cell) * smoothstep(thick, thick * 0.2, abs(cell));
      bcol += vec3f(0.42, 0.60, 0.52) * sheen * 0.30 * lit;
      let bead = step(0.86, fbm(u * 22.0, 2)) * smoothstep(thick, thick * 0.4, abs(cell));
      bcol += vec3f(0.55, 0.70, 0.62) * bead * 0.35 * lit;
      T = mix(T, bcol, cov);
      A = max(A, cov * (0.55 + 0.45 * lit));
    }
  }

  // ── (3) WET FOLIAGE CLUMPS clinging to the weave ──────────────────────────
  let leaf  = fbm(p * 3.1 + vec2f(t * 0.015, 1.7), 4);
  let clump = smoothstep(0.52, 0.82, leaf) * (0.35 + A);   // leaves cling to branches
  if (clump > 0.001) {
    let lcol = mix(vec3f(0.035, 0.085, 0.045), vec3f(0.100, 0.190, 0.085), fbm(p * 8.0, 3));
    T = mix(T, lcol, clamp(clump, 0.0, 1.0));
    A = max(A, clamp(clump * 0.9, 0.0, 1.0));
    T += vec3f(0.50, 0.68, 0.55) * step(0.90, leaf) * 0.30;   // glossy wet dots
  }

  // ── (4) GROUND ROOTS fanning up from the floor ────────────────────────────
  let rq = p - vec2f(0.0, 1.25);
  let ra = atan2(rq.x, -rq.y);                         // fan angle from below
  let rootField = abs(fract(ra * 3.8 + (fbm(p * 2.2, 2) - 0.5) * 1.6) - 0.5);
  let rootCov = smoothstep(0.11, 0.03, rootField) * smoothstep(-0.1, 0.9, p.y);
  if (rootCov > 0.001) {
    let rcol = mix(vec3f(0.030, 0.028, 0.022), vec3f(0.070, 0.075, 0.052), fbm(p * 6.0, 2));
    T = mix(T, rcol, rootCov);
    A = max(A, rootCov);
  }

  // ── (5) BURN STATE (uni(76)) — chars away, opens the passage ──────────────
  let A0 = A;                                          // material before it ashes
  let bn = fbm(p * 2.3 + vec2f(4.1, 2.6), 4);          // the burn front's shape
  let bd = burn * 1.30;                                // let it fully consume at 1.0
  let charAmt = smoothstep(bn - 0.10, bn + 0.12, bd);
  let ashAmt  = smoothstep(bn + 0.04, bn + 0.32, bd);
  T = mix(T, vec3f(0.045, 0.040, 0.045), charAmt);     // charred to black bark
  A = A * (1.0 - ashAmt);                              // burns through → gap opens

  var col = mix(back, T, A);

  // glowing embers along the front, only where material still exists
  let front = smoothstep(0.15, 0.0, abs(bd - bn)) * step(0.02, burn) * (1.0 - ashAmt * 0.6);
  col += vec3f(1.25, 0.48, 0.13) * front * A0 * (0.7 + 0.5 * fbm(p * 9.0 + vec2f(t * 0.9), 2));
  // a warm smoke lift above the char while it burns
  let smoke = smoothstep(0.0, 0.5, bd) * (1.0 - smoothstep(0.7, 1.15, bd)) * A0
              * smoothstep(0.35, -0.6, p.y) * (0.4 + 0.6 * fbm(p * vec2f(3.0, 6.0) + vec2f(0.0, -t * 0.5), 3));
  col = mix(col, vec3f(0.16, 0.14, 0.15), smoke * 0.30);

  return col;
}


// ── aspect: wet_green_readout ──────────────────────────────────────────────
// Re-skins an on-branch colour as glossy, sodden, deep-green growth: mottled
// leaf/moss, hard wet specular from the backlight, beaded droplets, deep knot
// shadow. Preserves cIn's shading via luminance so it composes over any branch
// shape. As burn rises the green desaturates to charcoal and ember cracks flare.
// burn passed from uni(76).
fn mod_tg_thicket_wet_green_readout(cIn: vec3f, p: vec2f, t: f32, burn: f32) -> vec3f {
  let lum = clamp(dot(cIn, vec3f(0.299, 0.587, 0.114)), 0.0, 1.0);

  // ── mottled wet-green albedo ──────────────────────────────────────────────
  let grain = fbm(p * 7.3, 3);                 // broad leaf/moss clumps
  let fine  = fbm(p * 23.0 + vec2f(4.1, 1.7), 2); // damp speckle
  let mottle = clamp(grain * 0.75 + fine * 0.25, 0.0, 1.0);

  let barkGreen = vec3f(0.030, 0.066, 0.038);  // sodden shadowed growth
  let leafGreen = vec3f(0.115, 0.255, 0.095);  // lit wet leaf
  let sickGreen = vec3f(0.190, 0.270, 0.070);  // yellow-green highlight (young, unburnable)
  var col = mix(barkGreen, leafGreen, smoothstep(0.15, 0.85, mottle));
  col = mix(col, sickGreen, smoothstep(0.60, 0.95, mottle) * 0.5);
  col *= 0.45 + 0.85 * lum;                     // carry the branch's own shading

  // ── backlight sheen (light from beyond, through the gaps) ─────────────────
  let gx = fbm(p * 7.3 + vec2f(0.02, 0.0), 3) - grain;
  let gy = fbm(p * 7.3 + vec2f(0.0, 0.02), 3) - grain;
  let edge = clamp(length(vec2f(gx, gy)) * 42.0, 0.0, 1.0);
  let backlit = vec3f(0.55, 0.95, 0.62);       // cold wet-green glow leaking from beyond
  col += backlit * edge * (0.30 + 0.35 * mottle);

  // ── glossy wet specular: tight hotspots on upper-facing wet surfaces ──────
  let sheen = pow(clamp(lum * (0.5 + 0.5 * mottle), 0.0, 1.0), 6.0);
  col += vec3f(0.80, 0.95, 0.78) * sheen * 0.28;

  // ── beaded water droplets: sparse procedural highlights, slow shimmer ─────
  let bcell = floor(p * 34.0);
  let bh = hash21(bcell);
  let bl = length(fract(p * 34.0) - 0.5);
  let bead = step(0.972, bh) * smoothstep(0.34, 0.06, bl);
  let twinkle = 0.55 + 0.45 * sin(t * 1.6 + bh * 40.0);
  col += vec3f(0.85, 1.0, 0.92) * bead * twinkle * 0.55;

  // ── deep self-shadow in the densest knot (low-lum, low-mottle cores) ──────
  let knot = (1.0 - smoothstep(0.05, 0.35, lum)) * (1.0 - mottle);
  col *= 1.0 - knot * 0.40;

  // ═══ BURN STATE (uni(76)) — chars the wet green away ══════════════════════
  if (burn > 0.001) {
    let flick = fbm(p * 9.0 + vec2f(0.0, -t * 0.6), 3);
    let heat  = smoothstep(0.55, 0.30, abs(flick - (0.35 + 0.30 * burn)));
    let emberWin = smoothstep(0.05, 0.35, burn) * (1.0 - smoothstep(0.75, 1.0, burn));
    let ember = vec3f(1.15, 0.42, 0.10) * heat * emberWin;

    let charc = vec3f(0.030, 0.024, 0.020);
    let ash  = vec3f(0.070, 0.066, 0.062);
    var burnt = mix(charc, ash, smoothstep(0.6, 1.0, burn));
    burnt += ember;                               // embers glow through the char
    burnt += vec3f(0.30, 0.10, 0.02) * heat * emberWin * 0.5; // heat bleed around cracks

    col = mix(col, burnt, smoothstep(0.0, 0.9, burn));
  }

  return col;
}


// ═══════════════════════════════════════════════════════════════════════════
// THE VIEW FN — layers the aspects into the thicket scene.
// ═══════════════════════════════════════════════════════════════════════════
fn mod_tg_thicket(p: vec2f, px: vec2f, t: f32) -> vec3f {
  let burn = clamp(uni(76), 0.0, 1.0);   // U[76] thicketBurn01 (0 wet Act1, 1 charred)

  // ── 0. base: a dim wet-forest depth gradient so the gaps read as depth,
  //    not void. Cool green-black up near the canopy breach, darker damp earth
  //    below. Warms a touch as the far light floods on burn.
  let deep = mix(vec3f(0.055, 0.100, 0.090), vec3f(0.020, 0.038, 0.036),
                 smoothstep(-1.0, 1.0, p.y));
  var c = mix(deep, mix(deep, vec3f(0.10, 0.075, 0.050), 0.5), burn * 0.5);

  // ── 1. the arched threshold beyond (the shape you cannot reach) — backdrop.
  c = mod_tg_thicket_beyond_shape(c, p, t, burn);

  // ── 2. backlight from beyond: halo, god-ray shafts, motes — drawn before the
  //    branches so they occlude it into slivers of glow between the boughs.
  c = mod_tg_thicket_backlight(c, p, t, burn);

  // ── 3. the ground-root tangle gripping the soil, before the branches overhang.
  c = mod_tg_thicket_roots(c, p, t, burn);

  // ── 4. the dominant branch weave / foliage / burn front over the lit backdrop.
  c = mod_tg_thicket_tangle(c, p, t, burn);

  // ── 5. wet-green material enrichment (the "not-yet-burnable" read): beads,
  //    sheen and sick-green skinned onto the DARKER on-branch pixels, held out of
  //    the bright breach and receding as the burn takes over so the tangle owns
  //    the char. lum-keyed mask keeps it off the glow, so the beyond stays clear.
  {
    let lum = dot(c, vec3f(0.299, 0.587, 0.114));
    let branchProxy = smoothstep(0.42, 0.10, lum);          // dark = likely branch
    let breach = exp(-length(p - vec2f(0.0, -0.30)) * 2.0); // hold out of the beyond glow
    let wetMask = branchProxy * (1.0 - breach * 0.7)
                * (1.0 - smoothstep(0.25, 0.65, burn)) * 0.55;
    let wet = mod_tg_thicket_wet_green_readout(c, p, t, burn);
    c = mix(c, wet, clamp(wetMask, 0.0, 1.0));
  }

  return max(c, vec3f(0.0));
}

module · tg_drowned

// THE DROWNED QUARTER (view 13) — combo B's payoff, rebuilt raymarched. A stone
// undercroft beneath the gate: fluted columns, silt floor, the trapdoor's light
// shaft. Its FLOODWATER is the island's tide (uni 60): valve closed → black
// water over everything, the bell's crown a drowned glimmer; sluice open → the
// quarter DRAINS → THE DROWNED BELL stands in the silt. Strike it (bell tolls,
// uni 107) → the crown opens → THE TIDEGLASS PEARL (uni 108). Anchors (px, LAW):
// bell (256,316) r70 · pearl (256,226) r34. Author: Claude Fable 5.

fn tgw_h(p: vec2f) -> f32 { return fract(sin(dot(p, vec2f(127.1, 311.7))) * 43758.5453); }
fn tgw_n2(p: vec2f) -> f32 {
  let i = floor(p); let f = fract(p); let u = f * f * (3.0 - 2.0 * f);
  return mix(mix(tgw_h(i), tgw_h(i + vec2f(1.0, 0.0)), u.x),
             mix(tgw_h(i + vec2f(0.0, 1.0)), tgw_h(i + vec2f(1.0, 1.0)), u.x), u.y);
}
fn tgw_fbm(p: vec2f, oct: i32) -> f32 {
  var v = 0.0; var a = 0.5; var q = p;
  for (var i = 0; i < 5; i++) { if (i >= oct) { break; } v += a * tgw_n2(q); q = q * 2.03 + vec2f(7.0, 3.0); a *= 0.5; }
  return v;
}
fn tgw_box(p: vec3f, b: vec3f) -> f32 { let q = abs(p) - b; return length(max(q, vec3f(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0); }
fn tgw_cylY(p: vec3f, h: f32, r: f32) -> f32 { let d = vec2f(length(p.xz) - r, abs(p.y) - h); return min(max(d.x, d.y), 0.0) + length(max(d, vec2f(0.0))); }
fn tgw_smin(a: f32, b: f32, k: f32) -> f32 { let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0); return mix(b, a, h) - k * h * (1.0 - h); }

const TGW_BELL = vec3f(0.0, 0.62, 0.8);   // bell centre → projects ≈ px(256,316)

// SDF → (dist, matId). 1 stone · 2 silt · 3 bell bronze · 4 pearl · 5 crown ring
fn tgw_map(p: vec3f, rung: f32, pearl: f32, t: f32) -> vec2f {
  var d = 1e5; var m = 1.0;
  // ── the undercroft: back wall + two column rows + a low vault hint ──
  let wall = tgw_box(p - vec3f(0.0, 1.6, -3.4), vec3f(4.6, 2.2, 0.4));
  if (wall < d) { d = wall; m = 1.0; }
  for (var k = 0; k < 4; k++) {
    let fx = select(-1.0, 1.0, (k % 2) == 0) * (1.55 + 0.9 * f32(k / 2));
    let fz = -0.4 - 1.1 * f32(k / 2);
    var col = tgw_cylY(p - vec3f(fx, 1.3, fz), 1.5, 0.26);
    // flutes
    let ang = atan2(p.z - fz, p.x - fx);
    col += 0.012 * sin(ang * 9.0);
    // capital + base
    col = min(col, tgw_box(p - vec3f(fx, 2.72, fz), vec3f(0.36, 0.10, 0.36)));
    col = min(col, tgw_box(p - vec3f(fx, -0.12, fz), vec3f(0.38, 0.12, 0.38)));
    if (col < d) { d = col; m = 1.0; }
  }
  // ── the silt floor, mounded ──
  let floorY = p.y + 0.28 + 0.07 * tgw_fbm(p.xz * 2.2, 3) - 0.05 * smoothstep(1.2, 0.0, length(p.xz - TGW_BELL.xz));
  if (floorY < d) { d = floorY; m = 2.0; }
  // ── THE DROWNED BELL: a great bronze bell, leaning in the silt so its flared
  //    MOUTH shows on the high side — unmistakably a bell, not a boulder ──
  let lean = 0.16 + rung * 0.05;                       // settled lean (+ the toll's nudge)
  let bp0 = p - TGW_BELL;
  let bp = vec3f(bp0.x * cos(lean) - bp0.y * sin(lean), bp0.x * sin(lean) + bp0.y * cos(lean), bp0.z);
  // classic profile by revolution: r grows toward the mouth, flares at the lip
  let q = vec2f(length(bp.xz), bp.y);
  let h01 = clamp((q.y + 0.40) / 1.05, 0.0, 1.0);      // 0 at mouth … 1 at shoulder
  let prof = 0.62 - 0.30 * h01 * h01 + 0.10 * exp(-h01 * 9.0);   // waist + flare
  var bell = max(abs(q.x - prof) - 0.055, abs(q.y - 0.12) - 0.55);   // shell wall
  // the LIP: a heavy torus right at the mouth — the read that says BELL
  let lip = length(vec2f(q.x - (prof + 0.02), q.y + 0.43)) - 0.075;
  bell = min(bell, lip);
  // shoulder cap + the CROWN loop and yoke bar on top
  let cap = length(bp - vec3f(0.0, 0.66, 0.0)) - 0.20;
  bell = tgw_smin(bell, cap, 0.10);
  if (bell < d) { d = bell; m = 3.0; }
  // the CROWN: a bright brass cradle-ring mounted WORLD-UP on the bell (so it
  // reads clean whatever the bell's lean) — held off the shoulder by two posts.
  let btop = TGW_BELL + vec3f(0.0, 0.72, 0.0);
  let cq = p - btop;
  let crown = length(vec2f(length(cq.xz) - 0.16, cq.y)) - 0.04;         // flat ring ø0.32
  if (crown < d) { d = crown; m = 5.0; }
  let post = tgw_cylY(vec3f(abs(cq.x) - 0.16, cq.y + 0.09, cq.z), 0.09, 0.028);
  if (post < d) { d = post; m = 5.0; }
  // ── THE PEARL — seated in the ring once the toll opens it, until taken ──
  if (rung > 0.5 && pearl < 0.5) {
    let pd = length(p - (btop + vec3f(0.0, 0.05, 0.0))) - 0.13;
    if (pd < d) { d = pd; m = 4.0; }
  }
  return vec2f(d, m);
}

fn tgw_nrm(p: vec3f, rung: f32, pearl: f32, t: f32) -> vec3f {
  let e = vec2f(0.006, 0.0);
  return normalize(vec3f(
    tgw_map(p + e.xyy, rung, pearl, t).x - tgw_map(p - e.xyy, rung, pearl, t).x,
    tgw_map(p + e.yxy, rung, pearl, t).x - tgw_map(p - e.yxy, rung, pearl, t).x,
    tgw_map(p + e.yyx, rung, pearl, t).x - tgw_map(p - e.yyx, rung, pearl, t).x));
}
fn tgw_march(ro: vec3f, rd: vec3f, rung: f32, pearl: f32, t: f32) -> vec2f {
  var tt = 0.02;
  for (var i = 0; i < 80; i++) {
    let h = tgw_map(ro + rd * tt, rung, pearl, t);
    if (h.x < 0.004 * max(tt, 1.0)) { return vec2f(tt, h.y); }
    tt += h.x * 0.85; if (tt > 20.0) { break; }
  }
  return vec2f(-1.0, 0.0);
}
fn tgw_ao(p: vec3f, n: vec3f, rung: f32, pearl: f32, t: f32) -> f32 {
  var occ = 0.0; var w = 1.0;
  for (var i = 1; i <= 4; i++) { let hh = 0.05 * f32(i); occ += w * (hh - tgw_map(p + n * hh, rung, pearl, t).x); w *= 0.72; }
  return clamp(1.0 - 2.1 * occ, 0.0, 1.0);
}
fn tgw_alb(m: f32, p: vec3f) -> vec3f {
  if (m < 1.5) {        // old stone — cold, mineral-streaked, tide-stained
    var a = vec3f(0.075, 0.082, 0.090) * (0.75 + 0.5 * tgw_fbm(p.xy * 3.0 + p.zz, 3));
    a *= 1.0 - 0.22 * smoothstep(0.35, 0.5, abs(fract(p.y * 1.8) - 0.5));   // courses
    a = mix(a, vec3f(0.055, 0.075, 0.070), smoothstep(1.6, 0.4, p.y) * 0.6); // tide stain
    return a;
  } else if (m < 2.5) { return vec3f(0.060, 0.056, 0.048) * (0.7 + 0.6 * tgw_fbm(p.xz * 4.0, 3)); }   // silt
  else if (m < 3.5) {   // bell bronze — verdigris blooms, barnacle crust low
    var a = vec3f(0.17, 0.125, 0.06);
    a = mix(a, vec3f(0.07, 0.14, 0.11), smoothstep(0.4, 0.75, tgw_fbm(p.xy * 6.0 + p.zz * 3.0, 3)));
    a = mix(a, vec3f(0.10, 0.095, 0.085), smoothstep(0.45, 0.1, p.y) * 0.5);
    return a;
  } else if (m < 4.5) { return vec3f(0.75, 0.82, 0.85); }   // pearl (lit specially)
  return vec3f(0.55, 0.42, 0.16);                           // crown ring brass — bright
}

fn mod_tg_drowned(pp: vec2f, px: vec2f, t: f32) -> vec3f {
  let mm = vec2f(uni(27), uni(28));
  let tide = clamp(uni(60), 0.0, 1.0);
  let rung = uni(107);
  let pearl = uni(108);
  // the water table: flooded (tide .65) drowns the bell crown; drained (.05) = ankle pools
  let wl = mix(0.02, 1.55, smoothstep(0.05, 0.65, tide));   // flooded stays below the EYE — you stand above the black water
  // ── camera: standing at the foot of the ladder, looking into the quarter ──
  let ro = vec3f(0.0, 1.95, 4.2);
  let uvx = px.x / 256.0 - 1.0;
  let uvy = px.y / 256.0 - 1.0;
  let rd = normalize(vec3f(uvx, -uvy - 0.12, -1.0));
  // ── ambient dark; the trapdoor's light shaft falls from above ──
  var c = vec3f(0.012, 0.020, 0.030);
  let hit = tgw_march(ro, rd, rung, pearl, t);
  if (hit.x > 0.0) {
    let p = ro + rd * hit.x;
    let n = tgw_nrm(p, rung, pearl, t);
    // key: the shaft of day from the trapdoor (cool-silver), angled from high right
    let ldir = normalize(vec3f(0.35, 0.9, 0.2));
    let key = vec3f(0.75, 0.85, 0.95);
    let dif = max(dot(n, ldir), 0.0);
    let ao = tgw_ao(p, n, rung, pearl, t);
    var col = tgw_alb(hit.y, p) * (0.14 + 1.5 * dif) * ao;
    col += vec3f(0.05, 0.075, 0.09) * ao * 0.6;                       // cavern fill
    // the shaft itself brightens what stands in its column (x ≈ 0.9, z ≈ 0.6)
    let inShaft = smoothstep(1.3, 0.25, length(p.xz - vec2f(0.9, 0.6)));
    col += key * inShaft * 0.55 * (0.5 + 0.5 * dif);
    // pearl: lit from within — mother-of-glass
    if (hit.y > 3.5 && hit.y < 4.5) {
      let fres = pow(1.0 - max(dot(-rd, n), 0.0), 2.0);
      col = mix(vec3f(0.55, 0.68, 0.75), vec3f(1.1, 1.05, 0.95), fres);
      col += vec3f(0.5, 0.75, 0.8) * (0.4 + 0.3 * sin(t * 2.2));
    }
    // bell after the toll: a live resonance shiver on the bronze
    if (rung > 0.5 && hit.y > 2.5 && hit.y < 3.5) {
      col += vec3f(0.25, 0.20, 0.10) * (0.5 + 0.5 * sin(t * 9.0 + p.y * 14.0)) * 0.25;
    }
    // the CROWN always gleams — a warm brass so the cradle is never lost in the dark
    if (hit.y > 4.5) {
      col += vec3f(0.45, 0.34, 0.13) * (0.45 + 0.3 * sin(t * 2.0));
    }
    // ── underwater: everything below the water table drinks the light ──
    if (p.y < wl) {
      let depth = clamp((wl - p.y) * 0.8, 0.0, 1.0);
      col *= mix(vec3f(1.0), vec3f(0.28, 0.5, 0.55), depth);
      // caustic shimmer near the surface
      col += vec3f(0.10, 0.22, 0.24) * (1.0 - depth) * tgw_n2(p.xz * 5.0 + vec2f(t * 0.7, 0.0)) * 0.4 * step(0.1, wl - 0.05);
    }
    c = col;
    c = mix(c, vec3f(0.010, 0.016, 0.024), smoothstep(6.0, 16.0, hit.x) * 0.5);
  }
  // ── the water SURFACE: where the view ray crosses the table before a solid ──
  if (rd.y < -0.001 || ro.y > wl) {
    let ts = (wl - ro.y) / rd.y
    ;
    if (ts > 0.0 && (hit.x < 0.0 || ts < hit.x)) {
      let sp = ro + rd * ts;
      if (abs(sp.x) < 4.6 && sp.z > -3.4 && sp.z < 4.4) {
        let rip = tgw_n2(sp.xz * 3.0 + vec2f(t * 0.5, t * 0.3)) - 0.5;
        let fres = pow(1.0 - max(dot(-rd, vec3f(rip * 0.3, 1.0, rip * 0.3)), 0.0), 2.0);
        var wcol = mix(vec3f(0.020, 0.045, 0.055), vec3f(0.10, 0.16, 0.19), fres);
        wcol += vec3f(0.35, 0.5, 0.55) * smoothstep(0.42, 0.5, rip) * 0.25;     // glints
        // THE DROWNED GLIMMER: while flooded, the bell's crown gleams below —
        // the "something down there" that pulls you to find the valve
        if (wl > 0.8) {
          let over = length(sp.xz - TGW_BELL.xz);
          wcol += vec3f(0.35, 0.65, 0.6) * smoothstep(0.9, 0.1, over) * (0.5 + 0.4 * sin(t * 1.8)) * 0.8;
        }
        c = mix(c, wcol, 0.82);
        // surface meets stone: a faint bathtub line
        c += vec3f(0.30, 0.42, 0.45) * smoothstep(0.05, 0.0, abs(rd.y * (hit.x - ts))) * 0.15;
      }
    }
  }
  // ── drained: wet-glisten pools in the silt hollows ──
  if (wl < 0.4) {
    let g = smoothstep(0.55, 0.85, tgw_fbm(px * 0.02 + vec2f(3.0, 7.0), 3));
    c += vec3f(0.10, 0.16, 0.18) * g * smoothstep(330.0, 470.0, px.y) * 0.5;
  }
  // ── affordances (screen-space light only) ──
  let bc = vec2f(256.0, 316.0);
  let hovB = smoothstep(84.0, 34.0, length(mm - bc));
  if (wl < 0.4 && rung < 0.5) {
    c += vec3f(0.85, 0.70, 0.35) * tg_ring(px, bc, 64.0, 58.0) * (0.25 + 0.3 * sin(t * 2.2)) * (0.5 + 0.5 * hovB);
  }
  if (rung > 0.5 && pearl < 0.5) {
    let pc = vec2f(256.0, 266.0);   // over the pearl where it truly projects
    c += vec3f(0.65, 0.85, 0.9) * tg_ring(px, pc, 34.0, 28.0) * (0.3 + 0.3 * sin(t * 2.8)) * (0.5 + 0.5 * smoothstep(52.0, 22.0, length(mm - pc)));
  }
  // click-confirm ripple
  let cfx = uni(84);
  if (cfx > 0.003) { let cd = length(px - vec2f(uni(85), uni(86))); c += vec3f(0.8, 0.85, 0.8) * exp(-pow((cd - (1.0 - cfx) * 55.0) * 0.25, 2.0)) * cfx * 0.4; }
  return c;
}

— STEP HOOKS (JAVASCRIPT) —

hook · tideglass_core

tideglass core — Act-I finale plays once & holds on the stay-tuned card (no replay)

try {
  const wd = sim.worldData
  // ── R-RESET (Galen Jul 31): on a cc-reset reload the engine sets wd.__fresh
  //    (space path sets it ONLY on reset). The host owns __fresh — a hook reply
  //    can't clear it — so latch once per session. The server autosave restore
  //    lands ~1s later and would resurrect the run we just wiped ("resets a
  //    moment then the save comes back"), so hold a wipe window: a RESTORED run
  //    (its clock is far ahead of a fresh one) appearing inside the window is
  //    wiped too; the autosave then persists the fresh start. ONLY wd.save.tg —
  //    this world's branch — is touched; nothing else in the player's save.
  //    NEVER delete a __key here: a deletion does not propagate back to the host
  //    (the reply applies only PRESENT keys), so a deleted window key leaves the
  //    host stuck at its last positive value → the window never closes → any run
  //    older than the age gate gets wiped forever (the "random resets", Jul 31).
  //    Set 0 instead — zero round-trips fine.
  if (wd.__fresh && !wd.__tgFreshSeen) { wd.__tgFreshSeen = 1; wd.__tgWipe = 12 }
  else if (!wd.__fresh && wd.__tgFreshSeen) wd.__tgFreshSeen = 0   // no reset this session → clear the latch (a snapshot capture may carry a stale 1, which would silently eat the NEXT legit R)
  if (typeof wd.__tgWipe === 'number' && wd.__tgWipe > 0) {
    // wipe ONLY while __fresh is present (reset sessions — it is stripped on every
    // load, so an inherited window can never fire) and only in the window's first
    // seconds (a hot-reload merge into a long session can't tail-gate a wipe).
    // the WHOLE wd.save is this world's slot (tideglass:__autosave) — wipe it all:
    // trigger/edge latches (__trig/__edge) and chapters live BESIDE tg, and a
    // surviving 'tg-lens' latch bricks the lens on the next run (Galen, Jul 31)
    if (wd.__fresh && wd.save && wd.save.tg && (wd.__tgWipe >= 12 || (wd.__tgWipe > 8 && (wd.save.tg.t || 0) > 20))) wd.save = {}
    wd.__tgWipe = Math.max(0, wd.__tgWipe - dt)
  }
  // ═══ PER-PLAYER SAVES (cutover v2 — sandbox 'save' wire deployed 3f27555):
  //     progress lives in wd.save.tg, engine-persisted per-user/per-anon.
  wd.save = wd.save || {}
  if (!wd.save.tg || wd.save.tg.v !== 2) wd.save.tg = {
    v: 2, view: 0, pv: 0, fade: 0, t: 0,
    dials: [0, 0, 0, 0], pipes: [0, 0, 0, 0, 0],
    door: 0, organ: 0, star: 0, lensA: 0, hold: 0, fin: 0, night: 0, vault: 0,
    clickP: 0, flash: [0, 0, 0, 0], lastToll: -1, act1music: false, musicAct: 0
  }
  const G = wd.save.tg
  if (typeof G.kilnBloom !== "number") G.kilnBloom = 0
  if (typeof G.sluiceOpen !== "number") G.sluiceOpen = 0   // the freed wheel is a TIDE VALVE
  if (typeof G.bellRung !== "number") G.bellRung = 0
  if (typeof G.pearl !== "number") G.pearl = 0
  if (typeof G.pearlSet !== "number") G.pearlSet = 0          // pearl seated in the cave receptacle
  if (typeof G.crystalPowered !== "number") G.crystalPowered = 0  // the airship's crystal, lit by the beam
  if (typeof G.wheelAnim !== "number") G.wheelAnim = 0
  if (typeof G.summon !== "number") G.summon = 0              // summoning cinematic clock 0..1 (Aug 10)
  if (typeof G.airshipArrived !== "number") G.airshipArrived = 0
  if (typeof G.obeliskOpen !== "number") G.obeliskOpen = 0
  if (typeof G.obeliskUnlocked !== "number") G.obeliskUnlocked = 0
  if (typeof G.summonMigrated !== "number") G.summonMigrated = 0
  if (typeof G.flyCine !== "number") G.flyCine = 0            // Act-I finale cinematic timeline (0 off · 0..1 playing · 1 = card)
  const inSeat = (id) => { const s = G.seats || []; return s[0] === id || s[1] === id || s[2] === id; };
  G.t += dt
  // the observatory's constellation puzzle (view 5) — folded in from LATTICE
  if (!Array.isArray(G.lat)) { G.lat = [0, 0, 0, 0, 0]; G.latBloom = 0 }
  // THE CORE (view 6) — reached only after the constellation locks gold. Three
  // rings of light; click a ring to turn its notch 30°; land all three at the
  // top gate to wake the core. Steps 0..11; solved when all are 0 (notch up).
  if (!Array.isArray(G.core)) { G.core = [5, 8, 2]; G.coreSolved = 0; G.coreGlow = 0 }
  // THE ORRERY (view 7) — the deepest room, opened by the woken core. Grab the
  // sun-bead (outer ring) or the moon-bead (inner) and swing it round; set the sun
  // BELOW the horizon and the moon ABOVE — the finale sky — and hold. The spring
  // tide the island remembers wakes the dawn. Answer: sunA=π (down), moonA=0 (up).
  if (typeof G.sunA !== 'number') { G.sunA = 2.5; G.moonA = -2.3; G.orrerySolved = 0; G.orreryGlow = 0; G.orreryHold = 0; G.tide = 0; G.dawn = 0; G.grab = null }
  // THE SEA-CAVE (view 8) — a bioluminescent grotto in the rock, LEFT off the
  // shore (left chevron, always open). Not a puzzle — a place to wander.

  // ── ACT 1 v2 state (additive guard — does NOT reset existing progress) ──
  if (typeof G.gate2 === 'undefined') {
    G.gate2 = false      // the drowned-gate combo (B) is dialed at low tide, notches down
    G.itemA = false      // legacy spanner flag, kept as a mirror of inv.spanner (old saves)
    G.act = 1            // the island is Act 1; the flight opens Act 2
  }
  if (typeof G.inv === 'undefined') {
    G.inv = { spanner: false, gear8: false, gear12: false, gear16: false, gear20: false }
    G.held = 0                       // 0 none ·1 spanner ·2 g8 ·3 g12 ·4 g16 ·5 g20
    G.seats = [0, 0, 0]              // gear id per engine seat (0 empty; 2..5)
    G.notch = 0                      // 0 = UP/12 o'clock · 1 = DOWN/6 o'clock (drowned gate)
    G.moons = [{ el: 'glass', omega: 0.06, amp: 1.0, phase: 0, hue: 0, risen: 1, alt: 1 }]
    G.activeMoon = -1                // -1 = none clicked (natural argmax); else moon index
    G.tideLevel = 1                  // current tide 0..1 from tide()
    G.tideBaseline = 0               // climbs each act (the world floods a little)
    G.thicketBurn = 0                // 0 impassable · 1 burned open (fire moon)
    G.engineOK = 0                   // gear train hits target cadence + direction
    G.enginePhase = 0                // crank phase accumulator (re-gearing never pops)
    G.fly = 0                        // helm pulled — act-boundary anim
    G.fireMoon = false               // Mirror Island grants it (section 13)
  }
  if (G.itemA && !G.inv.spanner) G.inv.spanner = true   // migrate the old spanner flag
  // GALEN'S RULE (Aug 10, overriding the earlier grandfather): the airship comes
  // ONLY from the kiln bloom's summons — for everyone, old saves included. The
  // one-shot reversal below un-grandfathers any save the deleted forward pass
  // touched (flag 1 = migrated without a real summon; nobody with flag 1 can
  // have legitimately summoned, since summon=1 blocked the trigger).
  if (G.summonMigrated === 1) { G.airshipArrived = 0; G.summon = 0; G.summonMigrated = 2 }
  if (G.act >= 2 && !G.crystalPowered) G.crystalPowered = 1   // a save that flew Act I before the pearl-beam existed keeps its power — the new chain gates FRESH runs, it doesn't brick earned wheels (Galen Jul 31)
  // STALE-LATCH HEAL (Galen Jul 31): an earlier reset wiped save.tg but left
  // __trig beside it — a latch without its truth-state can never fire again
  // (the bricked lens). Truth and latch are set in the same tick, so a latch
  // whose truth is absent is ALWAYS stale: clear it and the puzzle works again.
  if (wd.save.__trig) {
    const T = wd.save.__trig
    if (T['tg-lens'] && !(G.fin > 0)) delete T['tg-lens']
    if (T['tg-organ'] && !(G.organ >= 1)) delete T['tg-organ']
    if (T['tg-gate'] && !G.gateChaptered) delete T['tg-gate']
    if (T['tg-orrery'] && !G.orrerySolved) delete T['tg-orrery']
    if (T['tg-lattice'] && !G.latSolved) delete T['tg-lattice']
  }

  sim.defineChapters(['THE SHORE', 'THE GATE', 'THE ORGAN', 'THE LIGHT'])

  // pin the canvas
  for (const f of sim.fields.values()) {
    if ((f.name || '') === 'Tideglass') { const T = f.transform; T.x = 256; T.y = 256; T.vx = 0; T.vy = 0 }
  }

  const ANSWER_DIALS = [2, 0, 3, 1]      // the toll order (opens the door)
  // ── ACT 1 v2 LOCKED puzzle constants (unit-tested: scenes/tideglass-tests.mjs) ──
  const MIRROR_MAP = [1, 0, 3, 2]        // vertical-mirror glyph pairs (an involution)
  const COMBO_B = [1, 3, 0, 2]           // the drowned-gate combo (dial @6 o'clock, low tide)
  const CAVE_GLYPHS = [0, 2, 1, 3]       // = mirror(COMBO_B); the carving the cave pool reflects
  const LOW_THRESH = 0.25                // tide below this = the drowned gate is engageable
  const FIXED_PINION = [10, 12, 8]       // per-seat fixed pinion teeth (product 960)
  const GEAR_TEETH = { 2: 8, 3: 12, 4: 16, 5: 20 }   // item id -> tooth count
  const CRANK_RATE = 1.0                 // boiler crank turns/sec, CW
  const TARGET_CADENCE = 0.5             // shaft: one turn per two bell strikes
  const TARGET_DIR = -1                  // CCW after 3 meshes
  const CAD_TOL = 0.02                   // 2% relative tolerance on the cadence match
  const MOON_TABLE = { fire: { el: 'fire', omega: 0.09, amp: 0.6, phase: 1.7, hue: 0.05, risen: 1, alt: 0 } }
  const ANSWER_PIPES = [3, 1, 4, 2, 0]   // the stele columns (ticks - 1)
  const TONES = [220.0, 293.66, 329.63, 392.0]           // glyph voices
  const PIPE_TONES = [146.83, 164.81, 196.0, 220.0, 246.94]
  const sounds = []
  // ═══ SCENE TABLE v1 (Galen Jul 30): edge exits are DATA — one table drives the
  //     hit-zones AND the drawn chevrons (uni 88..103). Diegetic doors stay coded. ═══
  // INSPECT MODE (Galen Jul 30): press I — every click gets documented on a HUD
  //   console: coords · view · the named node/zone it hit · owning module · saved.
  const VIEWN = { 0:['SHORE','tg_views'],1:['GATE','tg_views'],2:['ORGAN HALL','tg_views'],3:['LENS','tg_views'],4:['TABLET','tg_views'],5:['CONSTELLATION','tg_views'],6:['CORE','tg_views2'],7:['ORRERY','tg_views2'],8:['SEA-CAVE','tg_views2'],9:['DECK','tg_deck'],10:['ENGINE','tg_engine'],11:['MIRROR ISLE','tg_mirror'],12:['THICKET','tg_air'],13:['DROWNED','tg_drowned'],14:['KILN','tg_kiln'] }
  const ZONES = {
    0: [[400,96,40,'moon (ruling)'],[338,62,28,'moon (other)'],[108,128,92,'airship (summoned)'],[214,318,80,'dome (vault-gated)'],[440,300,62,'gate building']],
    1: [[156,409,30,'dial 1'],[222,409,30,'dial 2'],[289,409,30,'dial 3'],[355,409,30,'dial 4'],[432,360,26,'notch control (12↔6)'],[256,261,140,'the doorway (enter when open)']],
    2: [[131,300,32,'pipe 1'],[193,300,32,'pipe 2'],[256,300,32,'pipe 3'],[319,300,32,'pipe 4'],[381,300,32,'pipe 5']],
    5: [[256,256,46,'constellation heart (latSolved-gated)']],
    6: [[256,256,50,'core heart (coreSolved-gated)']],
    8: [[150,420,36,'8t gear plank'],[256,300,46,'the tideglass socket (seat the pearl → the beam fires)'],[430,300,50,'the obelisk'],[430,300,36,'obelisk slot — the spanner']],
    9: [[432,256,44,'hatch → engine'],[256,120,52,'helm (engineOK-gated)']],
    10: [[176,256,46,'gear seat 1'],[256,256,46,'gear seat 2'],[336,256,46,'gear seat 3'],[90,404,38,'seized 12t gear (needs spanner held)']],
    11: [[346,250,54,'sluice wheel — spanner frees it; then the TIDE VALVE (click = open/close)']],
    13: [[256,316,70,'the drowned bell (needs the sluice open — low water)'],[256,266,38,'the pearl (after the bell tolls)']],
    14: [[256,290,60,'kiln mouth — anneal the glass bloom']],
  }
  const NAV = {
    0: [{ x: 28, y: 256, d: 1, to: 8 }, { x: 487, y: 256, d: 0, to: 1 }, { x: 108, y: 128, d: 2, to: 9, when: () => !!G.airshipArrived }],
    1: [{ x: 25, y: 256, d: 1, to: 0 }, { x: 487, y: 256, d: 0, to: 4 }, { x: 256, y: 210, d: 3, to: 13, when: () => !!G.gate2 }],
    2: [{ x: 25, y: 470, d: 1, to: 1 }, { x: 256, y: 30, d: 2, to: 3, when: () => G.organ > 0.5 }],
    3: [{ x: 60, y: 470, d: 3, to: 2 }, { x: 487, y: 256, d: 0, to: 12 }],
    4: [{ x: 25, y: 256, d: 1, to: 1 }, { x: 487, y: 256, d: 0, to: 12 }],
    5: [{ x: 25, y: 256, d: 1, to: 0 }],
    6: [{ x: 25, y: 256, d: 1, to: 5 }],
    7: [{ x: 25, y: 256, d: 1, to: 6 }],
    8: [{ x: 487, y: 256, d: 0, to: 0 }, { x: 25, y: 256, d: 1, to: 11 }],
    9: [{ x: 25, y: 256, d: 1, to: 0 }, { x: 432, y: 256, d: 3, to: 10, when: () => G.held === 1 }],
    10: [{ x: 25, y: 256, d: 1, to: 9 }],
    11: [{ x: 487, y: 256, d: 0, to: 8 }],
    12: [{ x: 25, y: 256, d: 1, to: 4 }, { x: 487, y: 256, d: 0, to: 14, when: () => (G.thicketBurn || 0) > 0.6 }],
    13: [{ x: 256, y: 40, d: 2, to: 1 }],
    14: [{ x: 25, y: 256, d: 1, to: 12 }],
  }

  const mx = wd.mouse_x, my = wd.mouse_y
  const hasMouse = typeof mx === 'number' && typeof my === 'number'
  const inView = v => G.view === v && G.fade < 0.35

  // ── travel ──
  const go = v => { if (v === G.view) return; G.pv = G.view; G.view = v; G.fade = 1; sounds.push({ frequency: 90, duration: 0.35, volume: 0.18, type: 'sine' }) }
  G.fade = Math.max(0, G.fade - dt * 1.8)

  // ── ACT 1 v2 hook-local helpers (G stores only plain data — these are its "methods";
  //    functions can NOT live on G because G is serialized into worldData). ──
  const mirror = arr => arr.map(g => MIRROR_MAP[g])
  const dialsMatch = target => G.dials.every((v, i) => v === target[i])
  const tide = () => {
    // pre-dawn the tide sits high (natural); AFTER dawn the orrery moon-handle goes
    // live and drives moon 1's altitude — drag the moon low to pull the tide out (§2).
    // pre-dawn the glass moon still ORBITS, so the tide ebbs and flows on a ~20s cycle —
    // wait for the ebb (moonHeight → 0) to engage the drowned gate. After dawn the orrery
    // moon-handle takes over. (Fix: pinning moonHeight=1 pre-dawn made low tide unreachable.)
    const moonHeight = ((G.dawn || 0) > 0.3) ? (1 + Math.cos(G.moonA || 0)) / 2 : (0.5 + 0.5 * Math.cos((G.t || 0) * 0.32))
    if (G.moons[0]) G.moons[0].alt = moonHeight
    let s = G.tideBaseline || 0
    for (const m of G.moons) s += (m.amp || 0) * (m.alt || 0)
    return Math.max(0, Math.min(1, s))
  }
  const dominantMoon = () => {
    if (G.activeMoon >= 0) return G.activeMoon
    let best = 0, bestAlt = -2
    for (let i = 0; i < G.moons.length; i++) { const a = G.moons[i].alt || 0; if (a > bestAlt) { bestAlt = a; best = i } }
    return best
  }
  const raiseMoon = spec => { G.moons.push(spec) }
  const onActComplete = n => {
    const next = MOON_TABLE[n + 1]                          // Act 2+ moons arrive as acts ship
    if (next && !G.moons.some(m => m.el === next.el)) raiseMoon(next)
    G.act = n + 1
    G.tideBaseline = Math.min(0.6, (G.tideBaseline || 0) + 0.12)   // the water climbs as you leave
  }
  const gearCadence = seats => {
    let r = CRANK_RATE, meshes = 0
    for (let i = 0; i < 3; i++) {
      const id = seats[i]
      if (!id) return { cadence: 0, dir: 0, full: false }   // an empty seat: the train is broken
      r *= FIXED_PINION[i] / GEAR_TEETH[id]
      meshes++
    }
    return { cadence: r, dir: Math.pow(-1, meshes), full: true }
  }
  // THE TIDE VALVE (Galen Jul 31): the freed sluice wheel DRIVES the tide.
  // Valve open → the island drains to low water; closed → the sea stands high.
  // Legible cause → effect (the old ambient tide cycle stayed deleted for good).
  // Progression is untouched: tideLow stays const true for the shipped gates;
  // the NEW dependency is the drowned quarter's floodwater, read from U[60].
  {
    const tideTarget = (G.inv.gear20 && G.sluiceOpen) ? 0.05 : 0.65
    G.tideLevel = (G.tideLevel ?? 0.65) + (tideTarget - (G.tideLevel ?? 0.65)) * Math.min(1, dt * 0.5)
  }
  const tideLow = true                 // BONES: low-tide requirement deleted (never legible to players)

  // ── the bells: cycle 11s, four tolls in the answer order ──
  const cyc = G.t % 11
  const slot = Math.floor((cyc - 1.0) / 2.2)
  const inToll = cyc >= 1.0 && (cyc - 1.0) % 2.2 < 0.1
  if (inToll && slot >= 0 && slot < 4 && G.lastToll !== slot) {
    G.lastToll = slot
    const g = ANSWER_DIALS[slot]                       // which buoy speaks
    G.flash[g] = 1
    const vol = inView(0) ? 0.5 : (inView(1) ? 0.16 : 0.05)
    if (vol > 0.06 || G.view === 0) sounds.push(
      { frequency: TONES[g], duration: 1.7, volume: vol, type: 'sine' },
      { frequency: TONES[g] * 2.0, duration: 0.9, volume: vol * 0.3, type: 'sine' })
  }
  if (cyc < 1.0) G.lastToll = -1
  for (let i = 0; i < 4; i++) G.flash[i] = Math.max(0, G.flash[i] - dt * 0.8)

  // ── clicks ──
  G.clickP = Math.max(0, G.clickP - dt * 2.2)
  G.wheelAnim = Math.max(0, (G.wheelAnim || 0) - dt * 0.8)
  if (G.flyCine > 0 && G.flyCine < 1) G.flyCine = Math.min(1, G.flyCine + dt / 10)   // the crossing plays ~10s, then HOLDS on the Act I card   // the valve turn animation
  if (sim.edge('tg-inspect', !!wd.key_i)) { G.inspect = !G.inspect; G.clickLog = G.clickLog || []; sounds.push({ frequency: G.inspect ? 880 : 440, duration: 0.12, volume: 0.2, type: 'sine' }) }
  if (G.dawnCine) G.dawnCine = Math.max(0, G.dawnCine - dt)
  if (hasMouse && sim.edge('tg-click', !!wd.mouse_down)) {
    G.clickP = 1
    const hit = (x, y, r) => Math.hypot(mx - x, my - y) < r
    const deny = (x, y) => { G.deny = { x, y, t: 1 }; sounds.push({ frequency: 82, duration: 0.18, volume: 0.3, type: 'square' }) }   // a locked thing ANSWERS the click
    // INSPECTOR CHANNEL (Galen Jul 30): every click is recorded — the AI reads
    // G.clicks over the bridge and decodes which shader block owns those pixels.
    G.clicks = G.clicks || []
    G.clicks.push({ x: Math.round(mx), y: Math.round(my), v: G.view, t: Math.round(G.t) })
    if (G.clicks.length > 8) G.clicks.shift()
    wd.__clicks = G.clicks   // mirror world-global: the AI reads clicks over the bridge
    G.clickFx = { x: mx, y: my, t: 1 }   // the game CONFIRMS it heard the click (Galen)
    if (G.inspect) {
      const vi = VIEWN[G.view] || ['?', '?']
      let what = null
      for (const e of (NAV[G.view] || [])) { if (hit(e.x, e.y, 45)) { what = 'chevron → ' + (VIEWN[e.to] || ['?'])[0] + ((!e.when || e.when()) ? '' : ' (locked)'); break } }
      if (!what) { for (const z of (ZONES[G.view] || [])) { if (hit(z[0], z[1], z[2])) { what = z[3]; break } } }
      if (!what && Math.abs(my - 490) < 15 && mx > 100 && mx < 412) what = 'satchel strip'
      if (!what) what = 'empty ground'
      G.clickLog = G.clickLog || []
      G.clickLog.push('(' + Math.round(mx) + ',' + Math.round(my) + ') ' + vi[0] + ' · ' + what + ' · ' + vi[1] + ' · saved ✓')
      if (G.clickLog.length > 6) G.clickLog.shift()
    }

    // ── THE SATCHEL: click a slot to take/replace the held item (any view). Slots at
    //    x 128/192/256/320/384, y 486; ids 1 spanner ·2 g8 ·3 g12 ·4 g16 ·5 g20. ──
    let satchelClicked = false
    if (!G.inspect && Math.abs(my - 490) < 15 && mx > 100 && mx < 412) {   // tight band: clears the y=470 trapdoor
      const slotX = [128, 192, 256, 320, 384]
      const owned = [G.inv.spanner, G.inv.gear8 && !inSeat(2), G.inv.gear12 && !inSeat(3), G.inv.gear16 && !inSeat(4), G.inv.gear20 && !inSeat(5)]
      for (let s = 0; s < 5; s++) {
        if (Math.abs(mx - slotX[s]) < 26 && owned[s]) {
          satchelClicked = true
          G.held = (G.held === s + 1) ? 0 : s + 1
          sounds.push({ frequency: 330 + s * 30, duration: 0.16, volume: 0.22, type: 'triangle' })
          break
        }
      }
    }
    if (satchelClicked) { /* consumed by the satchel — no world click this frame */ }
    else if (G.inspect) { /* INSPECT MODE: clicks are documentation, never gameplay (Galen) */ }
    else if (G.dawnCine > 0) { /* dawn transition: every chevron and zone is locked until the sky settles */ }
    else if (G.flyCine > 0) { /* Act-I finale plays once, then HOLDS on the stay-tuned card — no click-dismiss, no replay (Galen: stop with just text) */ }   // the FINALE owns the screen: mid-flight clicks pass like wind; a tap on the Act I card returns you to the deck
    else if ((NAV[G.view] || []).some(e => (!e.when || e.when()) && hit(e.x, e.y, 45) && (go(e.to) || true))) { /* the table took the click */ }
    else if (inView(0)) {
      // MUTUALLY EXCLUSIVE (else-if) — the dome zone (214,318) sits below the
      // building/right-chevron zones. The right chevron (487,256) and building
      // (440,300) both mean "into the gate", so they share one branch; the dome
      // (constellation) wins only when it's actually risen and clicked.
      if (!!G.airshipArrived && hit(108, 128, 92)) go(9)           // the moored airship (top-left) → its deck
      else if (G.vault > 0.03 && hit(214, 318, 80)) go(5)           // the risen dome → the constellation room
      else if (hit(440, 300, 62)) go(1)                             // the gate building (diegetic; right-edge chevron is table-driven)
      if (hit(400, 96, 40)) { G.activeMoon = ((G.activeMoon + 2) % (G.moons.length + 1)) - 1; sounds.push({ frequency: 528 + Math.max(0, G.activeMoon) * 66, duration: 0.5, volume: 0.22, type: 'sine' }) }   // the RULING moon — cycle rule
      else if (G.moons.length > 1 && hit(338, 62, 28)) { const curM = dominantMoon(); G.activeMoon = G.moons.findIndex((m, i) => i !== curM); sounds.push({ frequency: 594, duration: 0.5, volume: 0.22, type: 'sine' }) }   // the OTHER moon — click it to hand it the rule
    } else if (inView(5)) {
      if (G.latSolved && hit(256, 256, 46)) go(6) // the gold heart → dive into the orb
    } else if (inView(6)) {
      if (G.coreSolved && (G.coreGlow || 0) > 0.9 && hit(256, 256, 50)) go(7)  // the woken heart → the orrery
      else if (!G.coreSolved && hasMouse) {
        // pick a ring by how far the click is from the centre, then turn its
        // notch one step (30°). Bands match the draw radii (0.30/0.55/0.80).
        const rn = Math.hypot(mx - 256, my - 256) / 256
        let ring = -1
        if (rn < 0.425) ring = 0
        else if (rn < 0.675) ring = 1
        else if (rn < 0.98) ring = 2
        if (ring >= 0) {
          G.core[ring] = (G.core[ring] + 1) % 12
          sounds.push({ frequency: 330 + ring * 110, duration: 0.18, volume: 0.28, type: 'triangle' })
          if (G.core[0] === 0 && G.core[1] === 0 && G.core[2] === 0) {
            G.coreSolved = 1                             // all three notches at the gate — the core wakes
            sounds.push({ frequency: 264, duration: 2.0, volume: 0.40, type: 'sine' },
                        { frequency: 396, duration: 2.0, volume: 0.34, type: 'sine' },
                        { frequency: 528, duration: 2.4, volume: 0.30, type: 'sine' })
          }
        }
      }
    } else if (inView(1)) {
      if (hit(432, 360, 26)) { G.notch = G.notch ? 0 : 1; sounds.push({ frequency: 120 + G.notch * 70, duration: 0.4, volume: 0.3, type: 'square' }) }   // swing the notch targets 12 o'clock <-> 6 o'clock
      if (hit(256, 261, 140)) { if (G.gate2) { go(13) } else if (G.door > 0.9) { go(2) } else { deny(256, 261) } }   // the WHOLE doorway is the door (Galen's inspector clicks found the dead center); shut → it answers
      // dials at grid (156,409) (222,409) (288,409) (354,409)
      // dials always turn — you can dial in NEW combinations even after the gate
      // has settled open (the door stays open once first solved; a re-match re-chimes)
      for (let k = 0; k < 4; k++) {
        if (hit(156 + k * 66.5, 409, 30)) {
          G.dials[k] = (G.dials[k] + 1) % 4
          sounds.push({ frequency: TONES[G.dials[k]], duration: 0.35, volume: 0.3, type: 'triangle' })
          // the gate is a live lock: combo A opens the door, combo B opens the
          // trapdoor, any wrong code throws the bolts. Sounds fire on transitions.
          const mA = ANSWER_DIALS.every((v, i) => G.dials[i] === v)
          const mB = dialsMatch(COMBO_B) && tideLow && G.notch === 1     // the real drowned-gate open
          if (mA && !G.wasMatchA) sounds.push({ frequency: 55, duration: 2.2, volume: 0.7, type: 'sine' },
                                              { frequency: 110, duration: 1.6, volume: 0.35, type: 'sine' },
                                              { frequency: TONES[0], duration: 1.8, volume: 0.2, type: 'sine' })
          if (!mA && G.wasMatchA) sounds.push({ frequency: 70, duration: 0.7, volume: 0.4, type: 'sine' })   // the bolts throw — the door closes
          // combo-B feedback is the drowned-gate shudder (a wrong-but-down code at low
          // tide sheds silt, rendered), not a typed-equality chime; only the true open rings
          if (mB && !G.wasMatchB) sounds.push({ frequency: 174.61, duration: 1.8, volume: 0.5, type: 'sine' },
                                              { frequency: 261.63, duration: 1.6, volume: 0.35, type: 'sine' })
          G.wasMatchA = mA; G.wasMatchB = mB
        }
      }
    } else if (inView(2)) {
      // pipes at x 130.5 + k*62.7, wide zone
      if (G.organ < 0.5) for (let k = 0; k < 5; k++) {
        const cx = 130.5 + k * 62.7
        if (Math.abs(mx - cx) < 28 && my > 160 && my < 430) {
          G.pipes[k] = (G.pipes[k] + 1) % 5
          sounds.push({ frequency: PIPE_TONES[G.pipes[k]], duration: 0.8, volume: 0.35, type: 'triangle' },
                      { frequency: PIPE_TONES[G.pipes[k]] * 2, duration: 0.4, volume: 0.10, type: 'sine' })
          if (ANSWER_PIPES.every((v, i) => G.pipes[i] === v)) {
            sounds.push({ frequency: 146.83, duration: 2.6, volume: 0.5, type: 'sine' },
                        { frequency: 220.0, duration: 2.6, volume: 0.4, type: 'sine' },
                        { frequency: 293.66, duration: 2.6, volume: 0.35, type: 'sine' },
                        { frequency: 369.99, duration: 2.6, volume: 0.3, type: 'sine' })
            G.organGo = true
          }
        }
      }
    } else if (inView(3)) {
    } else if (inView(4)) {
    } else if (inView(7)) {
    } else if (inView(8)) {
      if (!G.inv.gear8 && hit(150, 420, 36)) { G.inv.gear8 = true; sounds.push({ frequency: 523.25, duration: 0.4, volume: 0.32, type: 'triangle' }, { frequency: 784.0, duration: 0.25, volume: 0.2, type: 'sine' }) }   // the creaking plank lifts → the 8t gear
      else if (G.pearl && !G.pearlSet && hit(256, 300, 46)) { G.pearlSet = 1; G.crystalPowered = 1; sounds.push({ frequency: 174.61, duration: 1.4, volume: 0.4, type: 'sine' }, { frequency: 880.0, duration: 0.5, volume: 0.3, type: 'sawtooth' }, { frequency: 1760.0, duration: 1.6, volume: 0.22, type: 'sine' }) }   // seat the pearl → the socket wakes and fires the beam up to the airship's crystal
      else if (!G.pearl && !G.pearlSet && hit(256, 300, 46)) { deny(256, 300); sounds.push({ frequency: 130.81, duration: 0.4, volume: 0.2, type: 'sine' }) }   // the empty socket — it waits for the tideglass pearl
      else if (!G.obeliskUnlocked && hit(430, 300, 50)) { G.obeliskUnlocked = 1; sounds.push({ frequency: 55, duration: 1.2, volume: 0.3, type: 'sawtooth' }, { frequency: 73.42, duration: 1.2, volume: 0.25, type: 'sawtooth' }) }   // the obelisk grinds — its slot leaves withdraw
      else if (G.obeliskOpen > 0.8 && !G.inv.spanner && hit(430, 300, 36)) { G.inv.spanner = true; G.itemA = true; sounds.push({ frequency: 523.25, duration: 0.3, volume: 0.35, type: 'triangle' }, { frequency: 784.0, duration: 0.25, volume: 0.2, type: 'sine' }, { frequency: 98.0, duration: 0.8, volume: 0.25, type: 'sine' }) }   // take the spanner from the obelisk slot
    } else if (inView(9)) {                           // AIRSHIP DECK
      if (hit(432, 256, 44)) { deny(432, 256) }   // the hatch opens only with the SPANNER HELD — the nav chevron (shown when held) takes it; otherwise deny
      else if (!G.engineOK && hit(256, 120, 52)) { deny(256, 120) }
      else if (G.engineOK && !G.crystalPowered && hit(256, 120, 52)) { deny(256, 120); sounds.push({ frequency: 87.31, duration: 0.6, volume: 0.24, type: 'sine' }) }   // the gears turn but the ship is dark — the crystal beneath the hull needs the pearl's beam
      else if (G.act < 2 && G.engineOK && G.crystalPowered && hit(256, 120, 52)) { G.fly = 1; G.flyCine = 0.001; if (G.act < 2) onActComplete(1); sounds.push({ frequency: 130.81, duration: 2.4, volume: 0.5, type: 'sine' }, { frequency: 196.0, duration: 2.4, volume: 0.35, type: 'sine' }, { frequency: 261.63, duration: 2.8, volume: 0.25, type: 'sine' }) }   // take the wheel — THE CROSSING plays (act advances only the first time; the wheel always flies)

    } else if (inView(10)) {                          // ENGINE ROOM — THE GEARHEART
      if (!G.inv.gear12 && G.held !== 1 && hit(90, 404, 38)) { deny(90, 404) }   // seized — needs the spanner HELD
      else if (!G.inv.gear12 && G.held === 1 && hit(90, 404, 38)) { G.inv.gear12 = true; sounds.push({ frequency: 196.0, duration: 0.5, volume: 0.35, type: 'sawtooth' }, { frequency: 392.0, duration: 0.3, volume: 0.2, type: 'sine' }) }   // the spanner frees the 12t gear seized in old grease
      else {
        // the three gear seats: drop a held gear in (empty hand + filled seat lifts it back out)
        const seatX = [176, 256, 336]
        for (let s = 0; s < 3; s++) {
          if (hit(seatX[s], 256, 46)) {
            if (G.seats[s] === 0 && G.held < 2) { deny(seatX[s], 256) }   // an empty seat wants a HELD gear
            else if (G.seats[s] === 0 && G.held >= 2) { G.seats[s] = G.held; G.held = 0; sounds.push({ frequency: 220 + s * 40, duration: 0.3, volume: 0.3, type: 'triangle' }) }
            else if (G.seats[s] !== 0 && G.held === 0) { G.held = G.seats[s]; G.seats[s] = 0; sounds.push({ frequency: 180 + s * 40, duration: 0.25, volume: 0.25, type: 'triangle' }) }
            const gc = gearCadence(G.seats)
            const ok = gc.full && Math.abs(gc.cadence - TARGET_CADENCE) <= TARGET_CADENCE * CAD_TOL && gc.dir === TARGET_DIR
            if (ok && !G.engineOK) sounds.push({ frequency: 110, duration: 1.6, volume: 0.5, type: 'sine' }, { frequency: 220, duration: 1.4, volume: 0.3, type: 'sine' }, { frequency: 330, duration: 1.2, volume: 0.2, type: 'sine' })
            G.engineOK = ok ? 1 : 0
            break
          }
        }
      }
    } else if (inView(11)) {                          // THE MIRROR ISLAND
      if (!G.inv.gear20 && G.held !== 1 && hit(346, 250, 54)) { deny(346, 250) }   // the sluice wants the spanner HELD
      else if (!G.inv.gear20 && G.held === 1 && hit(346, 250, 54)) { G.inv.gear20 = true; sounds.push({ frequency: 174.61, duration: 0.9, volume: 0.4, type: 'sawtooth' }, { frequency: 392.0, duration: 1.2, volume: 0.4, type: 'sine' }, { frequency: 587.33, duration: 1.0, volume: 0.25, type: 'sine' }) }   // the mirrored sluice (reflection) → the 20t gear (the fire sun now comes from the orrery, Aug 10)
      else if (G.inv.gear20 && hit(346, 250, 54)) { G.sluiceOpen = G.sluiceOpen ? 0 : 1; G.wheelAnim = 1; sounds.push({ frequency: 98, duration: 0.7, volume: 0.35, type: 'sawtooth' }, { frequency: G.sluiceOpen ? 220 : 147, duration: 1.1, volume: 0.25, type: 'sine' }) }   // the freed wheel is the TIDE VALVE — click turns it: open drains the island, closed floods it
    } else if (inView(13)) {                          // THE DROWNED QUARTER
      // flooded while the tide stands high — the sluice valve (mirror isle) drains it
      const drained = G.tideLevel < 0.2
      if (!drained && hit(256, 316, 70)) { deny(256, 316); sounds.push({ frequency: 55, duration: 0.5, volume: 0.22, type: 'sine' }) }   // a muffled glub — the water still owns it
      else if (drained && !G.bellRung && hit(256, 316, 70)) { G.bellRung = 1; sounds.push({ frequency: 65.4, duration: 3.5, volume: 0.6, type: 'sine' }, { frequency: 130.8, duration: 3.0, volume: 0.4, type: 'sine' }, { frequency: 196.0, duration: 2.2, volume: 0.22, type: 'sine' }) }   // THE DROWNED BELL tolls — heard through the whole island
      else if (drained && G.bellRung && !G.pearl && hit(256, 266, 38)) { G.pearl = 1; sounds.push({ frequency: 1046.5, duration: 0.6, volume: 0.3, type: 'sine' }, { frequency: 1568, duration: 0.8, volume: 0.2, type: 'sine' }) }   // the crown opens: THE TIDEGLASS PEARL, taken
    } else if (inView(12)) {                          // THE THICKET / BRAMBLES
    } else if (inView(14)) {                          // THE KILN — anneal the glass
      if (!G.kilnBloom && hit(256, 290, 60)) { G.kilnBloom = 1; sounds.push({ frequency: 261.63, duration: 1.3, volume: 0.4, type: "sine" }, { frequency: 392.0, duration: 1.5, volume: 0.32, type: "sine" }, { frequency: 523.25, duration: 1.7, volume: 0.26, type: "sine" }) }
      else if (G.kilnBloom && !G.summon && hit(256, 224, 44)) { G.summon = 0.001; sounds.push({ frequency: 110, duration: 0.9, volume: 0.3, type: "sine" }, { frequency: 880, duration: 0.9, volume: 0.25, type: "sine" }, { frequency: 60, duration: 0.14, volume: 0.5, type: "square" }, { frequency: 2200, duration: 0.12, volume: 0.3, type: "sawtooth" }) }   // click the risen bloom → it fires the summons skyward
    }
  }

  // obelisk slot easing (Aug 10): once clicked open, the stone leaves withdraw
  G.obeliskOpen = Math.max(0, Math.min(1, (G.obeliskOpen || 0) + (G.obeliskUnlocked ? dt / 1.5 : -dt)))
  // summoning clock: the bloom's call → bolt → portal → the ship emerges & moors
  if (G.summon > 0 && G.summon < 1) {
    G.summon = Math.min(1, G.summon + dt / 12)
    if (G.summon >= 0.8 && !G.airshipArrived) { G.airshipArrived = 1; sounds.push({ frequency: 196.0, duration: 1.5, volume: 0.3, type: 'sine' }, { frequency: 294.0, duration: 1.5, volume: 0.22, type: 'sine' }) }   // the distant horn — she has arrived
  }

  // ── the gate reflects the CURRENT code — combo A opens the door, a wrong code
  //    closes it (re-lockable); combo B opens the trapdoor to the drowned quarter ──
  const gateMatchA = ANSWER_DIALS.every((v, i) => G.dials[i] === v)
  G.gate2 = tideLow && dialsMatch(COMBO_B) && G.notch === 1     // low tide + combo B + notches DOWN
  G.door = gateMatchA ? Math.min(1, G.door + dt * 0.4) : Math.max(0, G.door - dt * 0.5)
  G.doorB = G.gate2 ? Math.min(1, (G.doorB || 0) + dt * 0.4) : Math.max(0, (G.doorB || 0) - dt * 0.5)   // B-side opens on the same glide as A
  if (G.door >= 1 && !G.gateChaptered) { G.gateChaptered = true; if (sim.trigger('tg-gate', true)) sim.completeChapter() }
  if (G.organGo) { G.organ = Math.min(1, G.organ + dt * 0.5); G.star = Math.min(1, G.star + dt * 0.35) }
  if (sim.trigger('tg-organ', G.organ >= 1)) sim.completeChapter()

  // ── the lens ──
  if (inView(3) && G.fin < 0.5) {
    if (hasMouse && wd.mouse_down) {
      const dx = mx - 256, dyUp = 430 - my
      if (dyUp > 20) {
        const az = Math.max(-1.15, Math.min(1.15, Math.atan2(dx, dyUp)))
        G.lensA += (az - G.lensA) * Math.min(1, dt * 5)
      }
    }
    const aligned = Math.abs(G.lensA - 0.25) < 0.075
    if (aligned) {
      const was = Math.floor(G.hold * 5)
      G.hold = Math.min(1, G.hold + dt / 2.4)
      if (Math.floor(G.hold * 5) > was) sounds.push({ frequency: 440 + G.hold * 440, duration: 0.15, volume: 0.2, type: 'sine' })
    } else G.hold = Math.max(0, G.hold - dt * 0.6)
    if (sim.trigger('tg-lens', G.hold >= 1)) {
      G.fin = 0.0001; sim.completeChapter()
      sounds.push({ frequency: 73.42, duration: 4.0, volume: 0.6, type: 'sine' },
                  { frequency: 146.83, duration: 4.0, volume: 0.45, type: 'sine' },
                  { frequency: 220.0, duration: 4.0, volume: 0.4, type: 'sine' },
                  { frequency: 293.66, duration: 4.0, volume: 0.35, type: 'sine' },
                  { frequency: 440.0, duration: 3.0, volume: 0.2, type: 'sine' })
    }
  }

  // ── the orrery: grab a luminary, swing it round its ring; the tide answers ──
  if (inView(7) && !G.orrerySolved) {
    const sunR = 0.78, moonR = 0.52
    if (hasMouse && wd.mouse_down) {
      const ux = (mx - 256) / 256, uy = (my - 256) / 256
      const r = Math.hypot(ux, uy)
      if (!G.grab) G.grab = (Math.abs(r - sunR) <= Math.abs(r - moonR)) ? 'sun' : 'moon'
      const ang = Math.atan2(ux, -uy)                       // 0 = up (matches vec2(sin,-cos) in the shader)
      if (G.grab === 'sun') G.sunA = ang; else G.moonA = ang
    } else G.grab = null
    const wrap = a => Math.atan2(Math.sin(a), Math.cos(a))
    const sunErr = Math.abs(wrap(G.sunA - Math.PI))         // 0 when the sun is set (bottom)
    const moonErr = Math.abs(wrap(G.moonA))                 // 0 when the moon is risen (top)
    const sunDown = (1 - Math.cos(G.sunA)) / 2
    const moonUp = (1 + Math.cos(G.moonA)) / 2
    G.tide = Math.max(0, Math.min(1, 0.55 * (0.5 * sunDown + 0.5 * moonUp) + 0.45 * sunDown * moonUp))
    const aligned = sunErr < 0.26 && moonErr < 0.26
    if (aligned) {
      const was = Math.floor((G.orreryHold || 0) * 5)
      G.orreryHold = Math.min(1, (G.orreryHold || 0) + dt / 1.3)
      if (Math.floor(G.orreryHold * 5) > was) sounds.push({ frequency: 330 + G.orreryHold * 500, duration: 0.16, volume: 0.22, type: 'sine' })
    } else G.orreryHold = Math.max(0, (G.orreryHold || 0) - dt * 0.7)
    if (sim.trigger('tg-orrery', (G.orreryHold || 0) >= 1)) {
      G.orrerySolved = 1
      if (!G.fireMoon) { raiseMoon(MOON_TABLE.fire); G.fireMoon = true }   // the tide puzzle brings the FIRE SUN (Galen Aug 10)
      G.dawnCine = 8   // the dawn holds the island still — nav locked while the sky turns (Galen Jul 30)
      sounds.push({ frequency: 65.41, duration: 4.0, volume: 0.55, type: 'sine' },
                  { frequency: 130.81, duration: 4.0, volume: 0.42, type: 'sine' },
                  { frequency: 196.0, duration: 4.0, volume: 0.36, type: 'sine' },
                  { frequency: 329.63, duration: 3.4, volume: 0.28, type: 'sine' },
                  { frequency: 523.25, duration: 3.0, volume: 0.20, type: 'sine' })
    }
  } else if (inView(7)) {
    // post-dawn the moon handle stays LIVE — drag it to pull the tide out/in (design §2 aha1)
    if (G.orrerySolved && hasMouse && wd.mouse_down) {
      const ux = (mx - 256) / 256, uy = (my - 256) / 256
      G.moonA = Math.atan2(ux, -uy)
    }
    G.tide = 1
  }

  // ── finale: return to a transformed shore ──
  if (G.fin > 0) {
    G.fin = Math.min(1, G.fin + dt / 3.5)
    // ONE-TIME whisk: when the finale blooms, sweep the player out of the lens
    // to the shore to watch the world turn to night. After that, act two is free
    // to roam — DON'T re-pin every frame (that yanked building→gate back to sea).
    if (G.fin > 0.35 && !G.finReturned) { if (G.view !== 0) go(0); G.finReturned = true }
    G.night = Math.min(1, G.night + dt / 6)
    if (G.fin > 0.6) G.vault = Math.min(1, G.vault + dt / 8)
  }

  // ── music: one score per era, brightness follows the place ──
  const era = (G.dawn || 0) > 0.3 ? 3 : (G.fin > 0.5 ? 2 : (G.organ > 0.5 ? 1 : 0))
  if (G.musicAct !== era + 1) {
    G.musicAct = era + 1
    if (era === 0) wd.__play_music = { score: { bpm: 54, loop: true, gain: 0.30, tracks: [
      { inst: 'sine', gain: 0.5, cutoff: 320, a: 1.2, d: 2.5, notes: 'A2 . . . . . . . E2 . . . . . . . F2 . . . . . . . E2 . . . . . . .' },
      { inst: 'triangle', gain: 0.14, cutoff: 700, a: 0.8, d: 2.0, notes: '. . . . A3+C4 . . . . . . . . . . . . . . . G3+B3 . . . . . . . . . . .' },
    ] } }
    if (era === 1) wd.__play_music = { score: { bpm: 58, loop: true, gain: 0.34, tracks: [
      { inst: 'sine', gain: 0.5, cutoff: 380, a: 1.0, d: 2.2, notes: 'D2 . . . . . . . A2 . . . . . . . B2 . . . . . . . A2 . . . . . . .' },
      { inst: 'triangle', gain: 0.18, cutoff: 900, a: 0.5, d: 1.6, notes: '. . D4 . . . F#4 . . . . . A4 . . . . . B3 . . . D4 . . . . . . . . .' },
    ] } }
    if (era === 2) wd.__play_music = { score: { bpm: 64, loop: true, gain: 0.38, swing: 0.05, tracks: [
      { inst: 'sine', gain: 0.5, cutoff: 420, a: 0.8, d: 2.0, notes: 'D2 . . . A2 . . . B2 . . . F#2 . . . G2 . . . D2 . . . G2 . . . A2 . . .' },
      { inst: 'triangle', gain: 0.2, cutoff: 1200, a: 0.3, d: 1.2, notes: 'D4 . F#4 . A4 . B4 . A4 . F#4 . D4 . E4 . D4 . F#4 . A4 . D5 . A4 . F#4 . E4 . D4 .' },
      { inst: 'sawtooth', gain: 0.06, cutoff: 600, a: 1.5, d: 2.5, notes: 'D3+F#3+A3 . . . . . . . G3+B3+D4 . . . . . . . B2+D3+F#3 . . . . . . . A2+C#3+E3 . . . . . . .' },
    ] } }
    if (era === 3) wd.__play_music = { score: { bpm: 60, loop: true, gain: 0.36, tracks: [   // the dawn — warm, major, rising
      { inst: 'sine', gain: 0.5, cutoff: 460, a: 1.0, d: 2.4, notes: 'G2 . . . . . . . D3 . . . . . . . C3 . . . . . . . D3 . . . . . . .' },
      { inst: 'triangle', gain: 0.2, cutoff: 1300, a: 0.4, d: 1.6, notes: 'G4 . B4 . D5 . B4 . C5 . E5 . D5 . B4 . A4 . C5 . B4 . G4 . D5 . B4 . G4 . D4 .' },
      { inst: 'sine', gain: 0.12, cutoff: 820, a: 1.6, d: 2.5, notes: 'G3+B3+D4 . . . . . . . C4+E4+G4 . . . . . . . D4+F#4+A4 . . . . . . . G3+B3+D4 . . . . . . .' },
    ] } }
  }
  // ── the observatory constellation (view 5): weave near a star to kindle it ──
  if (G.latSolved) {
    for (let i = 0; i < 5; i++) G.lat[i] = 1   // locked in — it holds, gold, forever
    G.latBloom = 1
  } else if (G.view === 5) {
    const AX = [0, -0.6, 0.6, 0, 0]
    const AY = [0, 0, 0, -0.6, 0.6]
    const cux = hasMouse ? (mx - 256) / 256 : -99
    const cuy = hasMouse ? (my - 256) / 256 : -99
    let lit = 0
    for (let i = 0; i < 5; i++) {
      const d = Math.hypot(cux - AX[i], cuy - AY[i])
      if (hasMouse && d < 0.22) G.lat[i] = Math.min(1, G.lat[i] + 0.06)
      else G.lat[i] = Math.max(0, G.lat[i] - 0.0035)
      if (G.lat[i] > 0.6) lit++
    }
    G.latBloom = lit >= 5 ? Math.min(1, (G.latBloom || 0) + 0.02) : Math.max(0, (G.latBloom || 0) - 0.01)
    if (sim.trigger('tg-lattice', lit >= 5)) {
      G.latSolved = 1                                                    // it locks
      sounds.push({ frequency: 264, duration: 1.6, volume: 0.34, type: 'sine' })
      sounds.push({ frequency: 528, duration: 1.8, volume: 0.30, type: 'sine' })   // a fifth — the vault answers
    }
  } else {
    // fade the weave when you leave the room, so re-entry starts calm
    for (let i = 0; i < 5; i++) G.lat[i] = Math.max(0, G.lat[i] - dt * 0.5)
    G.latBloom = Math.max(0, (G.latBloom || 0) - dt * 0.5)
  }
  // solving the constellation raises the vault-dome from the sea — a space opens
  if (G.latSolved) G.vault = Math.min(1, (G.vault || 0) + dt / 7)
  // waking the core lights it, and the light holds — it blooms once, forever
  if (G.coreSolved) { for (let i = 0; i < 3; i++) G.core[i] = 0; G.coreGlow = Math.min(1, (G.coreGlow || 0) + dt / 2.5) }
  // waking the orrery brings the dawn — the island's final turning, held forever
  if (G.orrerySolved) {
    G.sunA = Math.PI                                               // the sun settles set; the moon stays LIVE for the tide
    G.orreryGlow = Math.min(1, (G.orreryGlow || 0) + dt / 2.5)
    G.dawn = Math.min(1, (G.dawn || 0) + dt / 9)
    // one-time whisk out to the shore to witness the sunrise, then free to roam
    if (G.dawn > 0.28 && !G.dawnReturned) { if (G.view !== 0) go(0); G.dawnReturned = true }
  }

  wd.music_mod = { brightness: 0.25 + (G.view === 0 ? 0.35 : G.view === 8 ? 0.28 : G.view === 3 ? 0.3 : (G.view === 6 || G.view === 7) ? 0.24 : 0.12) + G.fin * 0.3 + (G.coreGlow || 0) * 0.2 + (G.dawn || 0) * 0.3, gain: 1 }
  if (sounds.length) wd.__play_sound = sounds

  // ── publish the whiteboard ──
  // gear-train: accumulate the crank phase hook-side (re-gearing never pops the spin),
  // recompute the honest cadence, refresh the tide + thicket-burn from the moons.
  G.enginePhase = (G.enginePhase || 0) + dt * CRANK_RATE
  const gcNow = gearCadence(G.seats)
  const shaftCadence = gcNow.cadence
  G.engineOK = (gcNow.full && Math.abs(gcNow.cadence - TARGET_CADENCE) <= TARGET_CADENCE * CAD_TOL && gcNow.dir === TARGET_DIR) ? 1 : 0
  const domFire = (G.moons[dominantMoon()] || {}).el === 'fire'     // thicket burns only under the fire moon
  G.thicketBurn = domFire ? Math.min(1, (G.thicketBurn || 0) + dt * 0.5) : Math.max(0, (G.thicketBurn || 0) - dt * 0.4)
  // (tide is computed once, above — the sluice valve drives it)
  const U = new Array(118).fill(0)
  U[0] = G.view; U[1] = G.pv; U[2] = G.fade; U[3] = G.t
  for (let i = 0; i < 4; i++) U[4 + i] = G.dials[i]
  U[8] = G.door
  for (let i = 0; i < 5; i++) U[9 + i] = G.pipes[i] / 4
  U[14] = G.organ; U[15] = G.lensA; U[16] = G.hold; U[17] = G.fin
  U[18] = G.door > 0 ? 1 : 0
  for (let i = 0; i < 4; i++) U[19 + i] = G.flash[i]
  U[23] = G.night; U[24] = sim.act; U[25] = G.clickP; U[26] = G.vault
  U[27] = hasMouse ? mx : -100; U[28] = hasMouse ? my : -100
  U[29] = G.star; U[30] = (hasMouse && wd.mouse_down) ? 1 : 0
  U[31] = Math.max(G.organ, G.door * 0.25)
  U[32] = G.latBloom || 0
  for (let i = 0; i < 5; i++) U[33 + i] = G.lat[i]
  U[38] = G.latSolved ? 1 : 0
  U[39] = G.coreSolved ? 1 : 0
  for (let i = 0; i < 3; i++) U[40 + i] = (G.core[i] / 12) * Math.PI * 2   // ring notch angles (rad)
  U[43] = G.coreGlow || 0
  U[44] = G.dawn || 0
  U[45] = G.sunA; U[46] = G.moonA; U[47] = G.tide || 0
  U[48] = G.orrerySolved ? 1 : 0
  U[49] = G.orreryGlow || 0
  U[50] = G.orreryHold || 0
  // ── ACT 1 v2 flags (52–59 kept; 55 retired; 59 repurposed to sluice-freed) ──
  U[51] = G.enginePhase             // crank phase accumulator (gear-train spin, no-pop)
  U[52] = G.doorB || 0              // drowned-gate opening, EASED (was a binary snap)
  U[53] = G.inv.spanner ? 1 : 0     // spanner in the satchel (deck render)
  U[54] = G.gate2 ? 1 : 0           // Mirror-Island route open
  U[55] = 0                         // RETIRED (was keyB) — reserved, do not reuse
  U[56] = G.engineOK ? 1 : 0        // helm / deck power visual
  U[57] = G.airshipArrived ? 1 : 0   // airship moored (summoned & arrived — Aug 10)
  U[58] = G.act || 1                // 1 = island, 2 = flight begun
  U[59] = G.inv.gear20 ? 1 : 0      // sluice-freed (the mirrored wheel has turned)
  // ── the moons / gears / satchel / tide / thicket board (uni 60–77) ──
  U[60] = G.tideLevel                                   // tide01
  U[61] = (G.inv.spanner ? 1 : 0) + (G.inv.gear8 && !inSeat(2) ? 2 : 0) + (G.inv.gear12 && !inSeat(3) ? 4 : 0) + (G.inv.gear16 && !inSeat(4) ? 8 : 0) + (G.inv.gear20 && !inSeat(5) ? 16 : 0)   // invMask
  U[62] = G.held                                        // heldItem 0..5
  U[63] = G.seats[0]; U[64] = G.seats[1]; U[65] = G.seats[2]   // seat gear ids
  U[66] = shaftCadence                                  // true shaft cadence
  U[67] = TARGET_CADENCE                                // the tide-bell target
  U[68] = G.engineOK ? 1 : 0                            // engineOK01
  U[69] = (G.inv.spanner || G.inv.gear8 || G.inv.gear12 || G.inv.gear16 || G.inv.gear20) ? 1 : 0   // satchelOpen01
  U[70] = G.inv.gear12 ? 1 : 0                          // gear12free01 (grease broken)
  U[71] = G.fly || 0                                    // fly01 (helm-pull anim)
  U[72] = dominantMoon()                                // dominantMoonId
  U[73] = G.moons.length                                // moonCount
  U[74] = G.tideBaseline || 0                           // tideBaseline01 (climbs per act)
  U[75] = G.activeMoon >= 0 ? 1 : 0                     // activeMoonSet01
  U[76] = G.thicketBurn || 0                            // thicketBurn01
  U[77] = G.notch || 0                                  // drowned-gate notch (0 up · 1 down)
  U[78] = ({ glass: 0, fire: 1 })[(G.moons[dominantMoon()] || {}).el] || 0   // ruling ELEMENT — sky + sea read it
  G.elBlend = (G.elBlend || 0) + (U[78] - (G.elBlend || 0)) * Math.min(1, dt * 0.7)
  U[82] = G.elBlend   // the eased element morph — ONE DAY-style conversion driver
  if (G.seaClock === undefined) G.seaClock = 1.0 + G.t * 0.55       // seamless takeover of the old clock
  G.seaClock += dt * 0.55 * (1 - 0.78 * G.elBlend)                  // magma runs at 22% — rate morphs, phase NEVER jumps
  U[83] = G.seaClock
  if (G.deny) { G.deny.t = Math.max(0, G.deny.t - dt * 2.2); if (G.deny.t <= 0) G.deny = null }
  U[79] = G.deny ? G.deny.t : 0; U[80] = G.deny ? G.deny.x : 0; U[81] = G.deny ? G.deny.y : 0   // deny flash
  if (G.clickFx) { G.clickFx.t = Math.max(0, G.clickFx.t - dt * 2.8); if (G.clickFx.t <= 0) G.clickFx = null }
  U[84] = G.clickFx ? G.clickFx.t : 0; U[85] = G.clickFx ? G.clickFx.x : 0; U[86] = G.clickFx ? G.clickFx.y : 0   // click-confirm
  U[104] = G.kilnBloom ? 1 : 0   // KILN — the glass has bloomed
  U[105] = (G.sluiceOpen ? 1.05 : 0) + G.wheelAnim * 2.5   // wheel angle: rest↔turned + the turning kick
  U[106] = G.sluiceOpen ? 1 : 0   // TIDE VALVE open
  U[107] = G.bellRung ? 1 : 0     // the drowned bell has tolled
  U[108] = G.pearl ? 1 : 0        // the tideglass pearl is taken
  U[109] = G.pearlSet ? 1 : 0     // pearl seated in the cave socket → it glows, the beam rises
  U[110] = G.crystalPowered ? 1 : 0  // the airship crystal is lit (shore beam + deck helm live)
  U[115] = G.summon || 0             // summoning clock 0..1 (bolt→portal→emerge→seal)
  U[116] = G.airshipArrived ? 1 : 0  // the airship persists after the cine
  U[117] = G.obeliskOpen || 0        // the sea-cave obelisk slot iris
  U[113] = G.flyCine                 // Act-I finale cinematic timeline (0 off · 0..1 crossing · 1 card)
  U[114] = G.inv.gear8 ? 1 : 0       // 8t gear EVER taken (satchel bit clears when seated — the cave plank must not haunt)
  { const _rm = (G.moons && G.moons[dominantMoon()]) || {}   // ruling moon → the cave's ocean view matches the sky
    U[111] = _rm.hue || 0
    U[112] = (_rm.alt || 0) }
  U[87] = G.inspect ? 1 : 0   // inspect-mode skin (blue cast + grid)
  const navRows = NAV[G.view] || []
  for (let ni = 0; ni < 4; ni++) {
    const e = navRows[ni]
    U[88 + ni * 4] = e ? e.x : 0; U[89 + ni * 4] = e ? e.y : 0
    U[90 + ni * 4] = e ? e.d : 0; U[91 + ni * 4] = (e && (!e.when || e.when()) && !(G.flyCine > 0)) ? 1 : 0
  }
  if (G.inspect) {
    const lines = [{ id: 'insp0', type: 'text', x: '14px', y: '14px', text: '◉ INSPECT ON (I) — clicks are documented for the AI', fontSize: '13px', color: '#7fe8d8' }]
    const lg = G.clickLog || []
    for (let li = 0; li < lg.length; li++) lines.push({ id: 'insp' + (li + 1), type: 'text', x: '14px', y: (34 + li * 18) + 'px', text: lg[lg.length - 1 - li], fontSize: '12px', color: li === 0 ? '#ffd9a0' : '#b8a888' })
    wd.hud = lines
  } else if (G.inspectWasOn) { wd.hud = [] }
  G.inspectWasOn = !!G.inspect
  // ACT I card subtitle (Galen): a quiet line under the mark while the card holds
  if (!G.inspect) {
    const showCard = (G.flyCine || 0) >= 0.82
    if (showCard && !G.hudCard) wd.hud = [{ id: 'act1sub', type: 'text', x: '30%', y: '63%', text: 'stay tuned for updates', fontSize: '17px', color: '#e8c98a', css: { fontFamily: 'Georgia, "Times New Roman", serif', fontStyle: 'italic', letterSpacing: '0.34em', textShadow: '0 0 14px rgba(255,190,90,0.45)', opacity: '0.92' } }]
    else if (!showCard && G.hudCard) wd.hud = []
    G.hudCard = showCard ? 1 : 0
  }
  wd.gpuUniforms = U
} catch (e) { /* the island keeps its silence */ }