// client-tips.jsx — ABC Client Guide — "The Field Guide" edition
// Standalone page — does NOT depend on app.jsx.

const { useEffect, useRef, useState, useContext, createContext, useCallback } = React;
const ABC = window.AC_DATA;
const Ic  = window.Icon;
const t   = (val, lang) => window.AC_T(val, lang);

// ── Language ──────────────────────────────────────────────────────────
const CTLangContext = createContext({ lang: "en", setLang: () => {} });
const useLang = () => useContext(CTLangContext);

const LangProvider = ({ children }) => {
  const [lang, setLangState] = useState(() => {
    try {
      const q = new URLSearchParams(window.location.search).get("lang");
      if (q === "en" || q === "ar") return q;
      const s = localStorage.getItem("ac_lang");
      if (s === "en" || s === "ar") return s;
      if (typeof navigator !== "undefined" && navigator.language?.toLowerCase().startsWith("ar")) return "ar";
    } catch (e) {}
    return "en";
  });
  useEffect(() => {
    document.documentElement.lang = lang;
    document.documentElement.dir = lang === "ar" ? "rtl" : "ltr";
    document.documentElement.dataset.lang = lang;
    try { localStorage.setItem("ac_lang", lang); } catch (e) {}
  }, [lang]);
  return <CTLangContext.Provider value={{ lang, setLang: setLangState }}>{children}</CTLangContext.Provider>;
};

// ── Helpers ───────────────────────────────────────────────────────────
const L = (en, ar) => ({ en, ar });
const g = (obj, lang) => (lang === "ar" ? obj.ar : obj.en);

const useInView = (threshold = 0.14) => {
  const ref = useRef(null);
  const [inView, setInView] = useState(false);
  useEffect(() => {
    const el = ref.current; if (!el) return;
    const obs = new IntersectionObserver(([e]) => {
      if (e.isIntersecting) { setInView(true); obs.disconnect(); }
    }, { threshold });
    obs.observe(el);
    return () => obs.disconnect();
  }, []);
  return [ref, inView];
};

const useScrollProgress = () => {
  const [p, setP] = useState(0);
  useEffect(() => {
    const cb = () => {
      const h = document.documentElement;
      const max = h.scrollHeight - h.clientHeight;
      setP(max > 0 ? Math.min(1, h.scrollTop / max) : 0);
    };
    window.addEventListener("scroll", cb, { passive: true }); cb();
    return () => window.removeEventListener("scroll", cb);
  }, []);
  return p;
};

// ── Guide content ─────────────────────────────────────────────────────
const INTRO = {
  title_a: L("Build with ", "ابدأ "),
  title_em: L("Foresight", "على بيّنة."),
  lead: L(
    "What our team has learned from forty-five years on Dubai's sites, set down plainly.",
    "خمسة وأربعون عاما في مواقع دبي علّمتنا دروسا. اقرأها قبل أن تبدأ، فهي عدّتك قبل أوّل حجر."
  ),
  measure_val: L("Lessons from the field", "خبرة في الميدان"),
};

const TIPS_HEAD = {
  eyebrow: L("From our team", "من فريقنا"),
  title_a: L("Ten lessons, ", "عشرة دروس، "),
  title_em: L("before the first brick.", "قبل أوّل حجر."),
  lead: L(
    "These are the lessons we share first with our valued clients:",
    "هذه الدروس التي نشاركها أولا مع عملائنا الكرام:"
  ),
  note_label: L("Our practice", "من ممارستنا"),
};

const TIPS = [
  {
    num: "01", num_ar: "01",
    tag: L("The brief", "البداية"),
    head: L("Begin with how your family lives", "ابدأ من عيشة أهل البيت"),
    line: L(
      "The majlis, the guests, the school-morning rush, the long nights of Ramadan. A good home is drawn around your real days, not around a facade.",
      "المجلس، والضيوف، وزحمة صباح المدرسة، وليالي رمضان الطويلة. البيت الطيّب ترسمه أيامكم الحقيقية، لا الواجهة."
    ),
    note: L(
      "We build homes that work the way families truly live in them.",
      "نبني بيوتا تسير على إيقاع الأهل الذين يسكنونها."
    ),
  },
  {
    num: "02", num_ar: "02",
    tag: L("The budget", "الميزانية"),
    head: L("Give your budget room to breathe", "اترك لميزانيتك متنفّسا"),
    line: L(
      "Set aside a comfortable margin from the first day. It turns the surprises that any build can meet into small, simple decisions instead of painful compromises later on.",
      "خصّص هامشا مريحا من أول يوم. البناء لا يخلو من مفاجآت، وهامشك هو ما يحوّلها إلى قرارات صغيرة، لا تنازلات مؤلمة."
    ),
    note: L(
      "We put the real numbers in front of you plainly, from the very first meeting.",
      "نضع الأرقام الحقيقية أمامكم بوضوح، من أول لقاء."
    ),
  },
  {
    num: "03", num_ar: "03",
    tag: L("The land", "الأرض"),
    head: L("Let the plot shape the design", "دع الأرض تشكّل التصميم"),
    line: L(
      "Where the sun falls, where the wind comes from, where the neighbours' windows look, and the direction of the qibla. These should guide the villa before the first line is drawn.",
      "أين تقع الشمس، ومن أين تهبّ الريح، وأين تطلّ نوافذ الجيران، واتجاه القبلة. كلّ هذا يجب أن يشكّل الفيلا قبل رسم أول خط."
    ),
    note: L(
      "The soil and the water table beneath a plot shape its foundations. We account for them before we build.",
      "تربة الأرض ومنسوب الماء تحتها يحدّدان الأساسات. نحسب لها حسابها قبل أن نبني."
    ),
  },
  {
    num: "04", num_ar: "04",
    tag: L("The people", "الثقة"),
    head: L("Choose who you trust with your home", "اختر من تأتمنه على بيتك"),
    line: L(
      "You, your consultant and your contractor make the decisions together. Within the family, agree early who speaks for the home. Many opinions make a rich brief; many instructions make a confused site.",
      "أنت واستشاريك ومقاولك تتخذون القرارات معا. وداخل العائلة، اتّفقوا مبكرا على من يتحدّث باسم البيت. الآراء الكثيرة تغني التصور، أما التعليمات الكثيرة فتربك الموقع."
    ),
    note: L(
      "Most of our work comes to us through families recommending us to one another. We earn the relationship before we earn the villa.",
      "أغلب أعمالنا تأتينا بتوصية الأهل بعضهم لبعض. نكسب الثقة قبل أن نكسب الفيلا."
    ),
  },
  {
    num: "05", num_ar: "05",
    tag: L("The build", "البناء"),
    head: L("Ask what lies behind the finish", "اسأل عمّا خلف التشطيب"),
    line: L(
      "Structure, waterproofing, insulation: the parts you don't see are the ones that decide how well your home ages.",
      "الهيكل، والعزل المائي، والعزل الحراري: الأجزاء التي لا تراها هي التي تقرّر كيف يكبر بيتك مع السنين."
    ),
    note: L(
      "We keep the core trades in our own hands, so the hidden work meets our own standard.",
      "نبقي الحرف الأساسية بأيدي فريقنا، ليخرج العمل المخفيّ على مستوانا نحن."
    ),
  },
  {
    num: "06", num_ar: "06",
    tag: L("The permits", "الموافقات"),
    head: L("Give the approvals their time", "أعط الموافقات وقتها"),
    line: L(
      "Approvals pass through Dubai's authorities one step at a time. A realistic schedule makes room for them from the start, so the waiting rarely catches you by surprise.",
      "الموافقات تمرّ بين جهات دبي خطوة خطوة. والجدول الواقعي يفسح لها المكان من البداية، حتى لا يفاجئك الانتظار."
    ),
    note: L(
      "Forty-five years working alongside Dubai's authorities have taught us where the time is truly saved.",
      "خمسة وأربعون عاما من العمل مع جهات دبي علّمتنا أين نوفر الوقت حقا."
    ),
  },
  {
    num: "07", num_ar: "07",
    tag: L("The drawings", "الرسومات"),
    head: L("Decide it on paper, not on site", "احسمها على الورق لا في الموقع"),
    line: L(
      "A change made on paper costs the stroke of a pencil. The same change, once the walls are up, costs time, money and peace of mind.",
      "التغيير على الورق يكلّف خطّ قلم. والتغيير نفسه بعد أن تقوم الجدران يكلّف وقتا ومالا وراحة بال."
    ),
    note: L(
      "A fully resolved set of drawings is the best foundation we can build from.",
      "مجموعة رسومات مكتملة ومحسومة هي خير أساس ننطلق منه في البناء."
    ),
  },
  {
    num: "08", num_ar: "08",
    tag: L("The materials", "المواد"),
    head: L("Judge the sample, not the catalogue", "احكم على العيّنة لا على الكتالوج"),
    line: L(
      "A material looks one way on a screen, another in a showroom, and another again in your own home. See it at full size, in your own light, before it is ordered.",
      "المادة تبدو على الشاشة غيرها في المعرض، وغيرها في بيتك أنت. شاهدها بحجمها الكامل وتحت ضوء بيتك قبل أن تطلبها."
    ),
    note: L(
      "We build a full sample and agree it with you before we carry it through the rest of the home.",
      "نبني عينة كاملة ونتّفق عليها معك قبل أن نمضي بها في باقي البيت."
    ),
  },
  {
    num: "09", num_ar: "09",
    tag: L("The comfort", "الراحة"),
    head: L("Plan comfort and privacy early", "خطّط للراحة والخصوصية مبكرا"),
    line: L(
      "Cooling that holds through August, shade where you need it, and privacy from overlooking windows. This is the comfort you feel day after day.",
      "تكييف يصمد في حرّ أغسطس، وظلّ حيث تحتاجه، وخصوصية من النوافذ المطلّة. هذه هي الراحة التي تعيشها يوما بعد يوم."
    ),
    note: L(
      "We route the services and insulation before the finishes go on: comfort is built into the walls, not added afterwards.",
      "نمدّ الخدمات والعزل قبل التشطيبات: الراحة مبنيّة في الجدران، لا مضافة بعدها."
    ),
  },
  {
    num: "10", num_ar: "10",
    tag: L("The decades", "بيت العمر"),
    head: L("Build the home of a lifetime", "اجعله بيت العمر"),
    line: L(
      "A home should hold your family today, and your children long after you. Choose what will still feel right, and still make you proud, twenty years from now.",
      "البيت يتّسع لأهلك اليوم، ولعيالك من بعدك. اختر ما يبقى جميلا، وما يبقى مفخرة لك، بعد عشرين سنة."
    ),
    note: L(
      "The children of our first clients now come to us with plots of their own. That is the test we build for.",
      "عيال عملائنا الأوائل يأتوننا اليوم بأراضيهم. وهذا هو الاختبار الذي نبني لأجله."
    ),
  },
];

const CTA_COPY = {
  eyebrow: L("Start the conversation", "ابدأ المحادثة"),
  title_a: L("A home worth living in ", "بيت العمر "),
  title_em: L("begins with a conversation.", "يبدأ بمحادثة."),
  sub: L(
    "Tell us what you are building. Your enquiry reaches our team directly. Where there is a fit, we plan together.",
    "خبّرنا عمّا تنوي بناءه. استفسارك يصل إلى فريقنا مباشرة. وحين نجد التوافق، نخطط معا."
  ),
  btn: L("Get in touch", "تواصل معنا"),
};

// ── UI Scaffolding ────────────────────────────────────────────────────
const LangToggle = () => {
  const { lang, setLang } = useLang();
  return (
    <div className="lang-toggle" role="group" aria-label="Language">
      <button className={lang === "en" ? "active" : ""} onClick={() => setLang("en")} aria-pressed={lang === "en"} lang="en">EN</button>
      <span className="lang-toggle__sep" aria-hidden="true">·</span>
      <button className={lang === "ar" ? "active" : ""} onClick={() => setLang("ar")} aria-pressed={lang === "ar"} lang="ar">AR</button>
    </div>
  );
};

const TopBar = () => {
  const { lang } = useLang();
  const [scrolled, setScrolled] = useState(false);
  const [menuOpen, setMenuOpen] = useState(false);
  const closeMenu = () => setMenuOpen(false);
  useEffect(() => {
    const cb = () => setScrolled(window.scrollY > 24);
    window.addEventListener("scroll", cb, { passive: true });
    return () => window.removeEventListener("scroll", cb);
  }, []);
  return (
    <header className={`topbar${scrolled ? " topbar--scrolled" : ""}${menuOpen ? " topbar--menu" : ""}`}>
      <div className="topbar__inner">
        <a className="topbar__brand" href="/" aria-label="Albanna Conwood">
          <img className="brand-logo brand-logo--color"
            src={lang === "ar" ? "assets/logo-ar-color.png?v=23" : "assets/logo-en-color.png?v=23"}
            alt="Albanna Conwood Construction L.L.C — Since 1981" />
          <img className="brand-logo brand-logo--mark" src="assets/logo-mark.png?v=23" alt="" aria-hidden="true" />
        </a>
        <nav className={`topbar__nav${menuOpen ? " is-open" : ""}`} aria-label="Page navigation">
          <a href="/the-practice.html" onClick={closeMenu}>{lang === "ar" ? "منهجنا" : "The Practice"}</a>
          <a href="/client-guide.html" className="active" onClick={closeMenu}>{lang === "ar" ? "دليل المالك" : "Client Guide"}</a>
        </nav>
        <div className="topbar__actions">
          <LangToggle />
          <button
            type="button"
            className="topbar__burger"
            aria-label={lang === "ar" ? "القائمة" : "Menu"}
            aria-expanded={menuOpen}
            onClick={() => setMenuOpen((o) => !o)}
          >
            <span /><span /><span />
          </button>
        </div>
      </div>
      <div className="topbar__scrim" aria-hidden="true" onClick={closeMenu} />
    </header>
  );
};

const Footer = () => {
  const { lang } = useLang();
  return (
    <footer className="footer">
      <div className="footer__bar">
        <div className="footer__brand">
          <img className="brand-logo brand-logo--color"
            src={lang === "ar" ? "assets/logo-ar-color.png?v=23" : "assets/logo-en-color.png?v=23"}
            alt="Albanna Conwood Construction L.L.C" />
          <img className="brand-logo brand-logo--white"
            src={lang === "ar" ? "assets/logo-ar-white.png?v=23" : "assets/logo-en-white.png?v=23"}
            alt="Albanna Conwood Construction L.L.C" />
        </div>
        <span className="footer__copy">{t(ABC.UI.footer_copyright, lang)}</span>
      </div>
    </footer>
  );
};

const ProgressBar = () => {
  const p = useScrollProgress();
  return <div className="cg-progress" style={{ transform: `scaleX(${p})` }} aria-hidden="true" />;
};

// ── Foresight field — drifting triangulated sightlines (a surveyor's net) ──
// Slow-floating points that link into faint emerald sightlines when near —
// a plan quietly forming in the air, mapping the ground ahead.
const ForesightField = () => {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    const host = canvas && canvas.parentElement;
    if (!canvas || !host) return;
    const ctx = canvas.getContext("2d");
    const reduce = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    const rnd = (a, b) => a + Math.random() * (b - a);
    const PI2 = Math.PI * 2;

    // depth-layer constants  0=far  1=mid  2=near
    const LINK      = [112, 148, 182];  // moderate — enough to form clusters, not a web
    const D_ALPHA   = [0.36, 0.62, 0.90]; // base dot opacity
    const D_SPEED   = [0.38, 0.68, 1.00]; // velocity scale
    const D_RADIUS  = [[0.9, 1.6], [1.8, 2.5], [2.8, 4.2]]; // dot size range
    const D_GLOW    = [2, 5, 10];       // shadowBlur base

    let w = 0, h = 0, pts = [], raf = 0;
    let signals = [], nextFire = 0, adj = [], lastT = 0;
    let mx = -9999, my = -9999;
    const MR = 148; // mouse influence radius

    // Precise per-zone exclusion matching actual text layout
    // Each zone: test fn + repulsion vector that pushes OUT of that zone
    const ZONES = [
      { t: (x,y) => x < w*0.62 && y > h*0.03 && y < h*0.30, fx:  0.030, fy: -0.010 }, // title
      { t: (x,y) => x < w*0.50 && y > h*0.27 && y < h*0.52, fx:  0.030, fy:  0.005 }, // lead
      { t: (x,y) => x > w*0.10 && x < w*0.82 && y > h*0.50 && y < h*0.70, fx: 0, fy: -0.040 }, // timeline label
    ];
    const anyZone = (x, y) => ZONES.some(z => z.t(x, y));

    // Spawn a single node, optionally pinned to a y-band
    const spawn = (layer, yLo, yHi, hub) => {
      const sp = D_SPEED[layer];
      let x, y, tries = 0;
      do {
        x = rnd(0, w);
        y = rnd(yLo != null ? yLo : 0, yHi != null ? yHi : h);
        tries++;
      } while (anyZone(x, y) && tries < 24);
      return {
        x, y, layer, hub: !!hub,
        vx: rnd(-0.05,0.06)*sp, vy: rnd(-0.04,0.04)*sp,
        dvx: rnd(-0.018,0.030)*sp, dvy: rnd(-0.018,0.016)*sp,
        ax1: rnd(0.30,0.95)*sp, fx1: rnd(0.00026,0.00068), px1: rnd(0,PI2),
        ay1: rnd(0.20,0.75)*sp, fy1: rnd(0.00020,0.00060), py1: rnd(0,PI2),
        ax2: rnd(0.10,0.42)*sp, fx2: rnd(0.00052,0.00125), px2: rnd(0,PI2),
        ay2: rnd(0.10,0.32)*sp, fy2: rnd(0.00048,0.00115), py2: rnd(0,PI2),
        brF: rnd(0.00042,0.00088), brP: rnd(0,PI2),
        r: hub ? rnd(3.6,5.8) : rnd(...D_RADIUS[layer]),
        baseOp: hub ? 0.88 : D_ALPHA[layer],
        lit: 0,
      };
    };

    const build = () => {
      const rect = host.getBoundingClientRect();
      w = Math.max(1, rect.width); h = Math.max(1, rect.height);
      canvas.width  = Math.round(w * dpr);
      canvas.height = Math.round(h * dpr);
      canvas.style.width  = w + "px";
      canvas.style.height = h + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

      const n = Math.round(Math.max(36, Math.min(52, (w * h) / 11000)));
      pts = [];

      // Hub anchor nodes — 3–4 large near-layer anchors
      const hc = Math.max(3, Math.round(n * 0.08));
      for (let i = 0; i < hc; i++) {
        const yLo = i < Math.ceil(hc/2) ? 0      : h * 0.52;
        const yHi = i < Math.ceil(hc/2) ? h * 0.52 : h;
        pts.push(spawn(2, yLo, yHi, true));
      }

      // 35% guaranteed below timeline (y > 55%)
      const lc = Math.round((n - hc) * 0.35);
      for (let i = 0; i < lc; i++) {
        pts.push(spawn(i % 3, h * 0.55, h, false));
      }

      // Remaining: full canvas
      const rem = n - pts.length;
      for (let i = 0; i < rem; i++) {
        const layer = i < rem * 0.33 ? 0 : i < rem * 0.67 ? 1 : 2;
        pts.push(spawn(layer, null, null, false));
      }

      // Sort far→near for z-order consistency
      pts.sort((a, b) => a.layer - b.layer);
      signals = [];
      adj = [];
      nextFire = performance.now() + rnd(700, 1500);
    };

    const render = (t) => {
      ctx.clearRect(0, 0, w, h);

      // — connections: sort by distance, cap per-node so each node joins ≤3-5 lines —
      const CAP = [4, 5, 6]; // max connections per [far, mid, near/hub]
      const connCt = new Uint8Array(pts.length);
      // gather candidates within LINK range
      const cands = [];
      for (let i = 0; i < pts.length; i++) {
        const a = pts[i];
        for (let j = i + 1; j < pts.length; j++) {
          const b = pts[j];
          if (Math.abs(a.layer - b.layer) > 1) continue;
          const maxD = LINK[Math.min(a.layer, b.layer)];
          const dx = a.x - b.x, dy = a.y - b.y;
          const d2 = dx*dx + dy*dy;
          if (d2 < maxD*maxD) cands.push([i, j, Math.sqrt(d2)]);
        }
      }
      cands.sort((a, b) => a[2] - b[2]); // shortest first → most natural network

      // adjacency for neuron signal routing (rebuilt every frame)
      adj = Array.from({ length: pts.length }, () => []);

      for (const [i, j, d] of cands) {
        const a = pts[i], b = pts[j];
        const capA = a.hub ? 6 : CAP[a.layer];
        const capB = b.hub ? 6 : CAP[b.layer];
        if (connCt[i] >= capA || connCt[j] >= capB) continue;
        connCt[i]++; connCt[j]++;
        adj[i].push(j); adj[j].push(i);

        const maxD = LINK[Math.min(a.layer, b.layer)];
        const prox = 1 - d / maxD;
        const da   = (D_ALPHA[a.layer] + D_ALPHA[b.layer]) * 0.5;
        const lit  = Math.max(a.lit, b.lit);
        const hub  = a.hub || b.hub;

        const alpha = Math.min(0.78, prox * da * 0.50 + lit * 0.30 + (hub ? prox * 0.18 : 0));
        if (alpha < 0.010) continue;

        if (hub && alpha > 0.06) {
          const g = ctx.createLinearGradient(a.x, a.y, b.x, b.y);
          g.addColorStop(0, "rgba(78,131,124," + (a.hub ? alpha : alpha*0.50).toFixed(3) + ")");
          g.addColorStop(1, "rgba(78,131,124," + (b.hub ? alpha : alpha*0.50).toFixed(3) + ")");
          ctx.strokeStyle = g;
        } else {
          ctx.strokeStyle = "rgba(78,131,124," + alpha.toFixed(3) + ")";
        }
        ctx.lineWidth = prox > 0.60 ? 1.2 * da : 0.55 * da;
        ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
      }

      // — dots (sorted far→near; skip if inside a text zone so dots never cover text) —
      for (const p of pts) {
        if (anyZone(p.x, p.y)) continue; // lines may pass through, dots never render over text
        const breath = Math.sin(t * p.brF + p.brP) * 0.11;
        const ao = Math.min(1, p.baseOp + p.lit * (1 - p.baseOp) * 0.55 + breath);
        const ar = p.r + p.lit * 1.8;

        if (p.hub) {
          ctx.beginPath(); ctx.arc(p.x, p.y, ar * 3.0, 0, PI2);
          const gr = ctx.createRadialGradient(p.x, p.y, ar * 0.4, p.x, p.y, ar * 3.0);
          gr.addColorStop(0, "rgba(78,131,124," + (ao * 0.18).toFixed(3) + ")");
          gr.addColorStop(1, "rgba(78,131,124,0)");
          ctx.fillStyle = gr; ctx.fill();
        }

        ctx.shadowColor = "rgba(78,131,124," + (0.22 + p.lit * 0.46 + (p.hub ? 0.14 : 0)).toFixed(2) + ")";
        ctx.shadowBlur  = D_GLOW[p.layer] + p.lit * (p.hub ? 20 : p.layer === 2 ? 14 : p.layer === 1 ? 8 : 4);
        ctx.beginPath(); ctx.arc(p.x, p.y, ar, 0, PI2);
        ctx.fillStyle = "rgba(78,131,124," + ao.toFixed(3) + ")";
        ctx.fill();

        if ((p.layer === 2 || p.hub) && ar > 2.4) {
          ctx.shadowBlur = 0;
          ctx.beginPath(); ctx.arc(p.x - ar*0.22, p.y - ar*0.22, ar*0.33, 0, PI2);
          ctx.fillStyle = "rgba(255,255,255," + (0.18 + p.lit*0.14).toFixed(2) + ")";
          ctx.fill();
        }
      }
      ctx.shadowBlur = 0;

      // Neuron signals: a soft spark gliding along a connection, comet tail behind
      for (const s of signals) {
        const a = pts[s.from], b = pts[s.to];
        if (!a || !b) continue;
        const ease = s.prog < 0.5
          ? 2 * s.prog * s.prog
          : 1 - Math.pow(-2 * s.prog + 2, 2) / 2; // easeInOutQuad — synaptic glide
        const hx = a.x + (b.x - a.x) * ease;
        const hy = a.y + (b.y - a.y) * ease;
        // fade in on departure, fade out on arrival
        const vis = Math.min(1, s.prog * 6, (1 - s.prog) * 6 + 0.35);

        const tt = Math.max(0, ease - 0.16);
        const tx = a.x + (b.x - a.x) * tt;
        const ty = a.y + (b.y - a.y) * tt;
        const tail = ctx.createLinearGradient(tx, ty, hx, hy);
        tail.addColorStop(0, "rgba(78,131,124,0)");
        tail.addColorStop(1, "rgba(78,131,124," + (0.42 * vis).toFixed(3) + ")");
        ctx.strokeStyle = tail;
        ctx.lineWidth = 1;
        ctx.beginPath(); ctx.moveTo(tx, ty); ctx.lineTo(hx, hy); ctx.stroke();

        ctx.shadowColor = "rgba(78,131,124,0.75)";
        ctx.shadowBlur = 9;
        ctx.beginPath(); ctx.arc(hx, hy, 1.7, 0, PI2);
        ctx.fillStyle = "rgba(96,148,140," + (0.85 * vis).toFixed(3) + ")";
        ctx.fill();
        ctx.shadowBlur = 0;
        ctx.beginPath(); ctx.arc(hx, hy, 0.7, 0, PI2);
        ctx.fillStyle = "rgba(236,244,242," + (0.8 * vis).toFixed(3) + ")";
        ctx.fill();
      }
      ctx.shadowBlur = 0;
    };

    // Fire a neuron signal from node index `from` to 1–2 of its neighbours
    const fireFrom = (from, hop, exclude) => {
      if (signals.length >= 26) return;
      const nbrs = (adj[from] || []).filter(n => n !== exclude);
      if (nbrs.length === 0) return;
      const count = nbrs.length > 1 && Math.random() < 0.35 ? 2 : 1;
      for (let k = 0; k < count && nbrs.length > 0; k++) {
        const idx = Math.floor(Math.random() * nbrs.length);
        const to = nbrs.splice(idx, 1)[0];
        signals.push({ from, to, prog: 0, hop });
      }
    };

    const step = (t) => {
      const dt = lastT ? Math.min(48, t - lastT) : 16.67;
      lastT = t;

      // Neuron firing: a signal leaves a node and travels its connections
      if (!reduce && t >= nextFire && pts.length > 0) {
        const hubIdxs = pts.reduce((a, p, i) => (p.hub && a.push(i), a), []);
        const src = hubIdxs.length > 0 && Math.random() > 0.3
          ? hubIdxs[Math.floor(Math.random() * hubIdxs.length)]
          : Math.floor(Math.random() * pts.length);
        pts[src].lit = Math.min(1, pts[src].lit + 0.65);
        fireFrom(src, 0, -1);
        nextFire = t + rnd(1400, 3200);
      }

      // Advance signals at constant speed; on arrival, light node + maybe chain
      if (signals.length > 0) {
        const next = [];
        for (const s of signals) {
          const a = pts[s.from], b = pts[s.to];
          if (!a || !b) continue;
          const d = Math.max(8, Math.hypot(b.x - a.x, b.y - a.y));
          s.prog += (dt * 0.085) / d;
          if (s.prog >= 1) {
            b.lit = Math.min(1, b.lit + 0.85);
            if (s.hop < 3 && Math.random() < 0.62 - s.hop * 0.14) {
              fireFrom(s.to, s.hop + 1, s.from);
            }
          } else {
            next.push(s);
          }
        }
        signals = next;
      }

      for (const p of pts) {
        const driftX = Math.sin(t*p.fx1+p.px1)*p.ax1 + Math.sin(t*p.fx2+p.px2)*p.ax2;
        const driftY = Math.sin(t*p.fy1+p.py1)*p.ay1 + Math.sin(t*p.fy2+p.py2)*p.ay2;

        p.vx += (p.dvx - p.vx) * 0.004;
        p.vy += (p.dvy - p.vy) * 0.004;

        // Mouse repulsion
        const mdx = p.x - mx, mdy = p.y - my;
        const md  = Math.sqrt(mdx*mdx + mdy*mdy);
        if (md < MR && md > 0.5) {
          const mf = (1 - md/MR) * 0.032;
          p.vx += (mdx/md)*mf; p.vy += (mdy/md)*mf;
          p.lit = Math.min(1, p.lit + 0.055*(1 - md/MR));
        } else {
          p.lit = Math.max(0, p.lit - 0.024);
        }

        // Precise per-zone repulsion
        for (const z of ZONES) {
          if (z.t(p.x, p.y)) { p.vx += z.fx; p.vy += z.fy; }
        }

        p.vx = Math.max(-0.26, Math.min(0.26, p.vx));
        p.vy = Math.max(-0.24, Math.min(0.24, p.vy));
        p.x += p.vx + driftX * 0.013;
        p.y += p.vy + driftY * 0.013;

        if (p.x < -18) p.x = w+18; else if (p.x > w+18) p.x = -18;
        if (p.y < -18) p.y = h+18; else if (p.y > h+18) p.y = -18;
      }

      render(t);
      raf = requestAnimationFrame(step);
    };

    const onMove = (e) => {
      const r = canvas.getBoundingClientRect();
      mx = e.clientX - r.left; my = e.clientY - r.top;
    };
    const onLeave = () => { mx = -9999; my = -9999; };

    build();
    if (reduce) render(0);
    else raf = requestAnimationFrame(step);

    let ro;
    if (window.ResizeObserver) {
      ro = new ResizeObserver(() => { build(); if (reduce) render(0); });
      ro.observe(host);
    } else {
      window.addEventListener("resize", build);
    }
    const onVis = () => {
      if (reduce) return;
      cancelAnimationFrame(raf);
      if (!document.hidden) raf = requestAnimationFrame(step);
    };
    document.addEventListener("visibilitychange", onVis);
    host.addEventListener("mousemove", onMove);
    host.addEventListener("mouseleave", onLeave);

    return () => {
      cancelAnimationFrame(raf);
      if (ro) ro.disconnect(); else window.removeEventListener("resize", build);
      document.removeEventListener("visibilitychange", onVis);
      host.removeEventListener("mousemove", onMove);
      host.removeEventListener("mouseleave", onLeave);
    };
  }, []);
  return <canvas ref={ref} className="cg-hero__field" aria-hidden="true" />;
};

// ── Hero ──────────────────────────────────────────────────────────────
const Hero = ({ lang }) => {
  const isAr = lang === "ar";
  return (
    <section className="cg-hero" dir={isAr ? "rtl" : "ltr"}>
      <ForesightField />
      <div className="cg-hero__inner">
        <h1 className="cg-hero__title">
          <span>{g(INTRO.title_a, lang)}</span>
          <em className="it">{g(INTRO.title_em, lang)}</em>
        </h1>
        <p className="cg-hero__lead">{g(INTRO.lead, lang)}</p>

        <div className="cg-hero__measure" aria-hidden="true">
          <span className="cg-hero__measure-cap">1981</span>
          <span className="cg-hero__measure-line">
            <span className="cg-hero__measure-val">{g(INTRO.measure_val, lang)}</span>
          </span>
          <span className="cg-hero__measure-cap">2026</span>
        </div>
      </div>
    </section>
  );
};


// ── Tip icons — thin-stroke, one per tip ──────────────────────────────
// Classes: .drw = stroke draws on (needs pathLength), .dsh = dashed guide
// (fades in), .dot = filled dot (pops in). Draw order = DOM order.
const TIP_ICONS = [
  /* 01 — open door: daily life shapes the plan */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <rect className="drw" pathLength="100" x="18" y="9" width="26" height="46"/>
    <line className="drw" pathLength="100" x1="44" y1="9" x2="56" y2="15"/>
    <line className="drw" pathLength="100" x1="56" y1="15" x2="56" y2="61"/>
    <line className="drw" pathLength="100" x1="44" y1="55" x2="56" y2="61"/>
    <circle className="dot" cx="51" cy="38" r="1.4" fill="currentColor" stroke="none"/>
  </svg>,
  /* 02 — balance scale: budget with margin */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <line className="drw" pathLength="100" x1="32" y1="14" x2="32" y2="52"/>
    <circle className="drw" pathLength="100" cx="32" cy="11" r="2.4"/>
    <line className="drw" pathLength="100" x1="12" y1="20" x2="52" y2="20"/>
    <line className="drw" pathLength="100" x1="12" y1="20" x2="7"  y2="32" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="12" y1="20" x2="17" y2="32" strokeWidth="0.8"/>
    <path className="drw" pathLength="100" d="M5 32 a7 7 0 0 0 14 0"/>
    <line className="drw" pathLength="100" x1="52" y1="20" x2="47" y2="32" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="52" y1="20" x2="57" y2="32" strokeWidth="0.8"/>
    <path className="drw" pathLength="100" d="M45 32 a7 7 0 0 0 14 0"/>
    <line className="drw" pathLength="100" x1="24" y1="52" x2="40" y2="52"/>
  </svg>,
  /* 03 — plot with sun: read the land */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <rect className="drw" pathLength="100" x="9" y="23" width="33" height="32"/>
    <line className="dsh" x1="9" y1="39" x2="24" y2="39" strokeWidth="0.8" strokeDasharray="3 2.2"/>
    <line className="dsh" x1="24" y1="39" x2="24" y2="55" strokeWidth="0.8" strokeDasharray="3 2.2"/>
    <circle className="drw" pathLength="100" cx="51" cy="13" r="5.5"/>
    <line className="drw" pathLength="100" x1="51" y1="3"  x2="51" y2="5.5" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="61" y1="13" x2="58.5" y2="13" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="58" y1="6"  x2="56.3" y2="7.7" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="44" y1="20" x2="45.7" y2="18.3" strokeWidth="0.8"/>
  </svg>,
  /* 04 — triangle with nodes: owner, consultant, contractor */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path className="drw" pathLength="100" d="M32 13 L55 50 L9 50 Z"/>
    <circle className="drw" pathLength="100" cx="32" cy="13" r="3.2" fill="var(--bg, #f2f3f4)"/>
    <circle className="drw" pathLength="100" cx="55" cy="50" r="3.2" fill="var(--bg, #f2f3f4)"/>
    <circle className="drw" pathLength="100" cx="9"  cy="50" r="3.2" fill="var(--bg, #f2f3f4)"/>
  </svg>,
  /* 05 — wall cutaway layers: how it is built */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <rect className="drw" pathLength="100" x="14" y="10" width="36" height="44"/>
    <line className="drw" pathLength="100" x1="26" y1="10" x2="26" y2="54" strokeWidth="0.8"/>
    <line className="dsh" x1="32" y1="10" x2="32" y2="54" strokeWidth="0.8" strokeDasharray="3 2.2"/>
    <line className="drw" pathLength="100" x1="38" y1="10" x2="38" y2="54" strokeWidth="0.8"/>
  </svg>,
  /* 06 — hourglass: approvals take their season */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <line className="drw" pathLength="100" x1="18" y1="10" x2="46" y2="10"/>
    <line className="drw" pathLength="100" x1="18" y1="54" x2="46" y2="54"/>
    <path className="drw" pathLength="100" d="M20 10 Q20 27 32 32 Q44 37 44 54"/>
    <path className="drw" pathLength="100" d="M44 10 Q44 27 32 32 Q20 37 20 54"/>
    <circle className="dot" cx="32" cy="48" r="1.4" fill="currentColor" stroke="none"/>
  </svg>,
  /* 07 — sheet with pencil: decide on paper */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path className="drw" pathLength="100" d="M16 7 L37 7 L46 16 L46 57 L16 57 Z"/>
    <path className="drw" pathLength="100" d="M37 7 L37 16 L46 16"/>
    <line className="drw" pathLength="100" x1="23" y1="28" x2="39" y2="28" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="23" y1="36" x2="34" y2="36" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="23" y1="49" x2="40" y2="44" strokeWidth="1"/>
    <line className="drw" pathLength="100" x1="40" y1="44" x2="43.5" y2="42.2" strokeWidth="0.8"/>
  </svg>,
  /* 08 — overlapping swatches: see the sample */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <rect className="dsh" x="11" y="11" width="27" height="27" strokeDasharray="3 2.2" strokeWidth="0.9"/>
    <rect className="drw" pathLength="100" x="25" y="25" width="28" height="28"/>
    <line className="drw" pathLength="100" x1="30" y1="48" x2="48" y2="30" strokeWidth="0.8"/>
  </svg>,
  /* 09 — sun over louvres: the quiet comforts */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path className="drw" pathLength="100" d="M22 26 a10 10 0 0 1 20 0"/>
    <line className="drw" pathLength="100" x1="32" y1="8" x2="32" y2="11" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="17" y1="13" x2="19" y2="15" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="47" y1="13" x2="45" y2="15" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="14" y1="36" x2="50" y2="36"/>
    <line className="drw" pathLength="100" x1="18" y1="45" x2="46" y2="45"/>
    <line className="drw" pathLength="100" x1="22" y1="54" x2="42" y2="54"/>
  </svg>,
  /* 10 — gable on the horizon: think in decades */
  <svg className="cg-tip__svg" viewBox="0 0 64 64" fill="none" stroke="currentColor" strokeWidth="1" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <line className="drw" pathLength="100" x1="6" y1="50" x2="58" y2="50"/>
    <path className="drw" pathLength="100" d="M18 50 L18 36 L32 24 L46 36 L46 50"/>
    <circle className="drw" pathLength="100" cx="53" cy="13" r="4.5"/>
    <line className="drw" pathLength="100" x1="53" y1="4.5" x2="53" y2="6.5" strokeWidth="0.8"/>
    <line className="drw" pathLength="100" x1="61.5" y1="13" x2="59.5" y2="13" strokeWidth="0.8"/>
  </svg>,
];

// ── A single tip ──────────────────────────────────────────────────────
const Tip = ({ tip, i, lang }) => {
  const [ref, inView] = useInView(0.2);
  const num = lang === "ar" ? tip.num_ar : tip.num;
  return (
    <li ref={ref} className={`cg-tip${inView ? " is-in" : ""}`}>
      <span className="cg-tip__station" aria-hidden="true">
        <span className="cg-tip__station-node" />
      </span>
      <div className="cg-tip__plate" aria-hidden="true">
        <span className="cg-tip__num">{num}</span>
        <div className="cg-tip__icon">{TIP_ICONS[i]}</div>
      </div>
      <div className="cg-tip__body">
        <div className="cg-tip__meta">
          <span className="cg-tip__accent" aria-hidden="true" />
          <span className="cg-tip__tag">{g(tip.tag, lang)}</span>
        </div>
        <h3 className="cg-tip__heading">{g(tip.head, lang)}</h3>
        <p className="cg-tip__line">{g(tip.line, lang)}</p>
      </div>
    </li>
  );
};

const Tips = ({ lang }) => {
  const isAr = lang === "ar";
  const [headRef, headIn] = useInView(0.3);
  const listRef = useRef(null);

  // Scroll-linked dimension spine + restrained ghost parallax.
  // rAF writes a CSS custom property + transforms; no React re-renders.
  // All geometry reads are batched before any writes to avoid layout thrash.
  useEffect(() => {
    const list = listRef.current;
    if (!list) return;
    const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    if (mq.matches) { list.style.setProperty("--spine-p", "1"); return; }

    let raf = 0;
    const update = () => {
      raf = 0;
      const vh = window.innerHeight || document.documentElement.clientHeight;
      const rect = list.getBoundingClientRect();
      const denom = Math.max(1, rect.height - vh * 0.42);
      const p = clamp((vh * 0.72 - rect.top) / denom, 0, 1);
      list.style.setProperty("--spine-p", p.toFixed(4));
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll, { passive: true });
    update();
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
      if (raf) cancelAnimationFrame(raf);
    };
  }, [lang]);

  return (
    <section className="cg-tips" dir={isAr ? "rtl" : "ltr"}>
      <div className="cg-tips__inner">
        <div ref={headRef} className={`cg-tips__head${headIn ? " is-in" : ""}`}>
          <div className="cg-tips__eyebrow">
            <span className="rule" aria-hidden="true" />
            {g(TIPS_HEAD.eyebrow, lang)}
          </div>
          <h2 className="cg-tips__title">
            <span>{g(TIPS_HEAD.title_a, lang)}</span>
            <em className="it">{g(TIPS_HEAD.title_em, lang)}</em>
          </h2>
          <p className="cg-tips__lead">{g(TIPS_HEAD.lead, lang)}</p>
        </div>
        <div ref={listRef} className="cg-tips__list">
          <span className="cg-tips__spine" aria-hidden="true">
            <span className="cg-tips__spine-fill" />
            <span className="cg-tips__spine-head" />
          </span>
          <ol className="cg-tips__grid">
            {TIPS.map((tip, i) => (
              <Tip key={tip.num} tip={tip} i={i} lang={lang} />
            ))}
          </ol>
        </div>
      </div>
    </section>
  );
};

// ── CTA ───────────────────────────────────────────────────────────────
const CTA = ({ lang }) => {
  const isAr = lang === "ar";
  const [ref, inView] = useInView(0.25);
  return (
    <section ref={ref} className={`cg-cta${inView ? " is-in" : ""}`} dir={isAr ? "rtl" : "ltr"}>
      <div className="cg-cta__inner">
        <span className="cg-cta__eyebrow">
          <span className="rule" aria-hidden="true" />
          <span>{g(CTA_COPY.eyebrow, lang)}</span>
        </span>
        <h2 className="cg-cta__title">
          <span>{g(CTA_COPY.title_a, lang)}</span>
          <em className="it">{g(CTA_COPY.title_em, lang)}</em>
        </h2>
        <p className="cg-cta__sub">{g(CTA_COPY.sub, lang)}</p>
        <a
          href={lang === "ar"
            ? "https://wa.me/97145146919?text=%D9%85%D8%B1%D8%AD%D8%A8%D8%A7%D8%8C%20%D8%A3%D9%88%D8%AF%20%D8%A7%D9%84%D8%AA%D9%88%D8%A7%D8%B5%D9%84%20%D8%A8%D8%B4%D8%A3%D9%86%20%D9%85%D8%B4%D8%B1%D9%88%D8%B9%20%D8%A8%D9%86%D8%A7%D8%A1."
            : "https://wa.me/97145146919?text=Hello%2C%20I%20would%20like%20to%20discuss%20a%20construction%20project."}
          className="cg-cta__btn"
          target="_blank"
          rel="noopener noreferrer"
        >
          <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" style={{flexShrink:0}}>
            <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/>
            <path d="M12 0C5.373 0 0 5.373 0 12c0 2.127.558 4.122 1.532 5.855L.054 23.454a.5.5 0 0 0 .492.608l5.777-1.513A11.945 11.945 0 0 0 12 24c6.627 0 12-5.373 12-12S18.627 0 12 0zm0 22c-1.891 0-3.667-.5-5.2-1.373l-.373-.217-3.43.899.916-3.34-.237-.386A9.937 9.937 0 0 1 2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10z"/>
          </svg>
          {g(CTA_COPY.btn, lang)}
        </a>
      </div>
    </section>
  );
};

// ── Error boundary + App ──────────────────────────────────────────────
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { error: null }; }
  static getDerivedStateFromError(e) { return { error: e }; }
  render() {
    if (this.state.error) return (
      <div style={{ padding: "80px 40px", fontFamily: "sans-serif", color: "#1a1918" }}>
        <p style={{ fontWeight: 700 }}>Something went wrong loading this page.</p>
        <pre style={{ fontSize: 13, color: "#888", marginTop: 12 }}>{this.state.error.message}</pre>
      </div>
    );
    return this.props.children;
  }
}

const ClientGuideApp = () => {
  const { lang } = useLang();
  return (
    <>
      <ProgressBar />
      <TopBar />
      <main>
        <Hero lang={lang} />
        <Tips lang={lang} />
        <CTA lang={lang} />
      </main>
      <Footer />
    </>
  );
};

const cgRoot = document.getElementById("root");
if (cgRoot && !document.querySelector('[data-page="home"]')) {
  ReactDOM.createRoot(cgRoot).render(
    <ErrorBoundary>
      <LangProvider>
        <ClientGuideApp />
      </LangProvider>
    </ErrorBoundary>
  );
}
