/* =========================================================== 1 A 1 — Componentes (rediseño minimal / líneas / lightbox) =========================================================== */ const I = { shop: "M3 9l1.5-4.5A2 2 0 0 1 6.4 3h11.2a2 2 0 0 1 1.9 1.5L21 9M3 9v9a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V9M3 9h18M8 9v2a2 2 0 0 1-2 2 2 2 0 0 1-2-2M16 9v2a2 2 0 0 0 2 2 2 2 0 0 0 2-2M12 9v2a2 2 0 0 1-2 2 2 2 0 0 1-2-2m8 0v2a2 2 0 0 1-2 2 2 2 0 0 1-2-2", wrench: "M14.7 6.3a4 4 0 0 0-5.4 5l-6 6a1.5 1.5 0 0 0 2 2l6-6a4 4 0 0 0 5-5.4l-2.3 2.3-2-.5-.5-2 2.6-2.4z", migrate: "M4 7h11M4 7l3-3M4 7l3 3M20 17H9m11 0l-3-3m3 3l-3 3", ai: "M12 3v3m0 12v3m9-9h-3M6 12H3m13.5-6.5L14.5 7.5m-5 9l-2 2m11 0l-2-2m-9-9l-2-2M9 9h6v6H9z", ads: "M3 11v2a1 1 0 0 0 1 1h2l4 4V6L6 10H4a1 1 0 0 0-1 1zM15 8a4 4 0 0 1 0 8M18 5a8 8 0 0 1 0 14", chart: "M4 20V10m5 10V4m5 16v-7m5 7V8M3 20h18", check: "M5 12l5 5L20 6", whatsapp: "M12 3a9 9 0 0 0-7.7 13.6L3 21l4.5-1.2A9 9 0 1 0 12 3zm0 2a7 7 0 0 1 5.9 10.8l-.3.5.6 2-2.1-.6-.5.3A7 7 0 1 1 12 5zm-2.2 3.3c-.2 0-.5.1-.7.4-.2.3-.8.8-.8 1.9s.9 2.2 1 2.4c.1.2 1.6 2.6 4 3.5 2 .8 2.4.6 2.8.6.4 0 1.3-.5 1.5-1.1.2-.6.2-1 .1-1.1l-.6-.3-1.3-.6c-.2 0-.3-.1-.5.1l-.6.8c-.1.1-.2.2-.4.1-.2-.1-.9-.4-1.7-1-.6-.6-1-1.2-1.2-1.4-.1-.2 0-.3.1-.4l.3-.4.2-.4v-.3l-.6-1.5c-.2-.4-.3-.4-.5-.4h-.6z", mail: "M3 6h18v12H3zM3 7l9 6 9-6", pin: "M12 21s-6-5.3-6-10a6 6 0 1 1 12 0c0 4.7-6 10-6 10zm0-7.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z", lock: "M7 11V8a5 5 0 0 1 10 0v3M5.5 11h13v9h-13z", arrow: "M5 12h14M13 6l6 6-6 6", "arrow-left": "M19 12H5M11 18l-6-6 6-6", "arrow-ne": "M7 17L17 7M8 7h9v9", "arrow-up": "M12 19V5M6 11l6-6 6 6", x: "M6 6l12 12M18 6L6 18", menu: "M3 12h18M3 6h18M3 18h18" }; function Icon({ name, w = 24, cls }) { return ( ); } /* Título de sección con mask-reveal: el texto sube desde detrás de la línea. El JS de reveal (app.jsx) le agrega .in al entrar en pantalla. */ function SectionTitle({ children, className }) { return (

{children}

); } const WA_LINK = "https://wa.me/526640000000"; const MAIL_LINK = "mailto:diego@1a1.mx"; const MAPS_LINK = "https://www.google.com/maps/place/SERCONTAD/@32.520156,-116.9926145,19z/"; const BRAND = ( 1·A·1 ); /* ---------------- NAV ---------------- */ function Nav({ t, lang, setLang, route }) { const [scrolled, setScrolled] = React.useState(false); const [menuOpen, setMenuOpen] = React.useState(false); const progRef = React.useRef(null); React.useEffect(() => { const onScroll = () => { setScrolled(window.scrollY > 12); const el = progRef.current; if (el) { const max = document.documentElement.scrollHeight - window.innerHeight; el.style.transform = `scaleX(${max > 0 ? Math.min(1, window.scrollY / max).toFixed(4) : 0})`; } }; window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); return () => window.removeEventListener("scroll", onScroll); }, []); const toggleMenu = () => setMenuOpen(!menuOpen); const closeMenu = () => setMenuOpen(false); React.useEffect(() => { document.body.style.overflow = menuOpen ? "hidden" : ""; return () => { document.body.style.overflow = ""; }; }, [menuOpen]); const routes = ["/trabajo", "/industrias", "/herramientas-gratuitas"]; return (
{BRAND} {t.nav.sub} {t.nav.subRole && {t.nav.subRole}}
{t.nav.cta}
); } /* ---------------- HERO ---------------- */ function Hero({ t }) { const h = t.hero; const cardRef = React.useRef(null); const onMouseMove = (e) => { const card = cardRef.current; if (!card) return; const rect = card.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const xc = rect.width / 2; const yc = rect.height / 2; // Tilt the card away from the mouse const rotateX = ((yc - y) / yc) * 8; // max 8deg const rotateY = ((x - xc) / xc) * -8; // max -8deg card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateY(-4px) scale(1.02)`; card.style.boxShadow = `0 54px 100px -40px rgba(0,0,0,.75)`; // Pan the image slot inside in the opposite direction const imgX = ((x - xc) / xc) * -12; // max -12px const imgY = ((y - yc) / yc) * -12; // max -12px const img = card.querySelector("image-slot"); if (img) { img.style.transform = `scale(1.12) translate(${imgX}px, ${imgY}px)`; } }; const onMouseLeave = () => { const card = cardRef.current; if (!card) return; card.style.transform = ""; card.style.boxShadow = ""; const img = card.querySelector("image-slot"); if (img) { img.style.transform = ""; } }; return (

{(() => { const titleText = h.title || ""; const dotIndex = titleText.indexOf(". "); if (dotIndex !== -1) { return ( {titleText.slice(0, dotIndex + 1)} {titleText.slice(dotIndex + 2)} ); } return {titleText}; })()}

{h.lead}

{h.ctaPrimary} {h.ctaSecondary}
{h.shotUrl}
{[...h.partners, ...h.partners].map((p, i) => ( = h.partners.length ? "true" : undefined}> ))}
); } /* ---------------- SHOWCASE (tarjetas en movimiento) ---------------- */ function Showcase({ t }) { const s = t.showcase; const stageRef = React.useRef(null); const cardsRef = React.useRef([]); const state = React.useRef({ rx: 0, ry: 0, drift: [0, 0, 0] }); // Intensidad de tilt por tarjeta: la central reacciona más const TILT = [0.55, 1, 0.75]; const apply = () => { const { rx, ry, drift } = state.current; cardsRef.current.forEach((el, i) => { if (!el) return; el.style.transform = `perspective(1100px) rotateX(${(rx * TILT[i]).toFixed(2)}deg) rotateY(${(ry * TILT[i]).toFixed(2)}deg) translateY(${drift[i].toFixed(1)}px)`; }); }; React.useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; const stage = stageRef.current; if (!stage) return; const onMove = (e) => { const r = stage.getBoundingClientRect(); state.current.rx = -((e.clientY - r.top) / r.height - 0.5) * 7; state.current.ry = ((e.clientX - r.left) / r.width - 0.5) * 7; apply(); }; const onLeave = () => { state.current.rx = 0; state.current.ry = 0; apply(); }; // Parallax: cada tarjeta deriva a distinta velocidad respecto al centro del viewport const SPEED = [0.05, 0.11, 0.08]; let raf = null; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = null; const r = stage.getBoundingClientRect(); const offset = r.top + r.height / 2 - window.innerHeight / 2; state.current.drift = SPEED.map((v) => -offset * v); apply(); }); }; stage.addEventListener("mousemove", onMove); stage.addEventListener("mouseleave", onLeave); window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); return () => { stage.removeEventListener("mousemove", onMove); stage.removeEventListener("mouseleave", onLeave); window.removeEventListener("scroll", onScroll); if (raf) cancelAnimationFrame(raf); }; }, []); const card = (i, cls) => (
(cardsRef.current[i] = el)}>
{s.cards[i].url}
{s.cards[i].num} {s.cards[i].name} · {s.cards[i].loc} {s.cards[i].desc}
); const titleText = s.title || ""; const dotIndex = titleText.indexOf(". "); return (

{dotIndex !== -1 ? ( {titleText.slice(0, dotIndex + 1)} {titleText.slice(dotIndex + 2)} ) : titleText}

01{s.eyebrow}

{s.para}

{s.note}

{card(0, "pos-l")} {card(1, "pos-c")} {card(2, "pos-r")}
); } /* ---------------- POR QUÉ ---------------- */ function Why({ t }) { return (
01{t.why.eyebrow} {t.why.title}

{t.why.lead}

{t.why.items.map((it, i) => (
{it.num}

{it.quote}

{it.h}

{it.p}

))}
); } /* ---------------- SERVICIOS — split editorial (aside fija + lista) ---------------- */ function Services({ t }) { const s = t.services; return (
{s.items.map((it, i) => ( {it.num}

{it.h}

{it.p}

    {it.bullets.map((b, j) =>
  • {b}
  • )}
))}
); } /* ---------------- ÍNDICE WEB — tarjetas estilo Showcase con preview flotante ---------------- */ function WebIndex({ t }) { const w = t.webIndex; const stageRef = React.useRef(null); const cardsRef = React.useRef([]); const state = React.useRef({ rx: 0, ry: 0, drift: [0, 0, 0] }); const TILT = [0.55, 1, 0.75]; const apply = () => { const { rx, ry, drift } = state.current; cardsRef.current.forEach((el, i) => { if (!el) return; el.style.transform = `perspective(1100px) rotateX(${(rx * TILT[i]).toFixed(2)}deg) rotateY(${(ry * TILT[i]).toFixed(2)}deg) translateY(${drift[i].toFixed(1)}px)`; }); }; React.useEffect(() => { if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; const stage = stageRef.current; if (!stage) return; const onMove = (e) => { const r = stage.getBoundingClientRect(); state.current.rx = -((e.clientY - r.top) / r.height - 0.5) * 7; state.current.ry = ((e.clientX - r.left) / r.width - 0.5) * 7; apply(); }; const onLeave = () => { state.current.rx = 0; state.current.ry = 0; apply(); }; const SPEED = [0.05, 0.11, 0.08]; let raf = null; const onScroll = () => { if (raf) return; raf = requestAnimationFrame(() => { raf = null; const r = stage.getBoundingClientRect(); const offset = r.top + r.height / 2 - window.innerHeight / 2; state.current.drift = SPEED.map((v) => -offset * v); apply(); }); }; stage.addEventListener("mousemove", onMove); stage.addEventListener("mouseleave", onLeave); window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); return () => { stage.removeEventListener("mousemove", onMove); stage.removeEventListener("mouseleave", onLeave); window.removeEventListener("scroll", onScroll); if (raf) cancelAnimationFrame(raf); }; }, []); const card = (i, cls) => (
(cardsRef.current[i] = el)} onClick={() => { window.pendingProjectOpen = i; window.dispatchEvent(new CustomEvent('openproject', { detail: { index: i } })); }}>
{w.cards[i].url}
{w.cards[i].type} {w.cards[i].title} {w.cards[i].company}
); return (
03{w.eyebrow} {w.title}

{w.lead}

{w.note}

{card(0, "pos-l")} {card(1, "pos-c")} {card(2, "pos-r")}
{w.cta}
); } /* ---------------- ÍNDICE WEB — listado estilo sección 01 con preview flotante ---------------- */ function WebListIndex({ t }) { const w = t.webListIndex; const allProjects = [t.gallery.featured, ...t.gallery.items]; const previewRef = React.useRef(null); const [activeIndex, setActiveIndex] = React.useState(null); const onMouseMove = (e) => { const el = previewRef.current; const cx = e.clientX; const cy = e.clientY; // Position preview centered on cursor if (el) { const width = 520; const height = 340; let x = cx - width / 2; let y = cy - height / 2; // Clamp to viewport edges with 12px margin x = Math.max(12, Math.min(x, window.innerWidth - width - 12)); y = Math.max(12, Math.min(y, window.innerHeight - height - 12)); el.style.left = `${x}px`; el.style.top = `${y}px`; } }; const handleProjectClick = (i) => { window.pendingProjectOpen = i; window.dispatchEvent(new CustomEvent('openproject', { detail: { index: i } })); }; return (
04{w.eyebrow} {w.title}

{w.lead}

{w.note}

{allProjects.map((p, i) => ( setActiveIndex(i)} onMouseLeave={() => setActiveIndex(null)} onClick={() => handleProjectClick(i)} >
{p.idx} {p.type}

{p.title}

{p.desc}

))}
{w.cta}
{/* Backdrop blur overlay – brings preview to the front */}
{allProjects.map((p, idx) => (
))}
); } /* ---------------- TRABAJO — exposición editorial (catálogo curatorial impreso) ---------------- */ function WorkExhibition({ t }) { const g = t.gallery; const all = [g.featured, ...g.items]; const [cat, setCat] = React.useState("all"); const inCat = (p) => cat === "all" || p.cat === cat; // Abre la pieza en el lightbox de la galería (montada en la misma página). const openPiece = (i) => { window.pendingProjectOpen = null; window.dispatchEvent(new CustomEvent("openproject", { detail: { index: i } })); }; // Tamaño curatorial por posición en la retícula de 12 columnas (composición asimétrica). const layout = ["exh-c-7", "exh-c-5 exh-drop", "exh-c-6", "exh-c-6 exh-drop", "exh-c-12 exh-protagonist"]; return (
{/* HERO editorial */}
05 {g.eyebrow} {g.room}

{g.title}

{g.lead}


{/* Filtros editoriales */}
{g.filters.map((f, i) => ( {i > 0 && } ))}
{/* Catálogo asimétrico — cada proyecto es una "pieza expuesta" */}
{all.map((p, i) => (
{p.type}

{p.title}

{p.desc}

{p.services ? (
{g.servicesLabel}
    {p.services.map((s, k) =>
  • {s}
  • )}
) : null} {p.tags ? (
{g.stackLabel}
    {p.tags.map((s, k) =>
  • {s}
  • )}
) : null}
))}
); } /* ---------------- DESARROLLO WEB — sala de galería (pin + scroll horizontal) ---------------- */ function Gallery({ t }) { const g = t.gallery; const all = [g.featured, ...g.items]; const [cat, setCat] = React.useState("all"); const [open, setOpen] = React.useState(false); const [idx, setIdx] = React.useState(0); const [slide, setSlide] = React.useState(0); const [glActive, setGlActive] = React.useState(false); const [closing, setClosing] = React.useState(false); const closeTimer = React.useRef(null); const sectionRef = React.useRef(null); const stageRef = React.useRef(null); const trackRef = React.useRef(null); const barRef = React.useRef(null); const countRef = React.useRef(null); const glCanvasRef = React.useRef(null); const glRef = React.useRef(null); const inCat = (p) => cat === "all" || p.cat === cat; const visible = all.filter(inCat); React.useEffect(() => { const checkPending = () => { if (typeof window.pendingProjectOpen === 'number') { const i = window.pendingProjectOpen; window.pendingProjectOpen = null; setTimeout(() => { setIdx(i); setOpen(true); setSlide((s) => s + 1); }, 150); } }; checkPending(); const handleOpen = (e) => { if (e.detail && typeof e.detail.index === 'number') { const i = e.detail.index; setIdx(i); setOpen(true); setSlide((s) => s + 1); } }; window.addEventListener('openproject', handleOpen); return () => { window.removeEventListener('openproject', handleOpen); }; }, []); // En táctil / pantallas chicas / reduced-motion la sala no se pinea: // el muro se recorre con scroll horizontal nativo (CSS). const STATIC_MQ = "(max-width: 880px), (pointer: coarse), (prefers-reduced-motion: reduce)"; // Pin + traducción: la sección mide 100vh + el recorrido del muro; // al bajar, el muro avanza hacia la derecha. React.useEffect(() => { const section = sectionRef.current, stage = stageRef.current, track = trackRef.current; if (!section || !stage || !track) return; const mq = window.matchMedia(STATIC_MQ); const n = visible.length; let dist = 0, raf = null; const apply = () => { raf = null; if (mq.matches || !dist) return; const top = section.getBoundingClientRect().top; const p = Math.min(1, Math.max(0, -top / dist)); track.style.transform = `translate3d(${(-p * dist).toFixed(1)}px, 0, 0)`; if (barRef.current) barRef.current.style.transform = `scaleX(${p.toFixed(4)})`; if (countRef.current) { const cur = Math.min(n, Math.floor(p * n) + 1); countRef.current.textContent = String(cur).padStart(2, "0") + " — " + String(n).padStart(2, "0"); } }; const measure = () => { if (mq.matches) { section.style.height = ""; track.style.transform = ""; return; } dist = Math.max(0, track.scrollWidth - stage.clientWidth); section.style.height = `calc(100vh + ${Math.round(dist)}px)`; apply(); }; const onScroll = () => { if (!raf) raf = requestAnimationFrame(apply); }; measure(); const timers = [120, 450].map((ms) => setTimeout(measure, ms)); // fuentes / layout tardío window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", measure); mq.addEventListener && mq.addEventListener("change", measure); return () => { timers.forEach(clearTimeout); window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", measure); mq.removeEventListener && mq.removeEventListener("change", measure); if (raf) cancelAnimationFrame(raf); section.style.height = ""; }; }, [cat, visible.length]); // Revelado cinematográfico por pieza (blur→foco + slice + número kinético) React.useEffect(() => { const section = sectionRef.current; if (!section) return; const pieces = Array.from(section.querySelectorAll(".gw-piece")); if (!pieces.length) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches || document.documentElement.classList.contains("motion-off"); section.classList.add("cine"); if (reduce || !window.anime) { pieces.forEach((p) => { p.classList.add("in-view"); p.querySelectorAll(".gw-plaque > *, .gw-index").forEach((el) => { el.style.opacity = 1; }); }); return; } const reveal = (piece) => { const lines = piece.querySelectorAll(".gw-plaque > *"); const index = piece.querySelector(".gw-index"); anime({ targets: lines, opacity: [0, 1], translateY: [16, 0], duration: 620, delay: anime.stagger(80, { start: 240 }), easing: "easeOutExpo" }); if (index) anime({ targets: index, opacity: [0, 1], translateY: [28, 0], duration: 1000, easing: "easeOutExpo" }); }; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting && !e.target.classList.contains("in-view")) { e.target.classList.add("in-view"); reveal(e.target); io.unobserve(e.target); } }); }, { threshold: 0.25 }); pieces.forEach((p) => io.observe(p)); return () => io.disconnect(); }, [cat]); const changeCat = (id) => setCat(id); const openAt = (p) => { const i = all.indexOf(p); setIdx(i); setOpen(true); setSlide((s) => s + 1); if (glRef.current) glRef.current.focus(i); // "la pieza se vuelve un mundo" }; const close = () => { setOpen(false); if (glRef.current) { glRef.current.unfocus(); // mantiene el lienzo a pantalla completa mientras la pieza se reduce, luego lo libera setClosing(true); clearTimeout(closeTimer.current); closeTimer.current = setTimeout(() => setClosing(false), 520); } }; const go = (dir) => { if (glRef.current) { // en GL recorremos todas las piezas const ni = (idx + dir + all.length) % all.length; setIdx(ni); setSlide((s) => s + 1); glRef.current.focus(ni); return; } const list = all.filter(inCat); const cur = Math.max(0, list.findIndex((p) => all.indexOf(p) === idx)); const ni = (cur + dir + list.length) % list.length; setIdx(all.indexOf(list[ni])); setSlide((s) => s + 1); }; React.useEffect(() => { const onKey = (e) => { if (!open) return; if (e.key === "Escape") close(); else if (e.key === "ArrowRight") go(1); else if (e.key === "ArrowLeft") go(-1); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }); React.useEffect(() => { document.body.style.overflow = open ? "hidden" : ""; return () => { document.body.style.overflow = ""; }; }, [open]); // Capa WebGL (Three.js) — sólo en el modo pineado de escritorio y con motion. // El DOM sigue siendo accesible (opacity:0, focusable); este canvas es lo visual. React.useEffect(() => { const canvas = glCanvasRef.current, stage = stageRef.current, section = sectionRef.current; if (!canvas || !stage || !section) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches || document.documentElement.classList.contains("motion-off"); if (reduce || !window.galleryGLSupported || !window.galleryGLSupported() || !window.createGalleryGL) return; const staticMq = window.matchMedia(STATIC_MQ); // Usa la imagen que el image-slot resuelve: la subida por el autor (_userUrl) o el src del asset. const resolveSrc = (p) => { const el = document.getElementById("lb-" + p.idx); return (el && (el._userUrl || el.getAttribute("src"))) || p.src; }; const items = all.map((p) => ({ src: resolveSrc(p), idx: p.idx, title: p.title })); const getProgress = () => { const top = section.getBoundingClientRect().top; const dist = section.offsetHeight - window.innerHeight; return dist > 0 ? Math.min(1, Math.max(0, -top / dist)) : 0; }; const build = () => { if (glRef.current || staticMq.matches) return; const gl = window.createGalleryGL({ canvas, items, getProgress, onPick: (i) => openAt(all[i]) }); if (gl) { glRef.current = gl; stage.classList.add("gl-on"); setGlActive(true); } }; const teardown = () => { if (glRef.current) { glRef.current.destroy(); glRef.current = null; } stage.classList.remove("gl-on"); setGlActive(false); }; const onMq = () => { staticMq.matches ? teardown() : build(); }; build(); staticMq.addEventListener && staticMq.addEventListener("change", onMq); // Imagen soltada en un slot → recarga esa textura en el WebGL al vuelo const onSlotChange = (e) => { if (!glRef.current || !e.detail || !e.detail.id) return; const pi = all.findIndex((p) => "lb-" + p.idx === e.detail.id); if (pi >= 0) glRef.current.updateTexture(pi, e.detail.url); }; document.addEventListener("imageslotchange", onSlotChange); // Pausa el render WebGL cuando la sala sale de pantalla (ahorro de batería/CPU) const vis = new IntersectionObserver((entries) => { entries.forEach((e) => { if (!glRef.current) return; e.isIntersecting ? glRef.current.resume() : glRef.current.pause(); }); }, { rootMargin: "120px" }); vis.observe(section); return () => { vis.disconnect(); document.removeEventListener("imageslotchange", onSlotChange); teardown(); staticMq.removeEventListener && staticMq.removeEventListener("change", onMq); }; }, []); // Tipografía kinética del case-study (GL) React.useEffect(() => { if (!glActive || !open || !window.anime) return; const els = document.querySelectorAll(".gw-case-body > *"); if (els.length) anime({ targets: els, opacity: [0, 1], translateY: [26, 0], duration: 720, delay: anime.stagger(70), easing: "easeOutExpo" }); }, [open, slide, glActive]); // Filtro en GL: las piezas fuera de categoría salen volando, las visibles se recolocan React.useEffect(() => { if (!glActive || !glRef.current) return; const indices = all.map((p, i) => i).filter((i) => cat === "all" || all[i].cat === cat); glRef.current.setVisible(indices); }, [cat, glActive]); const proj = all[idx]; const total = String(all.length).padStart(2, "0"); const pieceKey = (p) => (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openAt(p); } }; return (
{g.quick}
{g.room} {g.hint}
{all.map((p) => (
openAt(p)} onKeyDown={pieceKey(p)} aria-label={p.title}>
{g.pieceLabel} {p.idx}
{p.type} {p.title} {p.tags ? p.tags.join(" · ") : p.url}
))}
{g.room}

{g.endTitle}

{g.endCta}
{glActive && (
e.stopPropagation()}> {proj.idx} — {total} {proj.type}

{proj.title}

{proj.desc}

{proj.tags ?
{proj.tags.map((tg, i) => {tg})}
: null}
{g.endCta} {proj.url}
)}
e.stopPropagation()}>
{proj.url}
{proj.idx} — {total} {proj.type}

{proj.title}

{proj.desc}

{proj.tags ?
{proj.tags.map((tg, i) => {tg})}
: null}
{proj.url}
); } /* ---------------- AUDITORÍA — diagnóstico que se ensambla al scroll ---------------- */ function Audit({ t }) { const a = t.audit; const secRef = React.useRef(null); const bigRef = React.useRef(null); const zeroRef = React.useRef(null); const scanRef = React.useRef(null); const diagRef = React.useRef(null); React.useEffect(() => { const sec = secRef.current; if (!sec) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const motionOff = document.documentElement.classList.contains("motion-off"); if (reduce || motionOff || !window.anime) return; // sin motion: estado final por CSS const items = diagRef.current ? Array.from(diagRef.current.querySelectorAll("li")) : []; sec.classList.add("arm"); // oculta estados iniciales sólo si vamos a animar let played = false; const play = () => { if (played) return; played = true; const tl = anime.timeline({ easing: "easeOutExpo" }); tl.add({ targets: scanRef.current, translateY: ["-40%", "130%"], opacity: [0, 1, 1, 0], duration: 1300, easing: "easeInOutQuad" }, 0) .add({ targets: items, opacity: [0, 1], translateX: [-22, 0], duration: 560, delay: anime.stagger(85) }, 260) .add({ targets: bigRef.current, opacity: [0, 1], scale: [0.7, 1], duration: 1000, easing: "easeOutElastic(1, .55)" }, 520); if (zeroRef.current) { const obj = { v: 9 }; anime({ targets: obj, v: 0, round: 1, duration: 900, delay: 660, easing: "easeOutExpo", update: () => { if (zeroRef.current) zeroRef.current.textContent = String(obj.v); } }); } sec.classList.add("played"); }; const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { play(); io.disconnect(); } }); }, { threshold: 0.35 }); io.observe(sec); return () => io.disconnect(); }, []); // Imán sobre el $0 (sólo puntero fino) const onBigMove = (e) => { const el = bigRef.current; if (!el || !window.matchMedia("(hover: hover) and (pointer: fine)").matches) return; const r = el.getBoundingClientRect(); const dx = (e.clientX - (r.left + r.width / 2)) * 0.16; const dy = (e.clientY - (r.top + r.height / 2)) * 0.16; el.style.transform = `translate(${dx.toFixed(1)}px, ${dy.toFixed(1)}px)`; }; const onBigLeave = () => { if (bigRef.current) bigRef.current.style.transform = ""; }; return (
06{a.eyebrow} {a.title}

{a.p}

$0 {a.freeSm}
{a.cta}
{a.eyebrow}
    {a.list.map((li, i) => (
  • {li}
  • ))}
); } /* ---------------- CONTACTO ---------------- */ function Contact({ t }) { const [sent, setSent] = React.useState(false); const ch = { wa: "whatsapp", mail: "mail", pin: "pin" }; const href = { wa: WA_LINK, mail: MAIL_LINK, pin: MAPS_LINK }; return (
07{t.contact.eyebrow} {t.contact.title}

{t.contact.lead}

{t.contact.channels.map((c, i) => { const inner = ( {c.lbl}{c.val} ); return href[c.k] ? {inner} :
{inner}
; })}
{t.contact.sede.label} {t.contact.sede.title}

{t.contact.sede.note}

{ e.preventDefault(); setSent(true); }}>

{sent ? t.contact.form.ok : ""}

{t.contact.form.note}

); } /* ---------------- FOOTER ---------------- */ /* ---------------- Página "Próximamente" (Industrias / Herramientas) ---------------- */ function ComingSoon({ soon, data }) { return (
{soon.eyebrow}

{data.title}

{data.text}

{soon.cta} {soon.back}
); } function Footer({ t }) { return ( ); } /* ---------------- FAB de chat (WhatsApp) ---------------- */ function ChatFab() { return ( ); } Object.assign(window, { Icon, Nav, Hero, Showcase, Why, Services, WebIndex, WebListIndex, Gallery, Audit, Contact, Footer, ChatFab });