/* ========== La Chismosa components v2 ========== */ /* ---------- i18n (ES / EN) ---------- */ const LANG_KEY = "lachismosa-lang"; function getLang() { try { return localStorage.getItem(LANG_KEY) === "en" ? "en" : "es"; } catch (e) { return "es"; } } function setLang(l) { try { localStorage.setItem(LANG_KEY, l); } catch (e) {} document.documentElement.lang = l; window.dispatchEvent(new Event("langchange")); } // Devuelve la cadena según el idioma actual. Uso: t("Hola", "Hello") function t(es, en) { return getLang() === "en" ? en : es; } // Hook: suscribe el componente a cambios de idioma para que se vuelva a renderizar. function useLang() { const [lang, setL] = React.useState(getLang()); React.useEffect(() => { const h = () => setL(getLang()); window.addEventListener("langchange", h); document.documentElement.lang = getLang(); return () => window.removeEventListener("langchange", h); }, []); return lang; } /* ---------- Flecha noreste ↗ (reutilizable) ---------- */ function ArrowNE({ size = 18 }) { return ( ); } /* ---------- Custom Cursor — trazo crayón en página ---------- */ /* ---------- Custom Cursor — punto + anillo elegante ---------- */ function Cursor() { const ringRef = React.useRef(null); const dotRef = React.useRef(null); React.useEffect(() => { if (!window.matchMedia || !window.matchMedia("(pointer: fine)").matches) return; const ring = ringRef.current, dot = dotRef.current; const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const ease = reduced ? 1 : 0.18; const targets = "a, button, input, textarea, [data-cursor], .connect-badge, .lbw-col, .holding-card, .press-card, .ws-card, .carousel-card, .office-item"; let mx = -100, my = -100, rx = -100, ry = -100, raf, shown = false; document.documentElement.classList.add("ec-active"); const onMove = (e) => { mx = e.clientX; my = e.clientY; if (!shown) { shown = true; ring.style.opacity = "1"; dot.style.opacity = "1"; } dot.style.transform = `translate3d(${mx}px,${my}px,0) translate(-50%,-50%)`; }; const onLeave = () => { shown = false; ring.style.opacity = "0"; dot.style.opacity = "0"; }; const onOver = (e) => { if (e.target.closest && e.target.closest(targets)) ring.classList.add("ec-hover"); }; const onOut = (e) => { if (e.target.closest && e.target.closest(targets) && !(e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(targets))) ring.classList.remove("ec-hover"); }; // ¿el fondo bajo el cursor es oscuro/similar al cursor? → cambiar de color const bgIsDark = (x, y) => { const stack = document.elementsFromPoint(x, y); let node = stack.find(el => !el.closest(".ec-ring, .ec-dot, .nav, .connect-badge")); while (node && node !== document.body && node !== document.documentElement) { const ds = node.dataset && node.dataset.navbg; if (ds) return ds === "dark"; const m = getComputedStyle(node).backgroundColor.match(/rgba?\(([^)]+)\)/); if (m) { const p = m[1].split(",").map(parseFloat); const a = p[3] === undefined ? 1 : p[3]; if (a >= 0.3) return (0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]) / 255 < 0.5; } node = node.parentElement; } return false; }; let tick = 0; const loop = () => { rx += (mx - rx) * ease; ry += (my - ry) * ease; ring.style.transform = `translate3d(${rx}px,${ry}px,0) translate(-50%,-50%)`; if (shown && (tick++ % 6) === 0) { document.documentElement.classList.toggle("ec-on-dark", bgIsDark(mx, my)); } raf = requestAnimationFrame(loop); }; loop(); window.addEventListener("mousemove", onMove, { passive: true }); window.addEventListener("mouseleave", onLeave); document.addEventListener("mouseover", onOver); document.addEventListener("mouseout", onOut); return () => { cancelAnimationFrame(raf); document.documentElement.classList.remove("ec-active"); document.documentElement.classList.remove("ec-on-dark"); window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseleave", onLeave); document.removeEventListener("mouseover", onOver); document.removeEventListener("mouseout", onOut); }; }, []); return ( <>
); } /* ---------- Navigation ---------- */ function Nav({ active }) { const lang = useLang(); const [scrolled, setScrolled] = React.useState(false); const [invert, setInvert] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { const decide = () => { setScrolled(window.scrollY > 40); const nav = ref.current; if (!nav) return; // ¿Qué hay detrás de la barra? Decidimos texto claro u oscuro según el fondo. const els = document.elementsFromPoint(Math.round(window.innerWidth / 2), nav.offsetHeight + 4); const target = els.find(el => !el.closest(".nav, .ec-ring, .ec-dot, .connect-badge")); let light = false, node = target; while (node && node !== document.body && node !== document.documentElement) { const ds = node.dataset && node.dataset.navbg; if (ds) { light = ds === "light"; break; } const m = getComputedStyle(node).backgroundColor.match(/rgba?\(([^)]+)\)/); if (m) { const p = m[1].split(",").map(parseFloat); const a = p[3] === undefined ? 1 : p[3]; if (a >= 0.3) { light = (0.299 * p[0] + 0.587 * p[1] + 0.114 * p[2]) / 255 > 0.6; break; } } node = node.parentElement; } setInvert(light); }; window.addEventListener("scroll", decide, { passive: true }); window.addEventListener("resize", decide); decide(); return () => { window.removeEventListener("scroll", decide); window.removeEventListener("resize", decide); }; }, []); const links = [ { id: "home", label: t("HOME", "HOME"), href: "La Chismosa.html" }, { id: "nosotros", label: t("NOSOTROS", "ABOUT"), href: "nosotros.html" }, { id: "servicios", label: t("SERVICIOS", "SERVICES"), href: "servicios.html" }, { id: "archivo", label: t("ARCHIVO", "ARCHIVE"), href: "archivo.html" }, { id: "contacto", label: t("CONTACTO", "CONTACT"), href: "contacto.html" }, ]; return (
laChismosa
|
); } /* ---------- Intro / persiana ---------- */ function Intro({ onDone }) { const N = 10; const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const strips = el.querySelectorAll(".intro-strip"); const half = N / 2; strips.forEach((s, i) => { const dist = Math.abs(i - (half - 0.5)); s.style.setProperty("--d", `${(dist / half) * 0.3}s`); s.style.transformOrigin = i < half ? "top center" : "bottom center"; }); const t1 = setTimeout(() => el.classList.add("opening"), 900); const t2 = setTimeout(() => onDone?.(), 900 + 680 + 300 + 120); return () => { clearTimeout(t1); clearTimeout(t2); }; }, []); return (
{Array.from({ length: N }).map((_, i) => (
))}
La Chismosa
); } /* ---------- Invisible ink ---------- */ function InkText({ children }) { const ref = React.useRef(null); const heat = React.useRef(0); const rafRef = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; const onMove = (e) => { const r = el.getBoundingClientRect(); const dist = Math.hypot(e.clientX - (r.left + r.width / 2), e.clientY - (r.top + r.height / 2)); const radius = Math.max(r.width * 0.5, 80) + 170; const target = Math.max(0, 1 - dist / radius); cancelAnimationFrame(rafRef.current); const tick = () => { heat.current += (target - heat.current) * 0.08; el.style.setProperty("--heat", heat.current.toFixed(3)); if (Math.abs(heat.current - target) > 0.002) rafRef.current = requestAnimationFrame(tick); }; tick(); }; window.addEventListener("mousemove", onMove, { passive: true }); return () => { window.removeEventListener("mousemove", onMove); cancelAnimationFrame(rafRef.current); }; }, []); return {children}; } /* ---------- Redacted text ---------- */ function Redacted({ children, delay = 0 }) { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; el.style.setProperty("--rd", `${delay}s`); const io = new IntersectionObserver(([entry]) => { if (!entry.isIntersecting) return; io.disconnect(); el.classList.add("revealed"); }, { threshold: 0.9, rootMargin: "0px 0px -120px 0px" }); io.observe(el); return () => io.disconnect(); }, [delay]); return ( {children} ); } /* ---------- HERO V2 — Bloque 1 ---------- */ // Posición y radio del sello dentro del frame del vídeo, normalizados (0–1 sobre el ancho). // Como el vídeo se recorta con object-fit:cover, calculamos dónde cae el sello en cada // viewport (escritorio, tablet, móvil) en lugar de fijar porcentajes por media query. const HERO_SEAL = { x: 0.486, y: 0.579, r: 0.019 }; function Hero() { const lang = useLang(); const heroRef = React.useRef(null); const videoRef = React.useRef(null); const openRef = React.useRef(null); const [opened, setOpened] = React.useState(false); // Sitúa el botón sobre el sello calculando el recorte cover del vídeo en este viewport. React.useEffect(() => { const place = () => { const sec = heroRef.current, btn = openRef.current, v = videoRef.current; if (!sec || !btn || !v) return; const vr = v.getBoundingClientRect(); // área real del elemento vídeo const sr = sec.getBoundingClientRect(); // para coords relativas a la sección const W = vr.width, H = vr.height; const vw = v.videoWidth || 3840; const vh = v.videoHeight || 2160; const scale = Math.max(W / vw, H / vh); // object-fit: cover const dispW = vw * scale, dispH = vh * scale; const offX = (W - dispW) / 2, offY = (H - dispH) / 2; // Horizontalmente lo dejamos centrado en pantalla (como el indicador SCROLL); // el área es grande y el sello —casi centrado— queda holgadamente dentro. const yInVideo = offY + HERO_SEAL.y * dispH; btn.style.top = (vr.top - sr.top + yInVideo) + "px"; btn.style.setProperty("--seal-r", (HERO_SEAL.r * dispW) + "px"); }; place(); window.addEventListener("resize", place); const v = videoRef.current; if (v) v.addEventListener("loadedmetadata", place); return () => { window.removeEventListener("resize", place); if (v) v.removeEventListener("loadedmetadata", place); }; }, [opened]); // El vídeo arranca pausado en el primer frame: no se reproduce hasta pulsar ABRIR. React.useEffect(() => { const v = videoRef.current; if (!v) return; v.muted = true; const hold = () => { v.pause(); v.currentTime = 0; }; hold(); v.addEventListener("loadeddata", hold, { once: true }); return () => v.removeEventListener("loadeddata", hold); }, []); // Abrir el sobre: reproduce el vídeo de fondo del hero (en color y con sonido). const abrirSobre = () => { const v = videoRef.current; if (v) { v.muted = false; v.currentTime = 0; v.play().catch(() => { v.muted = true; v.play().catch(() => {}); }); } setOpened(true); }; // Al terminar el vídeo: se detiene, vuelve al primer frame y reaparece el botón. const alTerminar = () => { const v = videoRef.current; if (v) { v.pause(); v.currentTime = 0; } setOpened(false); }; return (
); } /* ---------- Bloque 2 — Vídeo & Quote ---------- */ function VideoQuote() { const lang = useLang(); return (

{t("Todo", "It all")}
{t("empieza", "begins")}
{t("con una conversación.", "with a conversation.")}

{t("Y nos encargamos de", "And we make sure")}
{t("que nadie la ignore", "no one ignores it")}

{t("¿HABLAMOS?", "SHALL WE TALK?")}
); } /* ---------- Bloque 3 — Nosotras ---------- */ function Manifesto() { const lang = useLang(); return (
| {t("NOSOTRAS", "ABOUT US")}

{t("Construimos ", "We build ")}{t("narrativas", "narratives")}{t(" para marcas donde el detalle no decora: ", " for brands where detail doesn't decorate: it ")}{t("define", "defines")}.

{t("Somos una agencia de comunicación especializada en hospitality, gastronomía y proyectos con identidad propia. Trabajamos con hoteles, restaurantes y marcas que entienden que hoy la conversación también forma parte de la experiencia.", "We are a communications agency specialized in hospitality, gastronomy and projects with their own identity. We work with hotels, restaurants and brands that understand that today the conversation is also part of the experience.")}

{t("A través de estrategia, prensa, influencia y contenido, convertimos marcas en conversaciones que dejan huella. Porque en hospitality, lo que se dice también construye deseo, posicionamiento y cultura.", "Through strategy, press, influence and content, we turn brands into conversations that leave a mark. Because in hospitality, what gets said also builds desire, positioning and culture.")}

{t("CONÓCENOS", "ABOUT US")}
); } /* ---------- Bloque 4 — Servicios ---------- */ function Services() { const lang = useLang(); const shiftRef = React.useRef(null); // Empujón sutil del scroll: la animación CSS hace el grueso del movimiento; // el scroll solo añade un pequeño desplazamiento extra sobre una capa aparte. React.useEffect(() => { const el = shiftRef.current; if (!el) return; const AMPLITUDE = 200; // px — empujón del scroll algo más marcado let raf = 0; const update = () => { raf = 0; const section = el.closest(".services-v2"); if (!section) return; const rect = section.getBoundingClientRect(); // progreso 0→1 mientras la sección cruza la ventana const raw = (window.innerHeight - rect.top) / (window.innerHeight + rect.height); const progress = Math.max(0, Math.min(1, raw)); el.style.transform = `translateX(-${progress * AMPLITUDE}px)`; }; const onScroll = () => { if (!raf) raf = window.requestAnimationFrame(update); }; window.addEventListener("scroll", onScroll, { passive: true }); update(); return () => { window.removeEventListener("scroll", onScroll); if (raf) window.cancelAnimationFrame(raf); }; }, []); const copy = lang === "en" ? "INFLUENCER MARKETING · DIGITAL STRATEGY · EMAIL MARKETING · ONLINE REPUTATION · VISUAL IDENTITY · " :"INFLUENCER MARKETING  ·  ESTRATEGIA DIGITAL  ·  EMAIL MARKETING  ·  ONLINE REPUTATION  ·  IDENTIDAD VISUAL  ·  "; return (

| {t("SERVICIOS", "SERVICES")}

{t("No trabajamos para generar ruido.", "We don't work to make noise.")}
{t("Trabajamos para generar ", "We work to make ")}{t("impacto", "impact")}.

{t("PRENSA", "PRESS")}

{copy} {copy} {copy}
{t("CONÓCENOS", "ABOUT US")}
); } /* ---------- Bloque 5 — Imagen a sangre ---------- */ function MapaBlock({ src, height }) { return (
); } /* ---------- WorldMap — Our Workspaces ---------- */ const OFFICES = [ { id: "malaga", city: "Málaga", type: "Oficina corporativa", addr: "Hacienda del Mar — Meliá Collection, meeting center", tz: "Europe/Madrid", img: "assets/maps/malaga.png" }, { id: "madrid", city: "Madrid", type: "Oficinas centrales", addr: "C/ de Fernando Santos, Chamberí, 27", tz: "Europe/Madrid", img: "assets/maps/madrid.png" }, { id: "cancun", city: "Cancún", type: "Área Técnica y Comercial", addr: "Carretera Aeropuerto-Bonfil km 11.5", tz: "America/Cancun", img: "assets/maps/cancun.png" }, { id: "cdmx", city: "Ciudad de México", type: "Área Técnica y Comercial", addr: "Campus corporativo de Coyoacán, PB T2", tz: "America/Mexico_City", img: "assets/maps/mexico-city.png" }, { id: "brasil", city: "Brasil", type: "Área Técnica y Comercial", addr: "São Paulo", tz: "America/Sao_Paulo", img: "assets/maps/rio-de-janeiro.png" }, { id: "middle", city: "Middle East", type: "Coming soon", addr: "Dubai / Riyadh", tz: null, img: "assets/maps/riyadh.png" }, { id: "miami", city: "Miami", type: "Coming soon", addr: "Miami, FL", tz: null, img: "assets/maps/miami.png" }, ]; function getStatus(tz) { if (!tz) return "soon"; try { const parts = new Intl.DateTimeFormat("en-US", { timeZone: tz, hour: "numeric", hour12: false, weekday: "short" }).formatToParts(new Date()); const weekday = parts.find(p => p.type === "weekday")?.value; const hour = parseInt(parts.find(p => p.type === "hour")?.value || "0"); return (!["Sat","Sun"].includes(weekday) && hour >= 9 && hour < 18) ? "open" : "closed"; } catch(e) { return "closed"; } } function StatusBadge({ status }) { useLang(); if (status === "soon") return React.createElement("span", { className: "status-badge status-soon" }, t("Próximamente", "Coming soon")); if (status === "open") return React.createElement("span", { className: "status-badge status-open" }, React.createElement("span", { className: "status-dot" }), t("Abierto ahora", "Open now") ); return React.createElement("span", { className: "status-badge status-closed" }, React.createElement("span", { className: "status-dot" }), t("Cerrado", "Closed") ); } const OFFICE_TYPE_EN = { "Oficina corporativa": "Corporate office", "Oficinas centrales": "Headquarters", "Área Técnica y Comercial": "Technical & Commercial Division", "Coming soon": "Coming soon", }; function officeType(type) { return t(type, OFFICE_TYPE_EN[type] || type); } function WorldMap() { const lang = useLang(); const [active, setActive] = React.useState("malaga"); const activeIdx = OFFICES.findIndex(o => o.id === active); const activeOffice = OFFICES[activeIdx] || OFFICES[0]; return (

| OUR WORKSPACES

{t("Distintas ciudades, una forma de entender ", "Different cities, one way of understanding ")}hospitality.

{OFFICES.map((o, i) => { const offset = i - activeIdx; const abs = Math.abs(offset); const pos = abs === 0 ? "center" : abs === 1 ? (offset < 0 ? "left" : "right") : "hidden"; const status = getStatus(o.tz); return (
setActive(o.id)}>
{o.img && {o.city}}
{o.city}
{o.city} {officeType(o.type)} {o.addr}
); })}
{String(activeIdx + 1).padStart(2, "0")} / {String(OFFICES.length).padStart(2, "0")}
{OFFICES.map(o => { const status = getStatus(o.tz); return ( ); })}
); } /* ---------- Bloque 7 — Quote "Y SÍ" ---------- */ function QuoteSection() { const lang = useLang(); return (

{t("Y SÍ,", "AND YES,")}

{t("Nos ", "We ")}{t("encanta", "love")}{t(" hablar de ti", " talking about you")}

{t("SABEMOS QUE ALGUNAS HISTORIAS", "WE KNOW SOME STORIES")}
{t("NACIERON PARA SER CONTADAS", "WERE BORN TO BE TOLD")}

{t("¿Estás listo para contar la tuya?", "Ready to tell yours?")}

); } /* ---------- Archivo / Portfolio ---------- */ const PRESS = [ { meta: "Hospitality · 2025", title: "Relanzamiento de marca para un resort boutique en la Costa del Sol.", titleEn: "Brand relaunch for a boutique resort on the Costa del Sol.", cap: "Hacienda · coastal", cls: "wide", img: "https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?w=900&q=85&fit=crop" }, { meta: "F&B · 2025", title: "Apertura editorial de un restaurante firma en Ciudad de México.", titleEn: "Editorial opening of a signature restaurant in Mexico City.", cap: "Interior · dining", cls: "tall", img: "https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=900&q=85&fit=crop" }, { meta: "Architecture · 2024",title: "Narrativa de producto para un estudio de arquitectura residencial.", titleEn: "Product narrative for a residential architecture studio.", cap: "Atelier · studio", cls: "third", img: "https://images.unsplash.com/photo-1600585154340-be6161a56a0c?w=900&q=85&fit=crop" }, { meta: "Prensa · 2024", title: "Campaña internacional · 28 medios internacionales de referencia.", titleEn: "International campaign · 28 leading international media outlets.", cap: "Press · portfolio", cls: "third", img: "https://images.unsplash.com/photo-1511578314322-379afb476865?w=900&q=85&fit=crop" }, { meta: "Interior · 2024", title: "Guardianship de marca para una colección de mobiliario de autor.", titleEn: "Brand guardianship for a designer furniture collection.", cap: "FF&E · detail", cls: "third", img: "https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=900&q=85&fit=crop" }, ]; function Press() { const lang = useLang(); return (

| {t("ARCHIVO", "ARCHIVE")}

{t("Un portfolio ", "An ")}{t("editado", "edited")}{t(".", " portfolio.")}
{t("Seleccionado, nunca acumulado.", "Curated, never accumulated.")}

{t("Archivo completo", "Full archive")}
{PRESS.map((p, i) => (
{p.img && {p.cap}}
{p.cap}
{p.meta.replace("Prensa", t("Prensa", "Press"))} 0{i + 1} / 0{PRESS.length}

{t(p.title, p.titleEn)}

))}
); } /* ---------- HoldingBrandName ---------- */ function HoldingBrandName({ slug }) { const names = { talentchef: "TalentChef", tailors: "Tailors", guinda: "Guinda", alia: "ALIA", libo: "LIBO", lachismosa: "laChismosa" }; if (!names[slug]) return null; return React.createElement("img", { src: "assets/logos/" + slug + ".svg", alt: names[slug], className: "brand-logo", loading: "lazy" }); } /* ---------- Holding / Sister brands ---------- */ const HOLDING_BRANDS = [ { cat: "FOOD & BEVERAGE", slug: "talentchef", img: "assets/Talent Chef.png", tone: 1 }, { cat: "HOTEL BRANDS", slug: "tailors", img: "assets/tailor.png", tone: 2, imgPos: "center 10%" }, { cat: "INTERIOR DESIGN & FF&E", slug: "guinda", img: "assets/guinda.png", tone: 3 }, { cat: "ARCHITECTURE", slug: "alia", img: "assets/Alia.png", tone: 4 }, { cat: "ASSETS GUARDIANSHIP", slug: "libo", img: "assets/Libro.png", tone: 5 }, { cat: "COMMUNICATION", slug: "lachismosa", img: "assets/La chismosa.png", tone: 6, current: true }, ]; function Holding() { return (
LittleBig Hospitality Group
); } /* ---------- Footer corporativo ---------- */ function Footer() { const lang = useLang(); const offices = [ { city: "MADRID", addr: <>C/ de Fernando Santo,
Chamberí, 27 }, { city: t("MÁLAGA", "MALAGA"), addr: <>Hacienda del Mar Melia
{t("Collection, meeting center", "Collection, meeting center")} }, { city: t("BRASIL", "BRAZIL"), addr: t("Próximamente", "Coming soon") }, { city: t("CANCÚN", "CANCUN"), addr: <>{t("Carretera Aeropuerto-", "Carretera Aeropuerto-")}
Bonfil km 11.5 }, { city: t("CIUDAD DE MÉXICO", "MEXICO CITY"), addr: <>{t("Campus corporativo de", "Corporate campus,")}
{t("Coyoacán, PB T2", "Coyoacán, GF T2")} }, { city: "MIDDLE EAST", addr: t("Próximamente", "Coming soon") }, { city: "MIAMI", addr: t("Próximamente", "Coming soon") }, ]; return ( ); } /* ---------- Badge circular "Let's connect" — sticky compartido ---------- */ /* (sustituye al antiguo botón silencio; nombre conservado para no romper páginas) */ function SilencioSticky() { const [fill, setFill] = React.useState(0); React.useEffect(() => { const onScroll = () => { const max = document.documentElement.scrollHeight - window.innerHeight; setFill(max > 0 ? Math.max(0, Math.min(1, window.scrollY / max)) : 0); }; window.addEventListener("scroll", onScroll, { passive: true }); window.addEventListener("resize", onScroll); onScroll(); return () => { window.removeEventListener("scroll", onScroll); window.removeEventListener("resize", onScroll); }; }, []); return ( laChismosa ); } Object.assign(window, { Intro, Cursor, Nav, Hero, VideoQuote, Manifesto, Services, MapaBlock, WorldMap, QuoteSection, Press, Holding, Footer, Redacted, InkText, HoldingBrandName, SilencioSticky });