/* global React, ReactDOM */
const { useState, useEffect, useRef, useCallback } = React;

// ============================================================
//  Logo mark — official Nodo isotipo (PNG)
// ============================================================
function LogoMark({ size = 22, className = "logo-mark" }) {
  return (
    <img
      src={(window.__resources && window.__resources.nodoIcon) || "assets/nodo-icon.png"}
      alt=""
      aria-hidden="true"
      className={className}
      width={size}
      height={size}
      style={{ width: size, height: size, objectFit: "contain", display: "block" }}
    />
  );
}

function Wordmark({ size = 18 }) {
  // Use official wordmark PNG (white "nodo." on transparent)
  // Aspect ratio of source: 1507 × 475 ≈ 3.17
  const h = Math.round(size * 1.05);
  return (
    <span className="nav-brand" style={{ gap: 10 }}>
      <LogoMark size={size + 8} />
      <img
        src={(window.__resources && window.__resources.nodoWordmark) || "assets/nodo-wordmark.png"}
        alt="nodo."
        style={{ height: h, width: "auto", display: "block" }}
      />
    </span>
  );
}

// ============================================================
//  Reduced-motion hook
// ============================================================
function useReducedMotion() {
  const [reduced, setReduced] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    const update = () => setReduced(mq.matches);
    update();
    mq.addEventListener?.("change", update);
    return () => mq.removeEventListener?.("change", update);
  }, []);
  return reduced;
}

// ============================================================
//  Hero video — ping-pong pre-horneado (ida + vuelta en el archivo),
//  loop nativo. En portrait usa el recorte vertical 9:16.
// ============================================================
function usePortrait() {
  const [portrait, setPortrait] = useState(
    () => window.matchMedia("(orientation: portrait)").matches
  );
  useEffect(() => {
    const mq = window.matchMedia("(orientation: portrait)");
    const update = () => setPortrait(mq.matches);
    mq.addEventListener?.("change", update);
    return () => mq.removeEventListener?.("change", update);
  }, []);
  return portrait;
}

function PingPongHero({ src, srcPortrait, poster, posterPortrait }) {
  const videoRef = useRef(null);
  const reduced = useReducedMotion();
  const portrait = usePortrait();
  const activePoster = portrait && posterPortrait ? posterPortrait : poster;

  useEffect(() => {
    const v = videoRef.current;
    if (!v || reduced) return;

    // Si el navegador pausó el video (ahorro de energía, autoplay
    // bloqueado), lo reanuda al primer gesto o al volver a la pestaña
    const resumeIfStalled = () => {
      if (v.paused) v.play().catch(() => {});
    };

    const heroSection = v.closest(".hero") || v.parentElement;
    heroSection.addEventListener("pointermove", resumeIfStalled);
    heroSection.addEventListener("touchstart", resumeIfStalled, { passive: true });

    const onVisibility = () => {
      if (!document.hidden) resumeIfStalled();
    };
    document.addEventListener("visibilitychange", onVisibility);

    // Pausa fuera de viewport, reanuda al volver a la vista
    let io = null;
    if ("IntersectionObserver" in window) {
      io = new IntersectionObserver(
        (entries) => {
          entries.forEach((entry) => {
            if (entry.isIntersecting) {
              resumeIfStalled();
            } else {
              try { v.pause(); } catch (e) {}
            }
          });
        },
        { threshold: 0.1 }
      );
      io.observe(v);
    }

    resumeIfStalled();

    return () => {
      heroSection.removeEventListener("pointermove", resumeIfStalled);
      heroSection.removeEventListener("touchstart", resumeIfStalled);
      document.removeEventListener("visibilitychange", onVisibility);
      if (io) io.disconnect();
    };
  }, [reduced, portrait]);

  return (
    <div className="hero-media">
      {reduced ? (
        <img src={activePoster} alt="" aria-hidden="true" />
      ) : (
        <video
          key={portrait ? "portrait" : "landscape"}
          ref={videoRef}
          src={portrait && srcPortrait ? srcPortrait : src}
          poster={activePoster}
          autoPlay
          muted
          loop
          playsInline
          preload="metadata"
          aria-hidden="true"
        />
      )}
    </div>
  );
}

// ============================================================
//  Scroll reveal
// ============================================================
function useReveal() {
  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      document.querySelectorAll(".reveal").forEach((el) => el.classList.add("in"));
      return;
    }
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) {
            e.target.classList.add("in");
            io.unobserve(e.target);
          }
        });
      },
      { rootMargin: "0px 0px -10% 0px", threshold: 0.05 }
    );
    document.querySelectorAll(".reveal").forEach((el) => io.observe(el));
    return () => io.disconnect();
  }, []);
}

// Micro-parallax for glow blobs
function useParallax() {
  useEffect(() => {
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let raf = 0;
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const vh = window.innerHeight || 1;
        document.querySelectorAll(".px-glow").forEach((el) => {
          const r = el.parentElement.getBoundingClientRect();
          if (r.bottom < 0 || r.top > vh) return;
          const c = (r.top + r.height / 2 - vh / 2) / vh; // -0.5..0.5
          el.style.setProperty("--py", (c * -36).toFixed(1) + "px");
        });
      });
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => { cancelAnimationFrame(raf); window.removeEventListener("scroll", onScroll); };
  }, []);
}

// ============================================================
//  Sections
// ============================================================

function Nav() {
  const [compact, setCompact] = useState(false);
  useEffect(() => {
    let raf = 0;
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => setCompact(window.scrollY > 80));
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    return () => { cancelAnimationFrame(raf); window.removeEventListener("scroll", onScroll); };
  }, []);
  return (
    <header className={"nav" + (compact ? " compact" : "")} role="banner">
      <a href="#top" className="pill" aria-label="nodo. inicio">
        <Wordmark size={16} />
      </a>
      <nav className="pill nav-links" aria-label="Principal">
        <a href="#servicios">servicios</a>
        <a href="#trabajo">trabajo</a>
        <a href="#nosotros">nosotros</a>
        <a href="#contacto">contacto</a>
      </nav>
      <a href="#contacto" className="btn btn-primary nav-cta">
        Cotiza tu sitio web
      </a>
    </header>
  );
}

function Hero() {
  return (
    <section id="top" className="hero" data-screen-label="01 Hero">
      <PingPongHero
        src={(window.__resources && window.__resources.heroVideoLoop) || "assets/hero-loop.mp4"}
        srcPortrait={(window.__resources && window.__resources.heroVideoMobile) || "assets/hero-mobile.mp4"}
        poster={(window.__resources && window.__resources.heroPoster) || "assets/hero-poster.jpg"}
        posterPortrait={(window.__resources && window.__resources.heroPosterMobile) || "assets/hero-poster-mobile.jpg"}
      />
      <div className="hero-content">
        <div className="hero-stage">
          <div className="hero-lockup" aria-label="nodo. — Estudio digital">
            <img
              src={(window.__resources && window.__resources.nodoWordmark) || "assets/nodo-wordmark.png"}
              alt="nodo."
              className="hero-wordmark-img"
            />
          </div>
          <h1 className="hero-headline" aria-label="conecta. crea. crece.">
            <span className="word w1">conecta<span className="sep">·</span></span>
            <span className="word w2">crea<span className="sep">·</span></span>
            <span className="word w3">crece<span className="point" style={{ background: "var(--nodo-blue)" }} /></span>
          </h1>

          <div className="hero-foot">
            <div>
              <p className="hero-sub">
                Diseñamos páginas web que hacen ver a tu empresa como las
                grandes — con marca, video y foto listos para vestirla.
              </p>
              <div className="hero-cta-row">
                <a href="#contacto" className="btn btn-primary btn-lg">
                  Cotiza tu sitio web
                  <ArrowRight />
                </a>
                <a href="#trabajo" className="btn btn-ghost btn-lg">Ver trabajo</a>
              </div>
            </div>

            <div className="hero-stats" role="group" aria-label="Métricas clave">
              <div className="hero-stat">
                <span className="num"><CountUp end={50} prefix="+" duration={1800} /></span>
                <span className="lbl">proyectos lanzados</span>
              </div>
              <div className="hero-stat">
                <span className="num"><CountUp end={10} suffix=" días" duration={1400} /></span>
                <span className="lbl">entrega promedio</span>
              </div>
              <div className="hero-stat">
                <span className="num"><CountUp end={4} suffix=" en 1" duration={1200} /></span>
                <span className="lbl">marca · video · foto, en tu web</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

// Count-up number — animates on mount
function CountUp({ end, prefix = "", suffix = "", duration = 1600 }) {
  const [val, setVal] = useState(0);
  const reduced = useReducedMotion();
  useEffect(() => {
    if (reduced) { setVal(end); return; }
    let raf = 0;
    const t0 = performance.now();
    const ease = (t) => 1 - Math.pow(1 - t, 3);
    const tick = (now) => {
      const p = Math.min(1, (now - t0) / duration);
      setVal(Math.round(ease(p) * end));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [end, reduced]);
  return <span>{prefix}{val}{suffix}</span>;
}

function ArrowRight() {
  return (
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M5 12h14" />
      <path d="M13 5l7 7-7 7" />
    </svg>
  );
}

function Marquee() {
  const names = ["Vetro PVC Templados", "Grupo DST", "JE Constructora", "Iuvene", "Hasi Films", "Monnet Medical Clinic", "Promit"];
  const row = names.map((n, i) => (
    <span className="mq-item" key={i}>{n}<span className="mq-dot" /></span>
  ));
  return (
    <div className="marquee" aria-label="Clientes">
      <div className="mq-track">
        <div className="mq-row">{row}</div>
        <div className="mq-row" aria-hidden="true">{row}</div>
      </div>
    </div>
  );
}

function ScrollWords({ text, emphasisRange = null }) {
  // Split text into words while preserving spaces. Range marks which words
  // are styled with the muted/em color (matching the original <em>).
  const ref = useRef(null);
  const [progress, setProgress] = useState(0);
  const reduced = useReducedMotion();
  const words = text.split(/(\s+)/); // keep whitespace tokens
  // Build a list of word indices (skipping pure-whitespace tokens)
  const wordIndices = [];
  words.forEach((w, i) => { if (!/^\s+$/.test(w)) wordIndices.push(i); });
  const totalWords = wordIndices.length;

  useEffect(() => {
    if (reduced) { setProgress(1); return; }
    const el = ref.current;
    if (!el) return;
    let raf = 0;
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const rect = el.getBoundingClientRect();
        const vh = window.innerHeight || 1;
        // Start revealing when section's top hits ~80% of viewport,
        // finish when bottom hits ~30%.
        const start = vh * 0.85;
        const end = vh * 0.25;
        const p = (start - rect.top) / (start - end);
        setProgress(Math.max(0, Math.min(1, p)));
      });
    };
    onScroll();
    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
    };
  }, [reduced]);

  const litCount = progress * totalWords;

  let wordCounter = -1;
  return (
    <p ref={ref} className="problema-copy">
      {words.map((tok, i) => {
        if (/^\s+$/.test(tok)) return <React.Fragment key={i}>{tok}</React.Fragment>;
        wordCounter += 1;
        const wIdx = wordCounter;
        // Per-word opacity ramps over a 1-word window for smooth feel
        const local = Math.max(0, Math.min(1, litCount - wIdx));
        const isEm = emphasisRange && wIdx >= emphasisRange[0] && wIdx <= emphasisRange[1];
        // Dim base: muted slate-ish; lit: full mist (or muted-em color)
        const baseAlpha = 0.12;
        const litAlpha = isEm ? 0.42 : 0.92;
        const alpha = baseAlpha + (litAlpha - baseAlpha) * local;
        return (
          <span
            key={i}
            style={{
              color: `rgba(248, 250, 252, ${alpha})`,
              transition: "color 220ms linear",
              display: "inline-block",
            }}
          >
            {tok}
          </span>
        );
      })}
    </p>
  );
}

function Problema() {
  // Words 11–17 ("pero tu presencia digital no lo está contando.")
  // are the emphasized/muted phrase from the original <em>.
  const text =
    "Tu competencia ya se ve de primer nivel en internet. Tú sabes que tu negocio es bueno — pero tu presencia digital no lo está contando. Y coordinar tres proveedores distintos para arreglarlo cuesta tiempo, dinero y noches sin dormir.";
  // Compute em range by word index
  const allWords = text.split(/\s+/);
  const emStart = allWords.findIndex((w) => w === "pero");
  const emEnd = allWords.findIndex((w, i) => i >= emStart && w.endsWith("contando."));
  return (
    <section className="section-pad" data-screen-label="02 Problema">
      <div className="container">
        <span className="section-label reveal">El problema</span>
        <div style={{ minHeight: "60vh" }}>
          <ScrollWords text={text} emphasisRange={[emStart, emEnd]} />
        </div>
      </div>
    </section>
  );
}

function Solucion() {
  const cards = [
    {
      n: "01",
      t: "Un solo punto de contacto",
      b: "Tu sitio nace con marca, video y foto ya integrados. No coordinas tres equipos: llegas a un solo nodo.",
    },
    {
      n: "02",
      t: "Diseño que convierte",
      b: "Cada decisión visual tiene una razón: que tu cliente actúe, no solo que se vea bonito.",
    },
    {
      n: "03",
      t: "Rápido y accesible",
      b: "Entregamos en días, no meses, a un precio pensado para que cualquier empresa pueda verse en grande.",
    },
  ];
  return (
    <section id="nosotros" className="section-pad band-ink" data-screen-label="03 Solución">
      <div className="container">
        <span className="section-label reveal">La solución</span>
        <h2 className="section-title reveal" data-delay="1" style={{ maxWidth: "16ch" }}>
          Un solo nodo<span className="point" /> donde todo conecta.
        </h2>
        <div className="sol-grid">
          {cards.map((c, i) => (
            <div key={c.n} className="glass sol-card reveal" data-delay={String(i + 1)}>
              <span className="glow px-glow" />
              <span className="num">{c.n}</span>
              <h3>{c.t}</h3>
              <p>{c.b}</p>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

function IconGlobe() {
  return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M3 12h18" /><path d="M12 3a13 13 0 0 1 0 18a13 13 0 0 1 0-18Z" /></svg>;
}
function IconBrand() {
  return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="4" /><path d="M12 2v3" /><path d="M12 19v3" /><path d="M2 12h3" /><path d="M19 12h3" /><path d="M5 5l2 2" /><path d="M17 17l2 2" /><path d="M5 19l2-2" /><path d="M17 7l2-2" /></svg>;
}
function IconCamera() {
  return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M3 8a2 2 0 0 1 2-2h1.5l1.2-2h8.6l1.2 2H19a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" /><circle cx="12" cy="13" r="3.5" /></svg>;
}
function IconVideo() {
  return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="6" width="14" height="12" rx="2" /><path d="M17 10l4-2v8l-4-2z" /></svg>;
}
function IconAll() {
  return <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><circle cx="6" cy="6" r="2.5" /><circle cx="18" cy="6" r="2.5" /><circle cx="6" cy="18" r="2.5" /><circle cx="18" cy="18" r="2.5" /><path d="M8 6h8" /><path d="M8 18h8" /><path d="M6 8v8" /><path d="M18 8v8" /></svg>;
}

function Servicios() {
  return (
    <section id="servicios" className="section-pad" data-screen-label="04 Servicios">
      <div className="container">
        <span className="section-label reveal">Servicios</span>
        <h2 className="section-title reveal" data-delay="1" style={{ maxWidth: "18ch" }}>
          La web al centro. Todo lo demás la impulsa.
        </h2>
        <div className="srv-grid">
          <div className="glass srv-card featured reveal" data-delay="1">
            <span className="glow px-glow" />
            <div className="srv-head">
              <div className="srv-icon" style={{ background: "rgba(26, 109, 239, 0.18)", borderColor: "rgba(26, 109, 239, 0.5)" }}><IconGlobe /></div>
              <span className="star">★ NUESTRO FUERTE</span>
              <h3>Páginas web</h3>
            </div>
            <p>Sitios rápidos, responsivos y hechos para vender: landing pages, sitios corporativos y e-commerce. Es el centro de todo lo que hacemos — marca, video y foto existen para que el tuyo se vea en grande.</p>
          </div>
          <div className="glass srv-card reveal" data-delay="2">
            <div className="srv-head">
              <div className="srv-icon"><IconBrand /></div>
              <h3>Branding e identidad</h3>
            </div>
            <p>Logo, paleta, tipografía y manual de marca. La identidad que tu sitio estrena, coherente en todo formato.</p>
          </div>
          <div className="glass srv-card reveal" data-delay="3">
            <div className="srv-head">
              <div className="srv-icon"><IconVideo /></div>
              <h3>Video y contenido</h3>
            </div>
            <p>Producción de video, drone y contenido que le da vida a tu sitio y a tus redes.</p>
          </div>
          <div className="glass srv-card reveal" data-delay="4">
            <div className="srv-head">
              <div className="srv-icon"><IconCamera /></div>
              <h3>Fotografía</h3>
            </div>
            <p>Producto, retrato corporativo y espacios. Imágenes reales para tu web — nada de stock.</p>
          </div>
          <div className="glass srv-card reveal" data-delay="5">
            <div className="srv-head">
              <div className="srv-icon"><IconAll /></div>
              <h3>Tu web + todo lo que la viste</h3>
            </div>
            <p>Sitio, marca, video y foto en un solo proyecto coordinado. Un solo equipo, una sola entrega, un solo costo — hasta 30% menos que contratar por separado.</p>
          </div>
        </div>
      </div>
    </section>
  );
}

function Proceso() {
  return (
    <section className="section-pad" data-screen-label="05 Proceso">
      <div className="container">
        <div className="proceso-head">
          <div>
            <span className="section-label reveal">Cómo trabajamos</span>
            <h2 className="section-title reveal" data-delay="1" style={{ maxWidth: "20ch" }}>
              Sin sorpresas, sin jerga, sin esperas eternas.
            </h2>
          </div>
          <p className="proceso-caption reveal" data-delay="2">
            Tres pasos, un solo nodo. Desde la primera llamada hasta el lanzamiento,
            siempre sabes en qué punto va tu proyecto.
          </p>
        </div>

        <div className="proceso-flow">
          <div className="proceso-step reveal" data-delay="1">
            <div className="pn"><span className="nd" /><span>01</span></div>
            <h4>Conectamos</h4>
            <p>Una llamada de 30 min. Entendemos negocio, audiencia y objetivos antes de tocar un pixel.</p>
          </div>
          <div className="proceso-step reveal" data-delay="2">
            <div className="pn"><span className="nd" /><span>02</span></div>
            <h4>Creamos</h4>
            <p>Tu sitio se construye mientras marca y contenido avanzan en paralelo, no en serie. Avances visibles cada 48 hrs.</p>
          </div>
          <div className="proceso-step reveal" data-delay="3">
            <div className="pn"><span className="nd" /><span>03</span></div>
            <h4>Lanzamos</h4>
            <p>Entrega lista para usarse: dominio, hosting, manual de marca y assets descargables.</p>
          </div>
        </div>
      </div>
    </section>
  );
}

// Carga diferida confiable para las capturas del portafolio: el loading="lazy"
// nativo no dispara dentro del marco con overflow, así que se observa a mano.
const BLANK_SRC = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'/%3E";
function LazyShot({ src, alt }) {
  const ref = useRef(null);
  const [ready, setReady] = useState(false);
  useEffect(() => {
    const el = ref.current;
    if (!el || !("IntersectionObserver" in window)) { setReady(true); return; }
    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((e) => {
          if (e.isIntersecting) { setReady(true); io.disconnect(); }
        });
      },
      { rootMargin: "800px 0px" }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return <img ref={ref} src={ready ? src : BLANK_SRC} alt={alt} decoding="async" />;
}

function Trabajo() {
  const R = window.__resources || {};
  // pan = duración del recorrido al hover, proporcional al alto de cada captura
  const items = [
    { id: "monnet", t: "Monnet Medical Clinic", c: "Medicina estética", img: R.pfMonnet || "assets/portfolio/monnet.webp", pan: "7.5s", big: true },
    { id: "vetro-pvc", t: "Vetro PVC Templados", c: "Cristal templado y PVC", img: R.pfVetro || "assets/portfolio/vetro-pvc.webp", pan: "11s" },
    { id: "hasi-films", t: "Hasi Films", c: "Productora audiovisual", img: R.pfHasi || "assets/portfolio/hasi-films.webp", pan: "9.5s" },
    { id: "promit", t: "Promit", c: "Consultoría empresarial", img: R.pfPromit || "assets/portfolio/promit.webp", pan: "7s", big: true },
    { id: "je-constructora", t: "JE Constructora", c: "Obra civil y maquinaria", img: R.pfJe || "assets/portfolio/je-constructora.webp", pan: "7.5s", big: true },
    { id: "iuvene", t: "Iuvene", c: "Medicina estética facial", img: R.pfIuvene || "assets/portfolio/iuvene.webp", pan: "10s" },
    { id: "grupo-dst", t: "Grupo DST", c: "Constructora", img: R.pfDst || "assets/portfolio/grupo-dst.webp", pan: "8.5s", wide: true },
  ];
  return (
    <section id="trabajo" className="section-pad" data-screen-label="06 Trabajo">
      <div className="container">
        <span className="section-label reveal">Trabajo</span>
        <h2 className="section-title reveal" data-delay="1" style={{ maxWidth: "16ch" }}>
          Sitios que ya están conectando.
        </h2>
        <p className="work-note reveal" data-delay="2">
          Sitios reales de clientes reales, en línea hoy y vendiendo. Pasa el cursor — o desliza — para recorrer cada página completa.
        </p>
        <div className="work-grid">
          {items.map((w, i) => (
            <figure
              key={w.id}
              className={"work-card reveal" + (w.big ? " big" : "") + (w.wide ? " wide" : "")}
              data-delay={String((i % 2) + 1)}
            >
              <div className="work-media" style={{ "--pan": w.pan }}>
                <div className="frame-bar" aria-hidden="true">
                  <span className="fdot" />
                  <span className="fdot" />
                  <span className="fdot" />
                  <span className="faddr">{w.t.toLowerCase()}</span>
                </div>
                <div className="shot-scroll">
                  <LazyShot src={w.img} alt={"Sitio web de " + w.t} />
                </div>
              </div>
              <figcaption>
                <b>{w.t}</b>
                <span>{w.c}</span>
              </figcaption>
            </figure>
          ))}
        </div>
      </div>
    </section>
  );
}

function Testimonials() {
  const items = [
    {
      q: "Pasamos de no tener web a cerrar clientes desde el primer mes. El proceso fue clarísimo.",
      n: "Mariana R.",
      r: "Dueña, Estudio de Bienestar",
    },
    {
      q: "Por fin un equipo que entiende web Y video. Una sola conversación para todo.",
      n: "Carlos T.",
      r: "Director, Constructora regional",
    },
    {
      q: "Rápidos, derechos y el resultado se ve carísimo. Vale cada peso.",
      n: "Luis G.",
      r: "Fundador, Marca de alimentos",
    },
  ];
  return (
    <section className="section-pad band-ink" data-screen-label="07 Testimonios">
      <div className="container">
        <span className="section-label reveal">Lo que dicen</span>
        <h2 className="section-title reveal" data-delay="1" style={{ maxWidth: "16ch" }}>
          Empresas reales. Resultados que se ven.
        </h2>
        <div className="test-grid">
          {items.map((t, i) => (
            <div key={i} className="glass test-card reveal" data-delay={String(i + 1)}>
              <span className="qmark">"</span>
              <p className="quote">{t.q}</p>
              <div className="who">
                <b>{t.n}</b>
                {t.r}
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}

function FinalCTA() {
  return (
    <section id="contacto" className="section-pad final" data-screen-label="07 CTA Final">
      <span className="glow" />
      <div className="container">
        <h2 className="reveal">
          ¿Listo para<br />conectar<span className="point" />?
        </h2>
        <p className="final-sub reveal" data-delay="1">
          Paquetes a la medida de tu etapa. Sin paquetes inflados ni sorpresas en la factura.
        </p>
        <div className="final-cta reveal" data-delay="2">
          <a href="https://wa.me/526142868911" target="_blank" rel="noopener noreferrer" className="btn btn-primary btn-lg">
            Cotiza tu sitio web
            <ArrowRight />
          </a>
        </div>
        <p className="final-meta reveal" data-delay="3">
          Cuéntanos tu proyecto y te armamos una propuesta en 48 horas.
        </p>
      </div>
    </section>
  );
}

function Footer() {
  return (
    <footer>
      <div className="container">
        <div className="foot-grid">
          <div className="foot-brand">
            <div className="wm">
              <LogoMark size={36} />
              <img
                src={(window.__resources && window.__resources.nodoWordmark) || "assets/nodo-wordmark.png"}
                alt="nodo."
                style={{ height: 32, width: "auto", display: "block" }}
              />
            </div>
            <p>Conectamos empresas con su mejor versión digital.</p>
          </div>
          <div className="foot-col">
            <h5>Estudio</h5>
            <ul>
              <li><a href="#nosotros">Nosotros</a></li>
              <li><a href="#trabajo">Trabajo</a></li>
              <li><a href="#servicios">Servicios</a></li>
              <li><a href="#contacto">Contacto</a></li>
            </ul>
          </div>
          <div className="foot-col">
            <h5>Servicios</h5>
            <ul>
              <li><a href="#servicios">Páginas web</a></li>
              <li><a href="#servicios">Branding</a></li>
              <li><a href="#servicios">Video</a></li>
              <li><a href="#servicios">Fotografía</a></li>
              <li><a href="#servicios">Paquete integral</a></li>
            </ul>
          </div>
          <div className="foot-col">
            <h5>Contacto</h5>
            <ul>
              <li><a href="https://wa.me/526142868911" target="_blank" rel="noopener noreferrer">WhatsApp · 614 286 8911</a></li>
              <li><a href="#">@nodo.studio</a></li>
              <li><a href="#">Chihuahua, MX</a></li>
            </ul>
          </div>
        </div>
        <div className="foot-bottom">
          <span>Chihuahua, MX · Estudio digital</span>
          <span>© 2026 Nodo. Todos los derechos reservados.</span>
        </div>
      </div>
    </footer>
  );
}

// ============================================================
//  App
// ============================================================
function App() {
  useReveal();
  useParallax();
  return (
    <React.Fragment>
      <Nav />
      <main>
        <Hero />
        <Marquee />
        <Problema />
        <Solucion />
        <Servicios />
        <Proceso />
        <Trabajo />
        <Testimonials />
        <FinalCTA />
      </main>
      <Footer />
    </React.Fragment>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
