import "@fontsource-variable/inter";
import "@fontsource/jetbrains-mono/400.css";
import "@fontsource/jetbrains-mono/500.css";
import "@fontsource/jetbrains-mono/600.css";
import "./styles.css";

console.log("%cCreated by Sarthak", "font-size:14px;font-weight:700;color:#F2A83B;");

/* ---------- CTA sample-address estimate (standalone so it always runs) ---------- */
(function(){
  function init(){
    var form = document.getElementById("ctaForm");
    var sel = document.getElementById("ctaAddr");
    var btn = document.getElementById("ctaEstimateBtn");
    var panel = document.getElementById("ctaEstimate");
    if(!sel || !panel) return;
    var money = function(n){ return "$" + Number(n).toLocaleString("en-US"); };
    var set = function(id, val){ var el = document.getElementById(id); if(el) el.textContent = val; };
    function render(scroll){
      var opt = sel.options[sel.selectedIndex];
      if(!opt || opt.dataset.placeholder){ panel.hidden = true; return; }
      set("estAddr", opt.value);
      set("estArea", Number(opt.dataset.area).toLocaleString("en-US") + " sq ft");
      set("estSquares", opt.dataset.squares + " sq");
      set("estPitch", opt.dataset.pitch);
      set("estMaterial", opt.dataset.material);
      set("estPrice", money(opt.dataset.low) + " – " + money(opt.dataset.high));
      panel.hidden = false;
      if(scroll) panel.scrollIntoView({behavior:"smooth", block:"nearest"});
    }
    function go(e){ if(e) e.preventDefault(); render(true); }
    if(form) form.addEventListener("submit", go);
    if(btn) btn.addEventListener("click", go);
    sel.addEventListener("change", function(){ if(!panel.hidden) render(false); });
  }
  if(document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
  else init();
})();

(function(){
  var reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
  var finePointer = matchMedia("(pointer: fine)").matches;
  var fmt = function(n){ return n.toLocaleString("en-US"); };

  /* ---------- scrollbar materialises only while scrolling ---------- */
  (function(){
    var to = null;
    addEventListener("scroll", function(){
      document.documentElement.classList.add("scrolling");
      clearTimeout(to);
      to = setTimeout(function(){ document.documentElement.classList.remove("scrolling"); }, 900);
    }, { passive: true, capture: true });
  })();

  /* ---------- mobile scroll progress bar ---------- */
  (function(){
    var bar = document.getElementById("mprogBar");
    if(!bar) return;
    var raf = null;
    function upd(){
      raf = null;
      var doc = document.scrollingElement || document.documentElement;
      var max = Math.max(1, doc.scrollHeight - innerHeight);
      bar.style.transform = "scaleX(" + Math.min(1, window.scrollY / max).toFixed(4) + ")";
    }
    addEventListener("scroll", function(){ if(raf == null) raf = requestAnimationFrame(upd); }, { passive: true });
    upd();
  })();

  /* ---------- footer: hail alerts by ZIP ---------- */
  (function(){
    var form = document.getElementById("zipForm");
    if(!form) return;
    var zin = document.getElementById("zipInput");
    form.addEventListener("submit", function(e){
      e.preventDefault();
      var v = zin.value.trim();
      if(!/^\d{5}$/.test(v)){ zin.focus(); zin.select(); return; }
      form.innerHTML = "<div class='fa-done'><span class='tag'>&#9873; WATCHING " + v + "</span>" +
        "<span>First alert lands after the next radar-verified swath. No spam, only storms.</span></div>";
    });
  })();

  /* ---------- scroll reveals: 150ms, 60ms stagger ---------- */
  var revealEls = [].slice.call(document.querySelectorAll(".reveal"));
  if(!reduced && "IntersectionObserver" in window){
    var groups = new Map();
    var io = new IntersectionObserver(function(entries){
      entries.forEach(function(e){
        if(!e.isIntersecting && e.boundingClientRect.top >= 0) return;
        io.unobserve(e.target);
        var parent = e.target.parentElement;
        var idx = groups.get(parent) || 0;
        groups.set(parent, idx + 1);
        setTimeout(function(){ e.target.classList.add("in"); }, idx * 60);
        setTimeout(function(){ groups.set(parent, Math.max(0,(groups.get(parent)||1)-1)); }, 400);
      });
    }, {threshold:.15});
    revealEls.forEach(function(el){ io.observe(el); });
  } else {
    revealEls.forEach(function(el){ el.classList.add("in"); });
  }

  /* ---------- count-ups: one-shot, comma-formatted ---------- */
  var counters = [].slice.call(document.querySelectorAll("[data-count]"));
  function runCounter(el){
    var target = +el.dataset.count;
    var suffix = el.dataset.suffix || "", prefix = el.dataset.prefix || "";
    var dec = +(el.dataset.decimals || 0);
    function print(v){
      el.textContent = prefix + (dec ? v.toFixed(dec) : fmt(Math.round(v))) + suffix;
    }
    if(reduced){ print(target); return; }
    var t0 = null, dur = 2400;
    function step(ts){
      if(!t0) t0 = ts;
      var p = Math.min((ts - t0)/dur, 1);
      p = 1 - Math.pow(1-p, 3);
      print(target * p);
      if(p < 1) requestAnimationFrame(step);
    }
    requestAnimationFrame(step);
  }
  if("IntersectionObserver" in window){
    var cio = new IntersectionObserver(function(entries){
      entries.forEach(function(e){
        if(e.isIntersecting){ cio.unobserve(e.target); runCounter(e.target); }
      });
    }, {threshold:.6});
    counters.forEach(function(el){ cio.observe(el); });
  } else counters.forEach(runCounter);

  /* ---------- hero contour-line shader (survey isolines, cyan) ---------- */
  (function(){
    var cv = document.getElementById("heroFx");
    if(!cv) return;
    var gl = cv.getContext("webgl", {alpha:true, antialias:false});
    if(!gl){ cv.remove(); return; }
    var vsrc = "attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}";
    var fsrc =
      "precision mediump float;" +
      "uniform vec2 u_res;uniform float u_t;uniform vec2 u_m;" +
      "void main(){" +
      " vec2 uv=gl_FragCoord.xy/u_res;" +
      " vec2 p=uv*vec2(u_res.x/u_res.y,1.);" +
      " p+=(u_m-.5)*.10;" +
      " float v=sin(p.x*4.2+u_t*.25)+sin(p.y*5.1-u_t*.2)+sin((p.x+p.y)*3.1+u_t*.12)+sin(length(p-vec2(1.2,.2))*6.-u_t*.3);" +
      " float f=fract(v*1.5);" +
      " float line=smoothstep(.90,.975,f)*(1.-smoothstep(.975,1.,f));" +
      " float mask=smoothstep(1.3,.1,distance(uv,vec2(.78,.82)));" +
      " gl_FragColor=vec4(.27,.69,.79,line*mask*.085);" +
      "}";
    function sh(t,s){var o=gl.createShader(t);gl.shaderSource(o,s);gl.compileShader(o);return o;}
    var pr=gl.createProgram();
    gl.attachShader(pr,sh(gl.VERTEX_SHADER,vsrc));
    gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,fsrc));
    gl.linkProgram(pr);gl.useProgram(pr);
    var buf=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buf);
    gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),gl.STATIC_DRAW);
    var loc=gl.getAttribLocation(pr,"p");
    gl.enableVertexAttribArray(loc);gl.vertexAttribPointer(loc,2,gl.FLOAT,false,0,0);
    var uRes=gl.getUniformLocation(pr,"u_res"),uT=gl.getUniformLocation(pr,"u_t"),uM=gl.getUniformLocation(pr,"u_m");
    var hero=document.querySelector(".hero"),mx=.5,my=.5;
    if(finePointer) hero.addEventListener("pointermove",function(e){
      var r=hero.getBoundingClientRect();
      mx=(e.clientX-r.left)/r.width; my=(e.clientY-r.top)/r.height;
    });
    var dpr=Math.min(devicePixelRatio||1,1.5);
    function rs(){var r=hero.getBoundingClientRect();cv.width=r.width*dpr;cv.height=r.height*dpr;gl.viewport(0,0,cv.width,cv.height);}
    rs();addEventListener("resize",rs);
    gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
    function paint(t){
      gl.uniform2f(uRes,cv.width,cv.height);
      gl.uniform1f(uT,t);
      gl.uniform2f(uM,mx,1-my);
      gl.drawArrays(gl.TRIANGLES,0,3);
    }
    paint(0); /* warm the compile at load */
    if(reduced){ paint(8); return; }
    var vis=true,t0=performance.now();
    if("IntersectionObserver" in window) new IntersectionObserver(function(en){vis=en[0].isIntersecting;}).observe(hero);
    (function frame(now){
      if(vis) paint((now-t0)/1000);
      requestAnimationFrame(frame);
    })(t0);
  })();

  /* ---------- cursor-following hero glow ---------- */
  (function(){
    var g=document.getElementById("heroGlow");
    if(!g) return;
    if(!finePointer || reduced){ g.remove(); return; }
    var hero=document.querySelector(".hero");
    hero.addEventListener("pointermove",function(e){
      var r=hero.getBoundingClientRect();
      g.style.opacity="1";
      g.style.transform="translate("+(e.clientX-r.left-260)+"px,"+(e.clientY-r.top-260)+"px)";
    });
    hero.addEventListener("pointerleave",function(){ g.style.opacity="0"; });
  })();

  /* ---------- card tilt physics (spring, with glare tracking) ---------- */
  if(finePointer && !reduced){
    [].slice.call(document.querySelectorAll(".ind,.risk-card,.plan,.founder-card,.feat,.oc")).forEach(function(el){
      var rx=0,ry=0,tx=0,ty=0,vx=0,vy=0,raf=null,over=false;
      function frame(){
        vx+=(tx-rx)*.16; vy+=(ty-ry)*.16; vx*=.72; vy*=.72;
        rx+=vx; ry+=vy;
        if(!over && Math.abs(rx)<.02 && Math.abs(ry)<.02 && Math.abs(vx)<.02 && Math.abs(vy)<.02){
          el.style.transform=""; raf=null; return;
        }
        el.style.transform="perspective(900px) rotateX("+rx.toFixed(2)+"deg) rotateY("+ry.toFixed(2)+"deg) translateY(-2px)";
        raf=requestAnimationFrame(frame);
      }
      el.addEventListener("pointerenter",function(){
        over=true; el.classList.add("tilting");
        if(!raf) raf=requestAnimationFrame(frame);
      });
      el.addEventListener("pointermove",function(e){
        var r=el.getBoundingClientRect();
        var px=(e.clientX-r.left)/r.width, py=(e.clientY-r.top)/r.height;
        ty=(px-.5)*5; tx=(.5-py)*5;
        el.style.setProperty("--gx",(px*100)+"%");
        el.style.setProperty("--gy",(py*100)+"%");
      });
      el.addEventListener("pointerleave",function(){ over=false; tx=0; ty=0; });
    });
  }

  /* ---------- mobile nav ---------- */
  (function(){
    var mb = document.getElementById("menuBtn");
    if(!mb) return;
    mb.addEventListener("click", function(){
      var open = document.body.classList.toggle("nav-open");
      mb.setAttribute("aria-expanded", open ? "true" : "false");
    });
    [].slice.call(document.querySelectorAll(".nav-links a")).forEach(function(a){
      a.addEventListener("click", function(){
        document.body.classList.remove("nav-open");
        mb.setAttribute("aria-expanded", "false");
      });
    });
  })();

  /* The 3D scene (three.js + gsap, ~540KB) loads after first paint;
     everything that needs the model lives inside initEstimator. */
  function initEstimator(createScene){
  /* ---------- 3D blueprint model: source of truth for the estimator ---------- */
  var modelWrap = document.getElementById("modelWrap");
  var roof3d = createScene(modelWrap, { reduced: reduced });
  roof3d.stage(1); /* no scan phase on this page: model arrives fully inked */
  roof3d.onDragStart(function(){
    var hint = document.getElementById("dragHint");
    if(hint) hint.style.opacity = "0";
  });
  (function(){
    var slot = document.getElementById("estSlot");
    function size(){
      var r = slot.getBoundingClientRect();
      if(r.width > 2 && r.height > 2) roof3d.resize(r.width, r.height);
    }
    if("ResizeObserver" in window) new ResizeObserver(size).observe(slot);
    else addEventListener("resize", size);
    size();
  })();

  /* ---------- estimator: quantities derived from the 3D model ----------
     rates below are pulled from a real Certainteed Landmark AR tear-off/install
     invoice (roofing material + labor line items on a 12/12, 64.89 sq job):
     shingles $203.19/sq, synthetic underlayment ~$44.53/sq (10 sq/roll @ $445.30),
     ridge/hip cap ~$4.00/lf, accessories (drip edge, vents, flashing, nails,
     starter, sealant) ~$53/sq, plus a steep-pitch labor surcharge that kicks in
     at 8/12 and climbs with pitch &mdash; the old flat/guessed numbers didn't scale
     with job size or pitch at all. */
  function makeEstimate(o){
    var CANON_W = {gable:42, hip:46, storm:58, estate:42, mustang:48};
    var CANON_P = {gable:6, hip:6, storm:5, estate:6, mustang:12};
    o.wf = CANON_W[o.style] || o.wf;
    o.pn = CANON_P[o.style] || o.pn;
    var mm = roof3d.measure({style:o.style, wf:o.wf, pn:o.pn});
    var layers = o.layers || 1;
    var rate = o.shingleRate || 203;
    /* hips and complex multi-facet roofs honestly carry more cut waste than plain gables */
    var waste = (o.style === "hip" || o.style === "mustang") ? 1.12 : 1.10;
    var squares = Math.ceil(mm.roofSqFt * waste / 100);
    var shingles = squares * rate;
    var rolls = Math.ceil(squares / 10), under = rolls * 445;
    var ridge = Math.round(mm.ridgeLf * 4.0);
    var miscRate = o.miscRate || 53;
    var misc = Math.round(squares * miscRate);
    var tear = squares * 78 * layers;
    var labor = Math.round(o.crew * o.days * 760);
    var gutters = o.gutters ? Math.round(o.gutters * 12) : 0;
    var extras = o.extras || [];
    var extrasSum = extras.reduce(function(a,x){ return a + x[1]; }, 0);
    /* real steep-fee tiering: 8-9/12, 10-11/12, 12/12+ */
    var steepRate = o.pn >= 12 ? 26 : o.pn >= 10 ? 20 : o.pn >= 8 ? 14.29 : 0;
    var steep = steepRate ? Math.round(squares * steepRate) : 0;
    var total = shingles + under + ridge + misc + tear + labor + gutters + extrasSum + steep;
    var lines = [
      ["SCAN &nbsp;&nbsp;" + o.scanLabel, "<span class='ok'>OK " + o.scanTime + "</span>", null],
      ["MEASURE &nbsp;roof " + fmt(mm.roofSqFt) + " sq ft &middot; " + o.pn + "/12 &middot; " + mm.facets + " facets", "<span class='ok'>OK</span>", "roof"],
      ["<b>" + (o.shingleLabel || "Architectural shingles") + "</b> &middot; " + squares + " sq", "$" + fmt(shingles), "roof"],
      ["<b>Synthetic underlayment</b> &middot; " + rolls + " rolls", "$" + fmt(under), "roof"],
      ["<b>Ridge" + (o.style === "hip" || o.style === "mustang" ? " + hip" : "") + " cap</b> &middot; " + mm.ridgeLf + " lf", "$" + fmt(ridge), "ridge"],
      ["<b>Drip edge, vents, fasteners</b>", "$" + fmt(misc), "eave"],
      ["<b>Tear-off + disposal</b> &middot; " + layers + " layer" + (layers > 1 ? "s" : ""), "$" + fmt(tear), "roof"]
    ];
    if(steep) lines.push(["<b>Steep-pitch labor</b> &middot; " + o.pn + "/12 &middot; " + squares + " sq", "$" + fmt(steep), "roof"]);
    if(o.gutters) lines.push(["<b>Gutters</b> &middot; " + o.gutters + " lf aluminum", "$" + fmt(gutters), "eave"]);
    extras.forEach(function(x){ lines.push(["<b>" + x[0] + "</b>", "$" + fmt(x[1]), null]); });
    lines.push(["<b>Labor</b> &middot; crew of " + o.crew + " &middot; " + o.days + " day" + (o.days > 1 ? "s" : ""), "$" + fmt(labor), null]);
    if(o.damage){
      var sev = o.damage > 38 ? "HIGH" : o.damage > 26 ? "MODERATE" : "LOW";
      lines.splice(2, 0, ["DAMAGE &nbsp;" + o.damage + " impacts &middot; facet F2 &middot; severity " + sev, "<span class='flag'>FLAG</span>", "damage"]);
      lines.splice(3, 0, ["<b>Digital Twin evidence pack</b> &middot; " + (o.damage * 4 + 38) + " photos", "included", "damage"]);
    }
    return {
      roof:{ style:o.style, wf:o.wf, pn:o.pn },
      lines:lines, total:"$" + fmt(total), time:o.time
    };
  }

  var jobs = [
    makeEstimate({style:"gable", wf:42, pn:6, scanLabel:"aerial imagery + street view", scanTime:"0.4s",
      layers:2, crew:5, days:2, gutters:110,
      extras:[["Chimney reflash + cricket",680],["Skylight curb + reflash &middot; 2 units",940]],
      time:"3.1 seconds."}),
    makeEstimate({style:"hip", wf:46, pn:8, scanLabel:"aerial imagery + street view", scanTime:"0.5s",
      shingleRate:203, crew:5, days:1, time:"3.4 seconds."}),
    makeEstimate({style:"storm", wf:58, pn:5, scanLabel:"hail impact detection", scanTime:"0.6s",
      shingleRate:255, shingleLabel:"Class-4 impact shingles",
      crew:6, days:2, damage:41, gutters:148,
      extras:[["Decking replacement &middot; 12 sheets",1560],["Drip edge + valley shield &middot; code items",1100],["Permits + inspection",850]],
      time:"2.8 seconds. Claim-ready."})
  ];

  var body = document.getElementById("estBody");
  var timer = [];
  function clearTimers(){ timer.forEach(clearTimeout); timer = []; }
  function renderJob(i, animate){ renderEstimate(jobs[i], animate); }

  function renderEstimate(j, animate){
    clearTimers();
    roof3d.set(j.roof);
    document.getElementById("estTime").textContent = j.time;

    body.innerHTML = "";
    var frag = document.createDocumentFragment();
    j.lines.forEach(function(l){
      var d = document.createElement("div");
      d.className = "est-line";
      d.innerHTML = "<span class='k'>"+l[0]+"</span><span class='v'>"+l[1]+"</span>";
      if(l[2]){
        d.setAttribute("data-hl", l[2]);
        d.addEventListener("mouseenter", function(){ roof3d.highlight(l[2]); });
        d.addEventListener("mouseleave", function(){ roof3d.highlight(null); });
      }
      frag.appendChild(d);
    });
    var tot = document.createElement("div");
    tot.className = "est-total";
    tot.innerHTML = "<span class='k'>ESTIMATE TOTAL</span><span class='v'>"+j.total+"</span>";
    frag.appendChild(tot);
    body.appendChild(frag);

    var rows = [].slice.call(body.querySelectorAll(".est-line"));
    if(!animate || reduced){
      rows.forEach(function(r){ r.classList.add("on"); });
      tot.classList.add("on");
      return;
    }
    var roofBox = document.querySelector(".est-roof");
    roofBox.classList.remove("scanning");
    void roofBox.offsetWidth;
    roofBox.classList.add("scanning");
    timer.push(setTimeout(function(){ roofBox.classList.remove("scanning"); }, 1300));
    rows.forEach(function(r, idx){
      timer.push(setTimeout(function(){ r.classList.add("on"); }, 220 + idx * 260));
    });
    timer.push(setTimeout(function(){ tot.classList.add("on"); }, 220 + rows.length * 260 + 180));
  }

  /* typed address: deterministic scripted estimate */
  function hashStr(s){
    var h = 2166136261;
    for(var i = 0; i < s.length; i++){ h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); }
    return h >>> 0;
  }
  function rng(seed){
    return function(){
      seed = Math.imul(seed ^ (seed >>> 15), 2246822519);
      seed = Math.imul(seed ^ (seed >>> 13), 3266489917);
      return ((seed ^= seed >>> 16) >>> 0) / 4294967296;
    };
  }
  function jobFromAddress(addr){
    var clean = addr.replace(/[<>&"]/g, "").replace(/\s+/g, " ").trim();
    var r = rng(hashStr(clean.toLowerCase()));
    var pitch = 4 + Math.floor(r() * 7);
    var hipStyle = r() < .45;
    var damaged = r() < .3;
    var impacts = damaged ? 18 + Math.floor(r() * 36) : 0;
    var widthFt = 30 + Math.round(r() * 30);
    var layers = r() < .3 ? 2 : 1;
    var crew = 4 + Math.floor(r() * 3);
    var days = 1 + Math.round(r() * 3) / 2;
    var short = clean.length > 30 ? clean.slice(0, 28) + "..." : clean;
    return makeEstimate({
      style: hipStyle ? "hip" : "gable", wf: widthFt, pn: pitch,
      scanLabel: short, scanTime: "0.5s",
      layers: layers, crew: crew, days: days,
      damage: impacts, misc: 600 + Math.round(r() * 350),
      time: (2.4 + r() * 1.2).toFixed(1) + " seconds." + (damaged ? " Claim-ready." : "")
    });
  }

  /* the addresses we have a client-supplied aerial survey for get their own modelled house */
  var BENT_OAK = "5705 Bent Oak Place, Dallas, TX 75248";
  function estateJob(){
    return makeEstimate({
      style: "estate", wf: 42, pn: 6,
      scanLabel: BENT_OAK, scanTime: "0.6s",
      layers: 1, crew: 6, days: 3, gutters: 165,
      extras: [["Chimney reflash + cricket", 680], ["Pool deck coping reseal", 420]],
      time: "3.3 seconds."
    });
  }
  var MUSTANG = "12552 Mustang Circle, Forney, TX 75126";
  function mustangJob(){
    return makeEstimate({
      style: "mustang", wf: 48, pn: 12,
      scanLabel: MUSTANG, scanTime: "0.7s",
      layers: 1, crew: 7, days: 3, gutters: 240, miscRate: 58,
      extras: [["Ice &amp; water shield &middot; valleys + eaves", 1850], ["Steep-pitch safety rigging", 620]],
      time: "3.6 seconds."
    });
  }

  var chips = [].slice.call(document.querySelectorAll(".est-chips .chip"));

  /* the two addresses with a client-supplied aerial survey get their modelled house;
     anything else falls back to the deterministic hashed estimate */
  function jobForAddress(addr){
    if(addr === MUSTANG) return mustangJob();
    if(addr === BENT_OAK) return estateJob();
    return jobFromAddress(addr);
  }

  /* custom address dropdown: native <select> popups can't be themed to match the site.
     picking an address loads its estimate immediately, no separate Run step. */
  var addrSelect = document.getElementById("addrSelect");
  var addrBtn = document.getElementById("addrBtn");
  var addrBtnLabel = document.getElementById("addrBtnLabel");
  var addrList = document.getElementById("addrList");
  var addrOptions = [].slice.call(addrList.querySelectorAll("li"));
  var addrValue = addrOptions.length ? addrOptions[0].dataset.value : "";
  function closeAddrList(){
    addrSelect.classList.remove("open");
    addrBtn.setAttribute("aria-expanded", "false");
    addrList.hidden = true;
  }
  function openAddrList(){
    addrSelect.classList.add("open");
    addrBtn.setAttribute("aria-expanded", "true");
    addrList.hidden = false;
  }
  addrBtn.addEventListener("click", function(){
    if(addrSelect.classList.contains("open")) closeAddrList(); else openAddrList();
  });
  addrOptions.forEach(function(li){
    li.addEventListener("click", function(){
      if(!li.classList.contains("on")){
        addrOptions.forEach(function(o){ o.classList.remove("on"); o.setAttribute("aria-selected", "false"); });
        li.classList.add("on");
        li.setAttribute("aria-selected", "true");
        addrValue = li.dataset.value;
        addrBtnLabel.textContent = addrValue;
        chips.forEach(function(x){ x.setAttribute("aria-pressed", "false"); });
        renderEstimate(jobForAddress(addrValue), true);
      }
      closeAddrList();
    });
  });
  document.addEventListener("click", function(e){
    if(!addrSelect.contains(e.target)) closeAddrList();
  });
  addEventListener("keydown", function(e){
    if(e.key === "Escape" && addrSelect.classList.contains("open")){ closeAddrList(); addrBtn.focus(); }
  });

  chips.forEach(function(c){
    c.addEventListener("click", function(){
      chips.forEach(function(x){ x.setAttribute("aria-pressed", x === c ? "true" : "false"); });
      renderJob(+c.dataset.job, true);
    });
  });

  /* closer CTA: the crane card runs the live estimator up top */
  var ctaForm = document.getElementById("ctaForm");
  if(ctaForm){
    ctaForm.addEventListener("submit", function(e){
      e.preventDefault();
      var v = document.getElementById("ctaAddr").value.trim();
      if(v.length < 5){ document.getElementById("ctaAddr").focus(); return; }
      addrValue = v;
      addrBtnLabel.textContent = v;
      addrOptions.forEach(function(o){ o.classList.remove("on"); o.setAttribute("aria-selected", "false"); });
      chips.forEach(function(x){ x.setAttribute("aria-pressed", "false"); });
      renderEstimate(jobForAddress(v), true);
      var est = document.querySelector(".est");
      scrollTo({ top: est.getBoundingClientRect().top + window.scrollY - 76, behavior: reduced ? "auto" : "smooth" });
    });
  }

  /* auto-run the first estimate when the hero is visible;
     the house draws itself in with the same plotter wipe chips use */
  if(!reduced && "IntersectionObserver" in window){
    var est = document.querySelector(".est");
    var eio = new IntersectionObserver(function(entries){
      if(entries[0].isIntersecting){
        eio.disconnect();
        roof3d.wipe();
        renderJob(0, true);
      }
    }, {threshold:.3});
    eio.observe(est);
  } else renderJob(0, false);

  }
  (function(){
    var load = function(){
      import("./scene/scene.js").then(function(m){ initEstimator(m.createScene); });
    };
    if("requestIdleCallback" in window) requestIdleCallback(load, { timeout: 1500 });
    else setTimeout(load, 250);
  })();

  /* ---------- seamless ticker loop ---------- */
  var track = document.getElementById("tickerTrack");
  if(track) track.innerHTML += track.innerHTML;

  /* ---------- chaos section: phase hover-linking ---------- */
  var chaos = document.getElementById("chaos");
  if(chaos){
    var hotPhase = null;
    function setPhase(p){
      if(p === hotPhase) return;
      hotPhase = p;
      [].forEach.call(chaos.querySelectorAll(".hot"), function(n){ n.classList.remove("hot"); });
      if(p) [].forEach.call(chaos.querySelectorAll('[data-phase="' + p + '"]'), function(n){ n.classList.add("hot"); });
    }
    chaos.addEventListener("mouseover", function(ev){
      var el = ev.target.closest("[data-phase]");
      setPhase(el ? el.getAttribute("data-phase") : null);
    });
    chaos.addEventListener("mouseleave", function(){ setPhase(null); });
  }

  /* ---------- station rail: section index ---------- */
  (function(){
    var rail = document.getElementById("rail"), track = document.getElementById("railTrack");
    if(!rail || !track) return;
    var car = document.getElementById("railCar"), read = document.getElementById("railRead");
    var defs = [
      ["hero","01 · ESTIMATE"], ["compare","02 · PROOF"], ["intel","03 · INTEL"],
      ["chaos","04 · COST"], ["product","05 · PRODUCT"], ["outcomes","06 · OUTCOMES"],
      ["industries","07 · INDUSTRIES"], ["risk","08 · RISK"], ["casefile","09 · CASE FILE"],
      ["founder","10 · FOUNDER"], ["pricing","11 · PRICING"], ["terms","12 · GLOSSARY"],
      ["book","13 · BOOK"]
    ];
    var secs = defs.map(function(d){ return {el:document.getElementById(d[0]), label:d[1]}; })
      .filter(function(s){ return s.el; });
    var nodes = secs.map(function(s){
      var b = document.createElement("button");
      b.className = "rail-node"; b.type = "button"; b.title = s.label;
      b.setAttribute("aria-label", s.label);
      b.addEventListener("click", function(){
        scrollTo({top:s.el.offsetTop - 52, behavior:reduced ? "auto" : "smooth"});
      });
      track.appendChild(b);
      return b;
    });
    var tops = [], maxScroll = 1;
    function layout(){
      var doc = document.scrollingElement || document.documentElement;
      maxScroll = Math.max(1, doc.scrollHeight - innerHeight);
      /* nodes live in the SAME space as the carriage: clamped scroll position / max scroll */
      tops = secs.map(function(s){ return Math.max(0, Math.min(s.el.offsetTop - 52, maxScroll)); });
      tops.forEach(function(tp, i){ nodes[i].style.top = (tp / maxScroll * 100) + "%"; });
    }
    var carP = 0, target = 0, actLabel = "", raf = null, lastT = 0;
    function frame(now){
      var rawDt = lastT ? now - lastT : 16;
      var dt = Math.min(rawDt, 50);
      lastT = now;
      var k = rawDt > 400 ? 1 : 1 - Math.exp(-dt / 110);
      carP += (target - carP) * k;
      if(Math.abs(target - carP) < .0005) carP = target;
      car.style.top = (carP * 100) + "%";
      read.textContent = actLabel + " · " + Math.round(carP * 100) + "%";
      if(carP === target){ raf = null; lastT = 0; return; }
      raf = requestAnimationFrame(frame);
    }
    function upd(){
      var doc = document.scrollingElement || document.documentElement;
      if(Math.abs(doc.scrollHeight - innerHeight - maxScroll) > 2) layout();
      var y = window.scrollY;
      target = Math.max(0, Math.min(1, y / maxScroll));
      var act = 0;
      tops.forEach(function(tp, i){ if(y >= tp - innerHeight * .35) act = i; });
      if(maxScroll - y < 4) act = secs.length - 1;
      nodes.forEach(function(n, i){ n.classList.toggle("on", i === act); });
      actLabel = secs[act].label;
      if(raf == null){ lastT = 0; raf = requestAnimationFrame(frame); }
    }
    addEventListener("scroll", upd, {passive:true});
    addEventListener("resize", function(){ layout(); upd(); });
    addEventListener("load", function(){ layout(); upd(); });
    layout(); upd();
  })();

  /* ---------- comparison slider ---------- */
  (function(){
    var cmp = document.getElementById("cmp");
    if(!cmp) return;
    var old = document.getElementById("cmpOld");
    var oldImg = document.getElementById("cmpOldImg");
    var handle = document.getElementById("cmpHandle");
    var cue = document.getElementById("cmpCue");
    var listOld = document.getElementById("cmpListOld");
    var listNew = document.getElementById("cmpListNew");
    var pos = 50, touched = false;
    function fit(){ oldImg.style.width = cmp.getBoundingClientRect().width + "px"; }
    function apply(){
      old.style.width = pos + "%";
      handle.style.left = pos + "%";
      handle.setAttribute("aria-valuenow", Math.round(pos));
      listOld.style.opacity = Math.max(0, Math.min(1, (pos - 12) / 30));
      listNew.style.opacity = Math.max(0, Math.min(1, (88 - pos) / 30));
    }
    function setFrom(clientX){
      var r = cmp.getBoundingClientRect();
      pos = Math.max(0, Math.min(100, (clientX - r.left) / r.width * 100));
      if(!touched){ touched = true; if(cue) cue.style.opacity = "0"; }
      apply();
    }
    var down = false;
    cmp.addEventListener("pointerdown", function(e){
      down = true;
      cmp.setPointerCapture(e.pointerId);
      setFrom(e.clientX);
    });
    cmp.addEventListener("pointermove", function(e){
      /* desktop follows the cursor freely; touch requires a press */
      if(down || (finePointer && e.pointerType === "mouse")) setFrom(e.clientX);
    });
    cmp.addEventListener("pointerup", function(){ down = false; });
    cmp.addEventListener("pointercancel", function(){ down = false; });
    handle.addEventListener("keydown", function(e){
      if(e.key === "ArrowLeft"){ pos = Math.max(0, pos - 4); apply(); e.preventDefault(); }
      if(e.key === "ArrowRight"){ pos = Math.min(100, pos + 4); apply(); e.preventDefault(); }
    });
    if("ResizeObserver" in window) new ResizeObserver(fit).observe(cmp);
    else addEventListener("resize", fit);
    fit(); apply();
    /* one gentle nudge on reveal announces the seam is draggable */
    if(!reduced && "IntersectionObserver" in window){
      var nio = new IntersectionObserver(function(en){
        if(!en[0].isIntersecting) return;
        nio.disconnect();
        setTimeout(function(){
          if(touched) return;
          var t0 = null;
          (function nf(ts){
            if(touched){ pos = 50; apply(); return; }
            if(!t0) t0 = ts;
            var p = Math.min((ts - t0) / 1200, 1);
            pos = 50 + Math.sin(p * Math.PI) * 7;
            apply();
            if(p < 1) requestAnimationFrame(nf);
          })(performance.now());
        }, 600);
      }, {threshold:.55});
      nio.observe(cmp);
    }
  })();

  /* ---------- custom tooltips for the timeline nodes ---------- */
  (function(){
    var nodes = [].slice.call(document.querySelectorAll("#chaos .tl-node[title]"));
    if(!nodes.length) return;
    var tip = document.createElement("div");
    tip.className = "tip";
    document.body.appendChild(tip);
    var cur = null;
    function hide(){ tip.classList.remove("on"); cur = null; }
    function show(n){
      cur = n;
      tip.textContent = n.dataset.tip;
      tip.classList.add("on");
      var r = n.getBoundingClientRect();
      var tw = tip.offsetWidth, th = tip.offsetHeight;
      var x = Math.max(10, Math.min(innerWidth - tw - 10, r.left + r.width / 2 - tw / 2));
      var y = r.top - th - 13;
      if(y < 70) y = r.bottom + 13;
      tip.style.left = x + "px";
      tip.style.top = y + "px";
    }
    nodes.forEach(function(n){
      n.dataset.tip = n.getAttribute("title");
      n.removeAttribute("title");
      if(finePointer){
        n.addEventListener("pointerenter", function(){ show(n); });
        n.addEventListener("pointerleave", hide);
      } else {
        /* touch: tap a diamond for its story, tap again or anywhere else to dismiss */
        n.addEventListener("click", function(e){
          e.stopPropagation();
          if(cur === n) hide(); else show(n);
        });
      }
    });
    if(!finePointer) addEventListener("click", hide);
    addEventListener("scroll", hide, { passive: true });
  })();

  /* ---------- pricing: monthly / annual ---------- */
  (function(){
    var bill = document.querySelector(".bill");
    if(!bill) return;
    var chips = [].slice.call(bill.querySelectorAll(".chip"));
    var prices = [].slice.call(document.querySelectorAll(".plan .price .a[data-mo]"));
    bill.addEventListener("click", function(ev){
      var c = ev.target.closest(".chip");
      if(!c) return;
      var k = c.dataset.bill;
      chips.forEach(function(x){
        x.classList.toggle("on", x === c);
        x.setAttribute("aria-pressed", x === c ? "true" : "false");
      });
      prices.forEach(function(a){
        a.textContent = "$" + a.dataset[k];
        a.classList.remove("pop");
        void a.offsetWidth;
        a.classList.add("pop");
        var b = a.parentElement.querySelector(".b");
        if(b) b.textContent = k === "yr" ? "/user/mo · billed annually" : "/user/mo";
      });
    });
  })();

  /* ---------- photo lightbox: arrows, dots, Esc / arrow-key support ---------- */
  var LB = (function(){
    var el = document.getElementById("lb");
    if(!el) return { open: function(){}, isOpen: function(){ return false; } };
    var img = el.querySelector(".lb-img");
    var capB = el.querySelector(".lb-cap b");
    var capS = el.querySelector(".lb-cap span");
    var count = el.querySelector(".lb-count");
    var dots = el.querySelector(".lb-dots");
    var items = [], ix = 0, lastFocus = null;
    function render(){
      var it = items[ix];
      img.src = it.src;
      img.alt = it.cap + " - " + it.addr;
      capB.textContent = it.cap;
      capS.textContent = it.addr;
      count.textContent = (ix + 1) + " / " + items.length;
      [].forEach.call(dots.children, function(d, i){ d.classList.toggle("on", i === ix); });
    }
    function nav(d){ ix = (ix + d + items.length) % items.length; render(); }
    function open(list, i){
      items = list;
      ix = Math.max(0, Math.min(i || 0, list.length - 1));
      dots.innerHTML = "";
      list.forEach(function(_, di){
        var b = document.createElement("button");
        b.type = "button";
        b.setAttribute("aria-label", "Photo " + (di + 1));
        b.addEventListener("click", function(){ ix = di; render(); });
        dots.appendChild(b);
      });
      lastFocus = document.activeElement;
      el.hidden = false;
      document.body.classList.add("lb-open");
      requestAnimationFrame(function(){ el.classList.add("in"); });
      render();
      el.querySelector(".lb-x").focus();
    }
    function close(){
      el.classList.remove("in");
      document.body.classList.remove("lb-open");
      setTimeout(function(){ el.hidden = true; }, 210);
      if(lastFocus && lastFocus.focus) lastFocus.focus();
    }
    el.querySelector(".lb-x").addEventListener("click", close);
    el.querySelector(".lb-prev").addEventListener("click", function(){ nav(-1); });
    el.querySelector(".lb-next").addEventListener("click", function(){ nav(1); });
    el.addEventListener("click", function(e){ if(e.target === el) close(); });
    addEventListener("keydown", function(e){
      if(el.hidden) return;
      if(e.key === "Escape"){ close(); e.preventDefault(); }
      if(e.key === "ArrowLeft"){ nav(-1); e.preventDefault(); }
      if(e.key === "ArrowRight"){ nav(1); e.preventDefault(); }
    });
    return { open: open, isOpen: function(){ return !el.hidden; } };
  })();

  /* ---------- product demo video lightbox ---------- */
  (function(){
    var el = document.getElementById("vlb");
    if(!el) return;
    var video = document.getElementById("vlbVideo");
    var lastFocus = null;
    function open(src){
      video.src = src;
      lastFocus = document.activeElement;
      el.hidden = false;
      document.body.classList.add("lb-open");
      requestAnimationFrame(function(){ el.classList.add("in"); });
      el.querySelector(".lb-x").focus();
      video.play().catch(function(){});
    }
    function close(){
      el.classList.remove("in");
      document.body.classList.remove("lb-open");
      video.pause();
      setTimeout(function(){ el.hidden = true; video.removeAttribute("src"); video.load(); }, 210);
      if(lastFocus && lastFocus.focus) lastFocus.focus();
    }
    el.querySelector(".lb-x").addEventListener("click", close);
    el.addEventListener("click", function(e){ if(e.target === el) close(); });
    addEventListener("keydown", function(e){
      if(el.hidden) return;
      if(e.key === "Escape"){ close(); e.preventDefault(); }
    });
    /* the deck IIFE already turns Enter/Space into a synthetic click on these
       same cards, so one click listener here covers pointer and keyboard */
    [].slice.call(document.querySelectorAll(".feat[data-video]")).forEach(function(f){
      f.addEventListener("click", function(){ open(f.dataset.video); });
    });
  })();

  /* ---------- hyper-local intelligence map ---------- */
  (function(){
    var map = document.getElementById("intelMap");
    if(!map) return;
    /* ix/iy are fractions of the source photo, anchored to actual structures,
       and re-projected through the object-fit:cover crop on every resize */
    var spots = [
      { ix:.095, iy:.72, type:"commercial", status:"URGENT", statusCls:"hot", weather:"HAIL EXPECTED",
        value:"+$395K", addr:"8333 Douglas Ave, Plano, TX 75024", zone:"Commercial office tower · 9 storeys",
        issue:"Roof membrane uplift", specs:"186 sq · TPO · crane staged", risk:"2 hail events / 3 yrs",
        photos:[["Office_Roof_Top_View","Roof aerial"],["Broken_Roof_Surface","Membrane damage"]] },
      { ix:.745, iy:.295, type:"commercial", status:"MONITORING", statusCls:"dim", weather:"CLEAR",
        value:"+$1.2M", addr:"5600 Tennyson Pkwy, Plano, TX 75024", zone:"Corporate campus · 3 wings",
        issue:"Critical membrane failure", specs:"480 sq · mod-bit · garden decks", risk:"20 yrs since replacement",
        photos:[["Mall_Rooftop_HVAC_Units","HVAC systems"],["Storm_Worn_Roof","Surface wear"]] },
      { ix:.385, iy:.545, type:"commercial", status:"MONITORING", statusCls:"dim", weather:"CLEAR",
        value:"+$520K", addr:"1900 Preston Rd, Plano, TX 75024", zone:"Retail anchor · single storey",
        issue:"Ponding water accumulation", specs:"310 sq · ballasted TPO", risk:"Wet insulation · 3 yrs since drainage check",
        photos:[["Warehouse_Roof_Aerial","Aerial survey"],["Cracked_Asphalt_Shingles","Substrate cracking"]] },
      { ix:.205, iy:.315, type:"residential", status:"LOW PRIORITY", statusCls:"", weather:"CLEAR",
        value:"+$19K", addr:"7212 Covewood Dr, Plano, TX 75024", zone:"Single-family ranch · residential zone",
        issue:"Aging composite shingles", specs:"26 sq · 6/12 · laminated comp", risk:"No recent events", fico:"742", tenure:"11 yrs",
        photos:[["Sunny_Suburban_House","Street view"],["Curled_Aging_Shingles","Shingle curl"]] },
      { ix:.345, iy:.86, type:"residential", status:"ACTION REQ", statusCls:"warn", weather:"STORM WATCH",
        value:"+$74K", addr:"1409 Pecan St, Plano, TX 75024", zone:"Townhome row · 6 units · HOA scope",
        issue:"Lifted ridge caps · wind", specs:"6 units · 92 sq · 8/12", risk:"Wind event · 14 days ago", fico:"718", tenure:"6 yrs",
        photos:[["Beige_Two_Story_House","Street view"],["Roof_Inspection_Angle","Ridge detail"]] },
      { ix:.13, iy:.42, type:"residential", status:"LOW PRIORITY", statusCls:"", weather:"CLEAR",
        value:"+$15K", addr:"812 Kestrel Ln, Plano, TX 75024", zone:"Single-family · residential zone",
        issue:"Granule loss · south facets", specs:"22 sq · 4/12 · 3-tab comp", risk:"No recent events", fico:"756", tenure:"14 yrs",
        photos:[["Modern_White_Suburban_Home","Street view"],["Residential_Roof_Texture","South facet"]] },
      { ix:.30, iy:.36, type:"storm", status:"URGENT", statusCls:"hot", weather:"HAIL VERIFIED",
        value:"+$27K", addr:"950 Ridgeline Rd, Plano, TX 75024", zone:"Storm path · April 22 supercell",
        issue:"41 impact points · facet F2", specs:"30 sq · 5/12 · Class-3 comp", risk:"1.75 in hail · verified", fico:"731", tenure:"9 yrs",
        photos:[["Brick_Front_Porch_Home","Street view"],["Hail_Damaged_Shingles","Impact bruising"]] },
      { ix:.855, iy:.865, type:"storm", status:"ACTION REQ", statusCls:"warn", weather:"HAIL VERIFIED",
        value:"+$140K", addr:"2300 Custer Ct, Plano, TX 75024", zone:"Storm path · multifamily · 3 buildings",
        issue:"Soft metal dents · cap sheet scuffing", specs:"3 bldgs · 214 sq · comp + cap sheet", risk:"1.25 in hail · verified",
        photos:[["Apartment_Complex","Complex overview"],["Storm_Worn_Roof","Cap sheet wear"]] }
    ];
    var panel = {
      el: document.getElementById("intelPanel"), box: document.getElementById("intelBox"),
      body: document.getElementById("ipBody"),
      tabs: document.getElementById("ipTabs"), photos: document.getElementById("ipPhotos"),
      photoN: document.getElementById("ipPhotoN"),
      status: document.getElementById("ipStatus"), weather: document.getElementById("ipWeather"),
      value: document.getElementById("ipValue"), addr: document.getElementById("ipAddr"),
      zone: document.getElementById("ipZone"), issue: document.getElementById("ipIssue"),
      specs: document.getElementById("ipSpecs"), risk: document.getElementById("ipRisk"),
      owner: document.querySelector(".ip-owner")
    };
    var img = map.querySelector("img");
    var marks = spots.map(function(s){
      var b = document.createElement("button");
      b.className = "mk " + s.type;
      b.setAttribute("aria-label", s.addr);
      b.addEventListener("click", function(){ select(s, b); });
      map.appendChild(b);
      return b;
    });
    /* project image-space anchors through the cover crop */
    var swathEl = map.querySelector(".swath");
    var SWQ = [[.16,.22],[.30,.13],[.98,.80],[.86,.95]]; /* corridor containing both storm pins */
    function place(){
      var cw = map.clientWidth, ch = map.clientHeight;
      var iw = img.naturalWidth, ih = img.naturalHeight;
      if(!iw || !cw) return;
      var sc = Math.max(cw / iw, ch / ih);
      var ox = (cw - iw * sc) / 2, oy = (ch - ih * sc) / 2;
      function px(p){ return [p[0] * iw * sc + ox, p[1] * ih * sc + oy]; }
      spots.forEach(function(s, i){
        var x = s.ix * iw * sc + ox, y = s.iy * ih * sc + oy;
        var out = x < 14 || x > cw - 14 || y < 14 || y > ch - 14;
        marks[i].style.left = x + "px";
        marks[i].style.top = y + "px";
        marks[i].classList.toggle("crop", out);
      });
      if(swathEl){
        var q = SWQ.map(px);
        swathEl.querySelector("polygon").setAttribute("points", q.map(function(p){ return p[0].toFixed(1) + "," + p[1].toFixed(1); }).join(" "));
        var ln = swathEl.querySelector("line");
        var m1 = [(q[0][0] + q[1][0]) / 2, (q[0][1] + q[1][1]) / 2];
        var m2 = [(q[2][0] + q[3][0]) / 2, (q[2][1] + q[3][1]) / 2];
        ln.setAttribute("x1", m1[0]); ln.setAttribute("y1", m1[1]);
        ln.setAttribute("x2", m2[0]); ln.setAttribute("y2", m2[1]);
        var grad = document.getElementById("swathGrad");
        if(grad){
          grad.setAttribute("x1", m1[0]); grad.setAttribute("y1", m1[1]);
          grad.setAttribute("x2", m2[0]); grad.setAttribute("y2", m2[1]);
        }
        var tp = px([.315, .105]);
        var tx = swathEl.querySelector("text");
        tx.setAttribute("x", tp[0]); tx.setAttribute("y", tp[1]);
      }
    }
    if(img.complete) place(); else img.addEventListener("load", place);
    if("ResizeObserver" in window) new ResizeObserver(place).observe(map);
    else addEventListener("resize", place);
    function showTab(which){
      [].forEach.call(panel.tabs.querySelectorAll("button"), function(b){
        var on = b.dataset.tab === which;
        b.classList.toggle("on", on);
        b.setAttribute("aria-selected", on ? "true" : "false");
      });
      panel.body.hidden = which !== "details";
      panel.photos.hidden = which !== "photos";
    }
    function select(s, node){
      marks.forEach(function(m){ m.classList.remove("sel"); });
      node.classList.add("sel");
      panel.el.classList.add("open");
      panel.box.classList.add("panel-open");
      panel.status.textContent = s.status;
      panel.status.className = "pill " + s.statusCls;
      panel.weather.textContent = s.weather;
      panel.value.textContent = s.value;
      panel.addr.textContent = s.addr;
      panel.zone.textContent = s.zone;
      panel.issue.innerHTML = "&#9888; " + s.issue;
      panel.specs.textContent = s.specs;
      panel.risk.textContent = s.risk;
      if(panel.owner) panel.owner.style.display = s.fico ? "" : "none";
      panel.photoN.textContent = s.photos.length;
      panel.photos.innerHTML = "";
      var gallery = s.photos.map(function(p){
        return { src: "./img/props/" + p[0] + ".jpg", cap: p[1], addr: s.addr };
      });
      s.photos.forEach(function(p, pi){
        var fig = document.createElement("figure");
        fig.className = "ip-ph";
        var im = document.createElement("img");
        im.src = "./img/props/" + p[0] + ".jpg";
        im.alt = p[1] + " - " + s.addr + " (opens full size)";
        im.loading = "lazy";
        var cap = document.createElement("figcaption");
        cap.className = "tag";
        cap.textContent = p[1];
        fig.appendChild(im); fig.appendChild(cap);
        fig.addEventListener("click", function(){ LB.open(gallery, pi); });
        panel.photos.appendChild(fig);
      });
      showTab("details");
    }
    panel.tabs.addEventListener("click", function(ev){
      var b = ev.target.closest("button[data-tab]");
      if(b) showTab(b.dataset.tab);
    });
    document.getElementById("ipClose").addEventListener("click", function(){
      marks.forEach(function(m){ m.classList.remove("sel"); });
      panel.el.classList.remove("open");
      panel.box.classList.remove("panel-open");
    });
    /* schedule conflict toast */
    (function(){
      var toast = document.getElementById("conflict");
      if(!toast) return;
      function dismiss(){ toast.classList.add("gone"); }
      document.getElementById("cfDismiss").addEventListener("click", dismiss);
      document.getElementById("cfDismissX").addEventListener("click", dismiss);
      document.getElementById("cfReschedule").addEventListener("click", function(){
        document.getElementById("cfMsg").innerHTML = "Legacy West inspection moved to <b>4:30 PM</b>. Crew and homeowner notified.";
        var head = toast.querySelector(".cf-head b");
        head.textContent = "Rescheduled";
        toast.querySelector(".cf-dot").textContent = "✓";
        toast.querySelector(".cf-dot").style.cssText = "background:rgba(34,158,105,.14);color:#229E69";
        toast.style.borderLeftColor = "#229E69";
        this.remove();
        setTimeout(dismiss, 2600);
      });
    })();
    [].slice.call(document.querySelectorAll(".intel-tabs .chip")).forEach(function(tab){
      tab.addEventListener("click", function(){
        [].forEach.call(document.querySelectorAll(".intel-tabs .chip"), function(t){
          t.classList.toggle("on", t === tab);
          t.setAttribute("aria-pressed", t === tab ? "true" : "false");
        });
        var f = tab.dataset.filter;
        map.classList.toggle("swath-on", f === "storm");
        marks.forEach(function(m, i){
          m.classList.toggle("hid", f !== "all" && spots[i].type !== f);
        });
      });
    });
  })();

  /* ---------- glossary carousel: drag physics + wheel + keys ---------- */
  (function(){
    var car = document.getElementById("car");
    var track = document.getElementById("carTrack");
    var prog = document.getElementById("carProg");
    if(!car || !track) return;
    var x = 0, vx = 0, minX = 0, raf = null, down = false, lastX = 0, lastT = 0, moved = 0;
    var goal = null;
    function bounds(){
      minX = Math.min(0, car.clientWidth - track.scrollWidth);
    }
    function cardStep(){
      var c = track.children[0];
      return c ? c.getBoundingClientRect().width + 16 : 328;
    }
    function apply(){
      track.style.transform = "translateX(" + x.toFixed(1) + "px)";
      var p = minX < 0 ? x / minX : 0;
      if(prog) prog.style.left = (p * 82) + "%";
    }
    function frame(){
      if(!down){
        if(goal != null){
          /* glide to the requested card seam */
          x += (goal - x) * .16;
          if(Math.abs(goal - x) < .5){ x = goal; goal = null; vx = 0; }
        } else {
          x += vx;
          vx *= .94;
          /* rubber-band back inside the bounds */
          if(x > 0){ x += (0 - x) * .18; if(Math.abs(x) < .4 && Math.abs(vx) < .4){ x = 0; vx = 0; } }
          else if(x < minX){ x += (minX - x) * .18; if(Math.abs(x - minX) < .4 && Math.abs(vx) < .4){ x = minX; vx = 0; } }
          else if(Math.abs(vx) < .8){
            /* momentum spent: settle on the nearest card seam */
            var st = cardStep();
            goal = Math.max(minX, Math.min(0, Math.round(x / st) * st));
            vx = 0;
            if(Math.abs(goal - x) < .5){ x = goal; goal = null; }
          }
        }
      }
      apply();
      if(!down && goal == null && Math.abs(vx) < .04 && x <= 0 && x >= minX){ raf = null; return; }
      raf = requestAnimationFrame(frame);
    }
    function wake(){ if(raf == null) raf = requestAnimationFrame(frame); }
    function nudge(dir){
      var st = cardStep();
      var base = goal != null ? goal : Math.round(x / st) * st;
      goal = Math.max(minX, Math.min(0, base + dir * st));
      vx = 0;
      wake();
    }
    var prevBtn = document.getElementById("carPrev"), nextBtn = document.getElementById("carNext");
    if(prevBtn) prevBtn.addEventListener("click", function(){ nudge(1); });
    if(nextBtn) nextBtn.addEventListener("click", function(){ nudge(-1); });
    car.addEventListener("pointerdown", function(e){
      down = true; moved = 0; goal = null;
      car.classList.add("dragging");
      car.setPointerCapture(e.pointerId);
      lastX = e.clientX; lastT = performance.now();
      vx = 0;
    });
    car.addEventListener("pointermove", function(e){
      if(!down) return;
      var now = performance.now();
      var dx = e.clientX - lastX;
      moved += Math.abs(dx);
      x += dx;
      vx = dx * Math.min(1, 16 / Math.max(now - lastT, 1));
      lastX = e.clientX; lastT = now;
      wake();
    });
    function up(){ if(!down) return; down = false; car.classList.remove("dragging"); wake(); }
    car.addEventListener("pointerup", up);
    car.addEventListener("pointercancel", up);
    car.addEventListener("click", function(e){ if(moved > 6) e.preventDefault(); }, true);
    car.addEventListener("wheel", function(e){
      var d = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : (e.shiftKey ? e.deltaY : 0);
      if(!d) return;
      e.preventDefault();
      goal = null;
      x -= d;
      wake();
    }, {passive:false});
    addEventListener("keydown", function(e){
      if(document.body.classList.contains("lb-open")) return;
      if(document.activeElement && car.contains(document.activeElement) || isNear()){
        if(e.key === "ArrowLeft"){ nudge(1); }
        if(e.key === "ArrowRight"){ nudge(-1); }
      }
    });
    function isNear(){
      var r = car.getBoundingClientRect();
      return r.top < innerHeight * .8 && r.bottom > innerHeight * .2;
    }
    if("ResizeObserver" in window) new ResizeObserver(function(){ bounds(); apply(); }).observe(track);
    addEventListener("resize", function(){ bounds(); apply(); });
    bounds(); apply();
  })();

  /* ---------- live product reel: center-stage deck ---------- */
  (function(){
    var stage = document.getElementById("deckStage");
    if(!stage) return;
    var cards = [].slice.call(stage.children);
    var vids = cards.map(function(c){ return c.querySelector("video"); });
    var dots = document.getElementById("deckDots");
    var cap = document.getElementById("deckCap");
    var n = cards.length, ix = 0, touched = false, visible = false;
    /* always start from the beginning; runs at 2x by default */
    var SPEEDS = [2, 1.5, 1], speedIx = 0;
    vids.forEach(function(v){
      v.playbackRate = SPEEDS[speedIx];
      v.muted = true;          /* attributes alone are not enough on some WebKit builds */
      v.playsInline = true;
      v.addEventListener("loadedmetadata", function(){
        v.playbackRate = SPEEDS[speedIx];
      });
    });
    /* mobile autoplay can be denied (low power mode, data saver): surface a real play button */
    var playBtn = document.createElement("button");
    playBtn.className = "deck-play";
    playBtn.setAttribute("aria-label", "Play reel");
    playBtn.innerHTML = '<svg viewBox="0 0 20 20" width="22" height="22" aria-hidden="true"><path d="M6 3.5v13l11-6.5z" fill="currentColor"/></svg>';
    stage.appendChild(playBtn);
    function tryPlay(v){
      v.muted = true;
      v.playsInline = true;
      var p = v.play();
      if(p && p.then) p.then(
        function(){ stage.classList.remove("blocked"); },
        function(){ if(visible) stage.classList.add("blocked"); }
      );
    }
    playBtn.addEventListener("click", function(e){
      e.stopPropagation();
      touched = true;
      /* one gesture unlocks every reel, so later chapters play without another tap */
      vids.forEach(function(v, i){
        v.muted = true; v.playsInline = true;
        var p = v.play();
        if(p && p.then) p.then(function(){
          if(i !== ix) v.pause();
          else stage.classList.remove("blocked");
        }, function(){});
      });
    });
    /* subtle corner overlay per reel: play/pause + a cycling speed chip, defaults to 2x.
       nested inside each card's own video wrap so it rides along with that card's
       carousel transform instead of needing separate position tracking. */
    var playGlyph = '<svg viewBox="0 0 20 20" width="13" height="13" aria-hidden="true"><path d="M6 3.5v13l11-6.5z" fill="currentColor"/></svg>';
    var pauseGlyph = '<svg viewBox="0 0 20 20" width="13" height="13" aria-hidden="true"><path d="M6 4h3v12H6zM11 4h3v12h-3z" fill="currentColor"/></svg>';
    var playGlyphBig = '<svg viewBox="0 0 20 20" width="22" height="22" aria-hidden="true"><path d="M6 3.5v13l11-6.5z" fill="currentColor"/></svg>';
    var pauseGlyphBig = '<svg viewBox="0 0 20 20" width="22" height="22" aria-hidden="true"><path d="M6 4h3v12H6zM11 4h3v12h-3z" fill="currentColor"/></svg>';
    var ctls = cards.map(function(c, i){
      var wrap = c.querySelector(".deck-video-wrap");
      var v = vids[i];
      /* centered pulse: brief play/pause glyph shown when the video itself is tapped */
      var pulseEl = document.createElement("div");
      pulseEl.className = "deck-pulse";
      pulseEl.setAttribute("aria-hidden", "true");
      wrap.appendChild(pulseEl);
      var pulseTimer = null;
      function pulse(glyph){
        pulseEl.innerHTML = glyph;
        pulseEl.classList.remove("show");
        void pulseEl.offsetWidth; /* restart the animation on repeated taps */
        pulseEl.classList.add("show");
        clearTimeout(pulseTimer);
        pulseTimer = setTimeout(function(){ pulseEl.classList.remove("show"); }, 650);
      }
      wrap.addEventListener("click", function(e){
        if(i !== ix) return; /* prev/next peeks keep switching reels via the card click handler */
        e.stopPropagation();
        touched = true;
        var wasPaused = v.paused;
        if(wasPaused) tryPlay(v); else v.pause();
        syncControls();
        pulse(wasPaused ? playGlyphBig : pauseGlyphBig);
      });
      var bar = document.createElement("div");
      bar.className = "deck-controls";
      bar.innerHTML =
        '<button class="deck-ctl-btn" type="button" aria-label="Pause"></button>' +
        '<div class="deck-ctl-seek" role="slider" tabindex="0" aria-label="Seek" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">' +
          '<div class="deck-ctl-seek-track"><div class="deck-ctl-seek-fill"></div><div class="deck-ctl-seek-thumb"></div></div>' +
        '</div>' +
        '<button class="deck-ctl-speed" type="button" aria-label="Playback speed"></button>';
      wrap.appendChild(bar);
      var playBtnEl = bar.querySelector(".deck-ctl-btn");
      var speedBtnEl = bar.querySelector(".deck-ctl-speed");
      var seekEl = bar.querySelector(".deck-ctl-seek");
      var seekTrack = bar.querySelector(".deck-ctl-seek-track");
      var seekFill = bar.querySelector(".deck-ctl-seek-fill");
      var seekThumb = bar.querySelector(".deck-ctl-seek-thumb");
      playBtnEl.addEventListener("click", function(e){
        e.stopPropagation();
        touched = true;
        if(v.paused) tryPlay(v); else v.pause();
        syncControls();
      });
      speedBtnEl.addEventListener("click", function(e){
        e.stopPropagation();
        touched = true;
        speedIx = (speedIx + 1) % SPEEDS.length;
        vids.forEach(function(vv){ vv.playbackRate = SPEEDS[speedIx]; });
        syncControls();
      });
      /* seekbar: drag or tap to scrub, arrow keys to nudge */
      function renderSeek(){
        var pct = v.duration ? Math.min(1, Math.max(0, v.currentTime / v.duration)) * 100 : 0;
        seekFill.style.width = pct + "%";
        seekThumb.style.left = pct + "%";
        seekEl.setAttribute("aria-valuenow", Math.round(pct));
      }
      function seekTo(clientX){
        var r = seekTrack.getBoundingClientRect();
        if(!r.width || !v.duration) return;
        var pct = Math.min(1, Math.max(0, (clientX - r.left) / r.width));
        v.currentTime = pct * v.duration;
        renderSeek();
      }
      var dragging = false;
      seekEl.addEventListener("pointerdown", function(e){
        e.stopPropagation();
        touched = true;
        dragging = true;
        seekEl.setPointerCapture(e.pointerId);
        seekTo(e.clientX);
      });
      seekEl.addEventListener("pointermove", function(e){
        if(dragging) seekTo(e.clientX);
      });
      seekEl.addEventListener("pointerup", function(){ dragging = false; });
      seekEl.addEventListener("pointercancel", function(){ dragging = false; });
      seekEl.addEventListener("keydown", function(e){
        if(!v.duration) return;
        var step = v.duration * .02;
        if(e.key === "ArrowRight"){ touched = true; v.currentTime = Math.min(v.duration, v.currentTime + step); renderSeek(); e.preventDefault(); e.stopPropagation(); }
        if(e.key === "ArrowLeft"){ touched = true; v.currentTime = Math.max(0, v.currentTime - step); renderSeek(); e.preventDefault(); e.stopPropagation(); }
      });
      v.addEventListener("timeupdate", renderSeek);
      v.addEventListener("loadedmetadata", renderSeek);
      return { play: playBtnEl, speed: speedBtnEl };
    });
    function syncControls(){
      ctls.forEach(function(c, i){
        var v = vids[i];
        var playing = v && !v.paused;
        c.play.innerHTML = playing ? pauseGlyph : playGlyph;
        c.play.setAttribute("aria-label", playing ? "Pause" : "Play");
        c.speed.textContent = SPEEDS[speedIx] + "×";
      });
    }
    syncControls();
    /* clips stay unfetched until the section approaches */
    function loadClips(){
      vids.forEach(function(v){
        if(!v.src && v.dataset.src){
          v.preload = "metadata"; /* fetch the poster frame once we commit to the section */
          v.src = v.dataset.src;
        }
      });
    }
    if("IntersectionObserver" in window){
      var lio = new IntersectionObserver(function(en){
        if(en[0].isIntersecting){ lio.disconnect(); loadClips(); apply(); }
      }, {rootMargin: "600px 0px"});
      lio.observe(stage);
    } else loadClips();
    /* blueprint figure cards select their chapter */
    var featSel = [].slice.call(document.querySelectorAll(".feat[data-reel]"));
    function syncFeats(){
      featSel.forEach(function(f){
        var i = +f.dataset.reel;
        var on = i === ix;
        f.classList.toggle("on", on);
        f.setAttribute("aria-pressed", on ? "true" : "false");
        var link = f.querySelector(".feat-link");
        if(link) link.innerHTML = on
          ? 'Now playing <span aria-hidden="true">&#9679;</span>'
          : 'Play reel 0' + (i + 1) + ' <span aria-hidden="true">&#9656;</span>';
      });
    }
    featSel.forEach(function(f){
      f.addEventListener("click", function(){
        touched = true;
        go(+f.dataset.reel);
        var r = stage.getBoundingClientRect();
        if(r.bottom < 80) stage.scrollIntoView({ behavior: reduced ? "auto" : "smooth", block: "center" });
      });
      f.addEventListener("keydown", function(e){
        if(e.key === "Enter" || e.key === " "){ e.preventDefault(); f.click(); }
      });
    });
    var dotEls = cards.map(function(_, i){
      var b = document.createElement("button");
      b.setAttribute("role", "tab");
      b.setAttribute("aria-label", "Reel " + (i + 1));
      b.addEventListener("click", function(){ touched = true; go(i); });
      dots.appendChild(b);
      return b;
    });
    function mod(a){ return ((a % n) + n) % n; }
    function sizeStage(){
      var h = cards[ix].offsetHeight;
      if(h > 40) stage.style.height = h + "px";
    }
    function apply(){
      cards.forEach(function(c, i){
        c.classList.remove("is-active", "is-prev", "is-next");
        if(i === ix) c.classList.add("is-active");
        else if(i === mod(ix - 1)) c.classList.add("is-prev");
        else if(i === mod(ix + 1)) c.classList.add("is-next");
      });
      dotEls.forEach(function(d, i){ d.classList.toggle("on", i === ix); });
      syncFeats();
      if(cap) cap.textContent = "LIVE CAPTURE · REC 0" + (ix + 1) + " / 0" + n;
      vids.forEach(function(v, i){
        if(i === ix && visible && !reduced) tryPlay(v);
        else v.pause();
      });
      if(!visible) stage.classList.remove("blocked");
      sizeStage();
      syncControls();
    }
    function go(i){ ix = mod(i); apply(); }
    document.getElementById("deckPrev").addEventListener("click", function(){ touched = true; go(ix - 1); });
    document.getElementById("deckNext").addEventListener("click", function(){ touched = true; go(ix + 1); });
    cards.forEach(function(c, i){
      c.addEventListener("click", function(){
        if(i !== ix){ touched = true; go(i); }
      });
    });
    /* swipe */
    var sx = null;
    stage.addEventListener("pointerdown", function(e){ sx = e.clientX; });
    stage.addEventListener("pointerup", function(e){
      if(sx == null) return;
      var dx = e.clientX - sx;
      sx = null;
      if(Math.abs(dx) > 44){ touched = true; go(ix + (dx < 0 ? 1 : -1)); }
    });
    if("IntersectionObserver" in window){
      new IntersectionObserver(function(en){
        visible = en[0].isIntersecting;
        apply();
      }, {threshold:.35}).observe(stage);
    } else visible = true;
    /* projector auto-advances until the user takes the controls,
       and never while the cursor is on the deck (someone is watching) */
    var watching = false;
    var deckBox = document.getElementById("deck");
    if(deckBox){
      deckBox.addEventListener("pointerenter", function(){ watching = true; });
      deckBox.addEventListener("pointerleave", function(){ watching = false; });
    }
    if(!reduced) setInterval(function(){ if(visible && !touched && !watching) go(ix + 1); }, 9000);
    addEventListener("resize", sizeStage);
    apply();
  })();

  /* ---------- CTA mesh-warp shader: the survey grid bends toward the cursor ---------- */
  (function(){
    var cv = document.getElementById("ctaFx");
    if(!cv) return;
    var host = cv.parentElement;
    var gl = cv.getContext("webgl", {alpha:true, antialias:false, premultipliedAlpha:false});
    if(!gl){ cv.remove(); return; }
    var dpr = Math.min(devicePixelRatio || 1, 1.5);
    function sh(t, s){ var o = gl.createShader(t); gl.shaderSource(o, s); gl.compileShader(o); return o; }
    var vs = sh(gl.VERTEX_SHADER, "attribute vec2 p;void main(){gl_Position=vec4(p,0.,1.);}");
    var fs = sh(gl.FRAGMENT_SHADER, [
      "precision mediump float;uniform vec2 u_res;uniform float u_t;uniform vec2 u_m;",
      "void main(){",
      "  vec2 uv=gl_FragCoord.xy/u_res;",
      "  float asp=u_res.x/u_res.y;",
      "  vec2 p=(uv-.5)*vec2(asp,1.);",
      "  vec2 m=(u_m-.5)*vec2(asp,1.);",
      "  vec2 d=p-m;float dist=length(d);",
      "  float pull=exp(-dist*2.4);",
      "  vec2 w=p+normalize(d+vec2(1e-4))*pull*.24",
      "        +vec2(sin(u_t*.10+p.y*1.4),cos(u_t*.08+p.x*1.2))*.018;",
      "  vec2 g1=abs(fract(w*6.)-.5);",
      "  float l1=smoothstep(.5,.455,max(g1.x,g1.y));",
      "  vec2 g2=abs(fract(w*24.)-.5);",
      "  float l2=smoothstep(.5,.47,max(g2.x,g2.y))*.3;",
      "  float glow=exp(-dist*3.0);",
      "  float vg=1.-smoothstep(.42,1.02,length(p));",
      "  float a=(l1*.36+l2*.16)*(.18+glow*1.05)*vg;",
      "  vec3 col=vec3(.27,.69,.79)*a+vec3(.95,.66,.23)*glow*.045*vg;",
      "  gl_FragColor=vec4(col,min(.8,a+glow*.06));",
      "}"
    ].join("\n"));
    var pr = gl.createProgram();
    gl.attachShader(pr, vs); gl.attachShader(pr, fs); gl.linkProgram(pr); gl.useProgram(pr);
    var buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 3,-1, -1,3]), gl.STATIC_DRAW);
    var loc = gl.getAttribLocation(pr, "p");
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
    var uRes = gl.getUniformLocation(pr, "u_res");
    var uT = gl.getUniformLocation(pr, "u_t");
    var uM = gl.getUniformLocation(pr, "u_m");
    gl.enable(gl.BLEND);
    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    function rs(){
      var r = host.getBoundingClientRect();
      cv.width = Math.max(2, r.width * dpr);
      cv.height = Math.max(2, r.height * dpr);
      gl.viewport(0, 0, cv.width, cv.height);
    }
    rs(); addEventListener("resize", rs);
    var mx = .5, my = .45, tx = .5, ty = .45, lastMove = 0;
    host.addEventListener("pointermove", function(e){
      var r = host.getBoundingClientRect();
      tx = (e.clientX - r.left) / r.width;
      ty = 1 - (e.clientY - r.top) / r.height;
      lastMove = performance.now();
    });
    function paint(t){
      gl.uniform2f(uRes, cv.width, cv.height);
      gl.uniform1f(uT, t);
      gl.uniform2f(uM, mx, my);
      gl.drawArrays(gl.TRIANGLES, 0, 3);
    }
    paint(0); /* warm the compile at load, off-screen */
    if(reduced){ return; }
    var vis = false, t0 = performance.now();
    if("IntersectionObserver" in window) new IntersectionObserver(function(en){ vis = en[0].isIntersecting; }).observe(host);
    else vis = true;
    (function frame(now){
      if(vis){
        var t = (now - t0) / 1000;
        if(now - lastMove > 3000){
          tx = .5 + Math.sin(t * .23) * .3;
          ty = .48 + Math.cos(t * .17) * .22;
        }
        mx += (tx - mx) * .05;
        my += (ty - my) * .05;
        paint(t);
      }
      requestAnimationFrame(frame);
    })(t0);
  })();

  /* ---------- CTA card glare tracks the cursor ---------- */
  if(finePointer && !reduced){
    var ctaCard = document.querySelector(".cta");
    if(ctaCard){
      ctaCard.addEventListener("pointermove", function(e){
        var r = ctaCard.getBoundingClientRect();
        ctaCard.style.setProperty("--gx", ((e.clientX - r.left) / r.width * 100) + "%");
        ctaCard.style.setProperty("--gy", ((e.clientY - r.top) / r.height * 100) + "%");
      });
    }
  }
})();
