/* ========== La Chismosa components ========== */
/* ---------- Custom Cursor ---------- */
const CURSOR_LABELS = [
{ sel: ".envelope-wrap, .envelope-cta, .envelope", text: "Abrir" },
{ sel: ".press-card", text: "Leer" },
{ sel: ".carousel-card, .office-item", text: "Ver" },
{ sel: ".contact-cta", text: "Escribir" },
{ sel: ".holding-card", text: "Descubrir" },
{ sel: ".silencio-sticky", text: "Subir" },
];
function Cursor() {
const dot = React.useRef(null);
const ring = React.useRef(null);
const lbl = React.useRef(null);
const pos = React.useRef({ x: -100, y: -100 });
const target = React.useRef({ x: -100, y: -100 });
React.useEffect(() => {
const onMove = (e) => {
target.current = { x: e.clientX, y: e.clientY };
if (dot.current)
dot.current.style.transform = `translate(${e.clientX}px, ${e.clientY}px) translate(-50%, -50%)`;
};
const onOver = (e) => {
if (!ring.current) return;
const el = e.target;
if (el.closest("a, button, [data-hover], .envelope-cta, .office-item, .press-card, .services-foot .item, .map-pin")) {
ring.current.classList.add("hover");
} else {
ring.current.classList.remove("hover");
}
if (lbl.current) {
const match = CURSOR_LABELS.find(({ sel }) => el.closest(sel));
if (match) {
lbl.current.textContent = match.text;
lbl.current.classList.add("visible");
} else {
lbl.current.classList.remove("visible");
}
}
};
let raf;
const loop = () => {
pos.current.x += (target.current.x - pos.current.x) * 0.18;
pos.current.y += (target.current.y - pos.current.y) * 0.18;
if (ring.current)
ring.current.style.transform = `translate(${pos.current.x}px, ${pos.current.y}px) translate(-50%, -50%)`;
if (lbl.current)
lbl.current.style.transform = `translate(${pos.current.x + 26}px, ${pos.current.y - 14}px)`;
raf = requestAnimationFrame(loop);
};
loop();
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseover", onOver);
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("mousemove", onMove);
window.removeEventListener("mouseover", onOver);
};
}, []);
return (
<>
>);
}
/* ---------- Navigation ---------- */
function Nav({ active, onNav }) {
const [scrolled, setScrolled] = React.useState(false);
React.useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 40);
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
const links = [
{ id: "nosotros", label: "Nosotros" },
{ id: "servicios", label: "Servicios" },
{ id: "prensa", label: "Prensa" },
{ id: "proyectos", label: "Proyectos" },
{ id: "contacto", label: "Contacto" }];
return (
);
}
/* ---------- 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) => (
))}
);
}
/* ---------- 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 / Envelope ---------- */
function Hero({ onReveal }) {
const [state, setState] = React.useState("closed");
const heroRef = React.useRef(null);
React.useEffect(() => {
const t = setTimeout(() => {setState("opening");}, 600);
return () => clearTimeout(t);
}, []);
React.useEffect(() => {
if (state === "opening") {
const t = setTimeout(() => {setState("revealed");onReveal?.();}, 1400);
return () => clearTimeout(t);
}
}, [state]);
React.useEffect(() => {
const onMove = (e) => {
if (!heroRef.current) return;
const r = heroRef.current.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5;
const y = (e.clientY - r.top) / r.height - 0.5;
const bg = heroRef.current.querySelector(".video-placeholder");
if (bg) bg.style.transform = `scale(${state === "revealed" ? 1.02 : 1.08}) translate(${x * -20}px, ${y * -20}px)`;
};
window.addEventListener("mousemove", onMove);
return () => window.removeEventListener("mousemove", onMove);
}, [state]);
return (
{state === "revealed" &&
}
state === "closed" && setState("opening")}>
LCH 2026
INVITACIÓN N.º 001
Para quien comunica con intención.
Una carta abierta de La Chismosa — Luxury Communications
{e.stopPropagation();setState("opening");}}>
Abrir carta
L
backed by a global group
Una agencia global de comunicación para marcas que entienden el lujo como lenguaje.
— Who we are —
"Porque el lujo
también se comunica ."
Antonio de Juan
Founder · Managing Director
© MMXXVI
Málaga · Madrid · Brasil · Cancún · Miami · CDMX
);
}
/* ---------- Manifesto ---------- */
function Manifesto() {
return (
— 01 / Nosotros
Construimos narrativas para marcas donde el detalle
no decora:
define .
Somos un estudio de comunicación dedicado exclusivamente al universo del lujo —
hospitality, gastronomía, arquitectura y diseño interior. Un equipo pequeño, respaldado
por un grupo global, que trabaja pieza a pieza, palabra a palabra.
No vendemos ruido. Ofrecemos criterio , discreción y una red de relaciones
cultivada durante más de dos décadas en las cinco ciudades que marcan la conversación.
Sabemos lo que no se publica.
);
}
/* ---------- Services Marquee ---------- */
const SERVICES = [
"Food & Beverage",
"Hotel Brands",
"Interior Design & FF&E",
"Architecture",
"Assets Guardianship",
"Communication"];
function ServicesMarquee() {
const row1 = React.useRef(null);
const row2 = React.useRef(null);
React.useEffect(() => {
let raf,x1 = 0,x2 = 0;
const tick = () => {
x1 -= 0.4;
x2 += 0.4;
if (row1.current) row1.current.style.transform = `translate3d(${x1}px,0,0)`;
if (row2.current) row2.current.style.transform = `translate3d(${x2}px,0,0)`;
if (row1.current) {
const w = row1.current.scrollWidth / 2;
if (Math.abs(x1) >= w) x1 = 0;
}
if (row2.current) {
const w = row2.current.scrollWidth / 2;
if (x2 >= w) x2 = 0;
}
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
const row = (dup = false) =>
<>
{SERVICES.map((s, i) =>
0{i + 1}
{s}
)}
>;
return (
— 02 / Servicios
Seis disciplinas, una misma gramática del lujo.
+ 500 Proyectos
{row()}{row(true)}
{row()}{row(true)}
{SERVICES.map((s, i) =>
0{i + 1} —
{s}
)}
);
}
/* ---------- Presence / Stats ---------- */
function Presence() {
const stats = [
{ n: "+20", em: "países", label: "Experience in over", desc: "Presencia operativa en más de veinte mercados." },
{ n: "+20", em: "países", label: "Trusted by over", desc: "Marcas de lujo que confían su relato a nuestro criterio." },
{ n: "+20", em: "países", label: "Success achieved in", desc: "Proyectos de comunicación, estrategia y custodia de activos." }];
return (
Respaldados por un grupo global , con base estratégica en cinco ciudades clave.
{stats.map((s, i) =>
{s.n} {s.em}
{s.label}
{s.desc}
)}
);
}
/* ---------- Interactive Map ---------- */
const OFFICES = [
{ id: "malaga", city: "Málaga", country: "España", addr: "Hacienda del Mar — Meliá Collection, meeting center", x: 48.8, y: 36.5, status: "open", img: "assets/maps/malaga.png" },
{ id: "madrid", city: "Madrid", country: "España", addr: "C/ de Fernando Santos, Chamberí, 27", x: 47.2, y: 34.0, status: "open", img: "assets/maps/madrid.png" },
{ id: "brasil", city: "Brasil", country: "Sudamérica", addr: "São Paulo", x: 35.2, y: 67.0, status: "open", img: "assets/maps/rio-de-janeiro.png" },
{ id: "cancun", city: "Cancún", country: "México", addr: "Carretera Aeropuerto-Bonfil km 11.5", x: 24.6, y: 49.0, status: "open", img: "assets/maps/cancun.png" },
{ id: "miami", city: "Miami", country: "USA", addr: "Miami, FL", x: 25.8, y: 45.0, status: "open", img: "assets/maps/miami.png" },
{ id: "cdmx", city: "CDMX", country: "México", addr: "Campus corporativo de Coyoacán, PB T2", x: 22.0, y: 52.0, status: "open", img: "assets/maps/mexico-city.png" },
{ id: "middle", city: "Middle East", country: "", addr: "Dubai / Riyadh", x: 60.0, y: 47.0, status: "open", img: "assets/maps/riyadh.png" }];
function WorldMap() {
const [active, setActive] = React.useState("malaga");
const activeOffice = OFFICES.find((o) => o.id === active) || OFFICES[0];
const activeIdx = OFFICES.findIndex((o) => o.id === active);
return (
— 04 / Our workspaces
Conversaciones a escala global , decisiones en persona.
Let's talk · +34 900 155 155
{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";
return (
setActive(o.id)}>
{o.img &&
}
{o.city} · {o.status === "open" ? "open" : "coming soon"}
{o.city}
{o.addr}
);
})}
{String(activeIdx + 1).padStart(2, '0')} / {String(OFFICES.length).padStart(2, '0')}
{activeOffice.status === "open" ? "Open now" : "Coming soon"}
{OFFICES.map((o) =>
setActive(o.id)}
onMouseEnter={() => setActive(o.id)}>
{o.city}
{o.status === "open" ? "Open" : "Coming soon"}
{o.addr}
)}
);
}
/* ---------- Press / Projects ---------- */
const PRESS = [
{ meta: "Hospitality · 2025", title: "Relanzamiento de marca para un resort boutique en la 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.", 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.", 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.", 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.", cap: "FF&E · detail", cls: "third",
img: "https://images.unsplash.com/photo-1555041469-a586c61ea9bc?w=900&q=85&fit=crop" }];
/* ---------- Billboard ---------- */
function Billboard() {
const ref = React.useRef(null);
const [progress, setProgress] = React.useState(0);
React.useEffect(() => {
const onScroll = () => {
const el = ref.current;
if (!el) return;
const r = el.getBoundingClientRect();
const vh = window.innerHeight;
// 0 when section enters viewport from below, 1 when it leaves at top
const p = 1 - (r.top + r.height / 2) / (vh + r.height / 2);
setProgress(Math.max(0, Math.min(1, p)));
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const shift = (progress - 0.5) * 60; // parallax for the photo
// Reveal animation choreography (driven by scroll progress 0 → 1)
// 0.20 → 0.45: pink panel sweeps in (clip-path)
// 0.42 → 0.58: line 1 ("It's time to")
// 0.50 → 0.66: line 2 ("Tell YOUR Story")
// 0.66 → 0.80: wordmark
const stage = (a, b) => Math.max(0, Math.min(1, (progress - a) / (b - a)));
const panelP = stage(0.18, 0.46);
const line1P = stage(0.40, 0.58);
const line2P = stage(0.50, 0.68);
const markP = stage(0.66, 0.82);
// panel drops from top like a canvas falling with gravity (ease-in²)
const easedP = panelP * panelP;
const panelClip = `inset(0% 0% ${(1 - easedP) * 100}% 0%)`;
return (
{/* Animated mupi overlay — covers and replays the pink billboard */}
It's time to
Tell YOUR Story
la
CHISMOSA
{/* (mupi moved inside billboard-media so it tracks the photo crop) */}
— Manifesto
La Chismosa
·
Out-of-home
·
Madrid / 2025
);
}
function Press() {
return (
— 05 / Prensa & Proyectos
Un portfolio editado . Seleccionado, nunca acumulado.
Archivo completo →
{PRESS.map((p, i) =>
{p.img &&
}
{p.cap}
{p.meta}
0{i + 1} / 0{PRESS.length}
{p.title}
)}
);
}
/* ---------- Holding / Sister brands ---------- */
const HOLDING_BRANDS = [
{ cat: "Food & Beverage", name: "TalentChef", italic: false, tone: 1 },
{ cat: "Hotel Brands", name: "Tailors", italic: true, tone: 2 },
{ cat: "Interior Design & FF&E", name: "Guinda", italic: true, tone: 3 },
{ cat: "Architecture", name: "Alia", italic: true, tone: 4 },
{ cat: "Assets Guardianship", name: "Libo", italic: true, tone: 5 },
{ cat: "Communication", name: "la CHISMOSA", italic: true, tone: 6, current: true }];
function Holding() {
return (
— 06 / Backed by
LITTLE BIG
Hospitality Group
Una familia de marcas especialistas. Cada disciplina, una firma propia. Una sola conversación.
);
}
/* ---------- Contact / Footer ---------- */
function Contact() {
const megaRef = React.useRef(null);
React.useEffect(() => {
const el = megaRef.current;
if (!el) return;
const letters = el.querySelectorAll(".footer-mega-letter");
const io = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
io.disconnect();
letters.forEach((l, i) => l.style.setProperty("--ld", `${i * 0.048}s`));
requestAnimationFrame(() => letters.forEach(l => l.classList.add("in")));
}, { threshold: 0.25 });
io.observe(el);
return () => io.disconnect();
}, []);
return (
);
}
Object.assign(window, {
Intro, Cursor, Nav, Hero, Manifesto, ServicesMarquee, Presence, WorldMap, Press, Contact, Redacted, InkText
});