// Sections: Nav, Hero, Manifesto, Services, Method, Audience, Contact, Footer
const Nav = ({ t, lang, setLang, fonts }) => {
const [open, setOpen] = React.useState(false);
const ignoreScroll = React.useRef(false);
// Lock body scroll when menu open
React.useEffect(() => {
const prev = document.body.style.overflow;
document.body.style.overflow = open ? "hidden" : prev || "";
return () => { document.body.style.overflow = prev; };
}, [open]);
// Close on ESC
React.useEffect(() => {
const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
const go = (id) => {
setOpen(false);
const el = document.getElementById(id);
if (el) {
ignoreScroll.current = true;
const navEl = document.querySelector(".fa-nav");
const offset = (navEl ? navEl.getBoundingClientRect().height : 60) + 8;
window.scrollTo({ top: el.offsetTop - offset, behavior: "smooth" });
setTimeout(() => { ignoreScroll.current = false; }, 1200);
}
};
return (
);
};
const HeroVideo = () => {
const ref = React.useRef(null);
const [src, setSrc] = React.useState(null);
React.useEffect(() => {
let url = null;
let cancelled = false;
fetch("assets/hero-loop.mp4")
.then((r) => r.blob())
.then((blob) => {
if (cancelled) return;
url = URL.createObjectURL(blob);
setSrc(url);
})
.catch(() => {});
return () => {
cancelled = true;
if (url) URL.revokeObjectURL(url);
};
}, []);
return (
);
};
const Hero = ({ t, layout = "photo" }) => {
const ref = React.useRef(null);
React.useEffect(() => {
const onScroll = () => {
const el = ref.current;
if (!el) return;
const y = window.scrollY;
const h = window.innerHeight;
// Fade out by 60% scroll of viewport, parallax up
const progress = Math.min(1, y / (h * 0.7));
const opacity = 1 - progress;
const translate = y * 0.3; // parallax: move slower than scroll
el.style.setProperty("--hero-fade", opacity);
el.style.setProperty("--hero-shift", `${translate}px`);
};
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
{layout === "video" ? : null}
{layout === "ink" ? (
) : null}
{t.hero.eyebrow ? (
{t.hero.eyebrow}
) : null}
{t.hero.title[0]}
{t.hero.title[1]}
{t.hero.lede}
);
};
const Manifesto = ({ t }) => (
{t.manifesto.eyebrow}
{t.manifesto.title.split("\n").map((line, i) => (
{line}
))}
{t.manifesto.paragraphs.map((p, i) =>
{p}
)}
{t.manifesto.signature && (
{t.manifesto.signature.name}
{t.manifesto.signature.role}
)}
);
const Service = ({ item, labels, isOpen, onToggle }) => (
{item.title}
{item.subtitle}
{item.summary}
+
{labels.focus}
{item.focus.map((f, i) => - {f}
)}
{labels.format}
{item.format.map((f, i) => - {f}
)}
{labels.result}
{item.result}
);
const ServicesDial = ({ t }) => {
const [active, setActive] = React.useState(0);
const items = t.services.items;
const cur = items[active];
return (
{/* Mobile: horizontal tab strip */}
{items.map((s, i) => (
))}
{/* Desktop: vertical timeline + content */}
{items.map((s, i) => (
))}
{cur.title}
{cur.subtitle}
{cur.summary}
{/* Mobile-only headline (replaces vline content layout) */}
{cur.number}
{cur.title}
{cur.subtitle}
{cur.summary}
);
};
const ServicesHead = ({ t, center = false }) => (
{t.services.eyebrow}
{t.services.title.split("\n").map((line, i) => (
{line}
))}
{t.services.lede && {t.services.lede}
}
);
// ---- 1 · LIST (accordion actual) ----
const ServicesList = ({ t }) => {
const [open, setOpen] = React.useState(-1);
return (
{/* Mobile: cards apiladas (todas visibles) */}
{t.services.items.map((item, i) => (
{item.title}
{item.subtitle}
{item.summary}
{item.format && item.format.length > 0 && (
{item.format.slice(0, 3).map((o, k) => (
- {o}
))}
)}
))}
{t.services.labels.cta}
→
{/* Desktop: accordion list (sin cambios) */}
{t.services.items.map((item, i) => (
setOpen(open === i ? -1 : i)}
/>
))}
);
};
// ---- 2 · PICTOGRAMA XL ----
const ServicesPictogram = ({ t }) => (
);
// ---- 3 · NUMERAL monumental ----
const ServicesNumeral = ({ t }) => (
);
// ---- 4 · TRIADA radial ----
const ServicesTriad = ({ t }) => (
{t.services.eyebrow}
{t.services.items.map((item, i) => (
{item.number}
{item.title}
{item.subtitle}
))}
);
// ---- 5 · CARDS verticales con glyph hero ----
const ServicesVCards = ({ t }) => (
);
// ---- 6 · MINIMAL — glyph + duración ----
const ServicesMinimal = ({ t }) => (
);
// ---- 7 · FLOW horizontal con conexión ----
const ServicesFlow = ({ t }) => (
{t.services.items.map((item, i) => (
{item.number}
{item.title}
{item.subtitle}
))}
);
// ---- 8 · EMBLEM (cartel/postal) ----
const ServicesEmblem = ({ t }) => (
);
// ---------- 9 · EDITORIAL — índice de libro ----------
const ServicesEditorial = ({ t }) => {
const [open, setOpen] = React.useState(-1);
return (
{t.services.items.map((item, i) => {
const isOpen = open === i;
return (
setOpen(isOpen ? -1 : i)}
>
{item.subtitle}
{item.title}
{item.summary}
{isOpen ? "−" : "+"}
{t.services.labels.focus}
{item.focus.map((f, k) => - {f}
)}
{t.services.labels.format}
{item.format.map((f, k) => - {f}
)}
{t.services.labels.result}
{item.result}
{t.services.labels.cta}
→
);
})}
);
};
// ---------- 10 · STICKY — sticky scroll cinematográfico ----------
const ServicesSticky = ({ t }) => {
const [active, setActive] = React.useState(0);
const refs = React.useRef([]);
React.useEffect(() => {
const obs = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
const idx = Number(e.target.getAttribute("data-idx"));
setActive(idx);
}
});
},
{ rootMargin: "-40% 0px -40% 0px", threshold: 0 }
);
refs.current.forEach((r) => r && obs.observe(r));
return () => obs.disconnect();
}, []);
const cur = t.services.items[active] || t.services.items[0];
return (
{t.services.items.map((item, i) => (
(refs.current[i] = el)}
data-idx={i}
className={`fa-svc-st-pane ${active === i ? "is-active" : ""}`}
>
{String(i + 1).padStart(2, "0")}
{item.title}
{t.services.labels.focus}
{item.focus.map((f, k) => - {f}
)}
{t.services.labels.format}
{item.format.map((f, k) => - {f}
)}
{t.services.labels.result}
{item.result}
))}
);
};
// ---------- 11 · AUDIENCE — anclaje en tipología de cliente ----------
const ServicesAudience = ({ t }) => {
const [active, setActive] = React.useState(0);
const items = t.services.items;
const cur = items[active];
return (
{items.map((item, i) => (
))}
);
};
const Services = ({ t, layout = "list" }) => {
const variants = {
list: ServicesList,
pictogram: ServicesPictogram,
numeral: ServicesNumeral,
triad: ServicesTriad,
vcards: ServicesVCards,
minimal: ServicesMinimal,
flow: ServicesFlow,
emblem: ServicesEmblem,
editorial: ServicesEditorial,
sticky: ServicesSticky,
audience: ServicesAudience,
};
const View = variants[layout] || ServicesList;
return (
);
};
// ---------- METHOD LAYOUTS ----------
const MethodSplit = ({ t }) => (
{t.method.eyebrow}
{t.method.title.split("\n").map((line, i) => (
{line}
))}
{t.method.negatives.map((n, i) =>
{n}
)}
{t.method.positives.map((p, i) => (
))}
);
const MethodHeader = ({ t, center = false }) => (
{t.method.eyebrow}
{t.method.title.split("\n").map((line, i) => (
{line}
))}
);
const MethodSteps = ({ t }) => {
const [active, setActive] = React.useState(0);
const items = t.method.positives;
return (
{items.map((p, i) => (
))}
{items.map((p, i) => (
setActive(i)}>
{String(i + 1).padStart(2, "0")} / {String(items.length).padStart(2, "0")}
{p.glyph ? : null}
{p.label}
{p.note}
))}
);
};
const MethodTabs = ({ t }) => {
const [active, setActive] = React.useState(0);
const items = t.method.positives;
return (
{items.map((p, i) => (
))}
{items[active].glyph ? : null}
{items[active].label}
{items[active].note}
);
};
const MethodDiagram = ({ t }) => {
const items = t.method.positives;
return (
{t.method.eyebrow}
{items.map((p, i) => (
))}
);
};
const MethodCards = ({ t }) => (
{t.method.positives.map((p, i) => (
{String(i + 1).padStart(2, "0")}
{p.glyph ? : null}
{p.label}
{p.note}
))}
);
const MethodStack = ({ t }) => (
{t.method.positives.map((p, i) => (
-
{String(i + 1).padStart(2, "0")}
{p.glyph ? : null}
))}
);
const MethodMarquee = ({ t }) => {
const [active, setActive] = React.useState(0);
const items = t.method.positives;
const cur = items[active];
return (
{items.map((p, i) => (
))}
{cur.glyph ? : null}
{cur.label}
{cur.note}
);
};
const MethodTimeline = ({ t }) => (
{t.method.positives.map((p, i) => (
-
{String(i + 1).padStart(2, "0")} · {t.method.eyebrow}
{p.label}
{p.note}
))}
);
// ---------- 9 · SIMPLE — editorial mínima ----------
const MethodSimple = ({ t }) => (
{t.method.eyebrow}
{t.method.title.split("\n").map((line, i) => (
{line}
))}
{t.method.positives.map((p, i) => (
{String(i + 1).padStart(2, "0")}
))}
);
const Method = ({ t, layout = "split" }) => {
const variants = {
split: MethodSplit,
simple: MethodSimple,
steps: MethodSteps,
tabs: MethodTabs,
diagram: MethodDiagram,
cards: MethodCards,
stack: MethodStack,
marquee: MethodMarquee,
timeline: MethodTimeline,
};
const View = variants[layout] || MethodSplit;
return (
);
};
const Clients = ({ t }) => (
{t.clients.eyebrow}
{t.clients.title.split("\n").map((l, i) => (
{l}
))}
);
// Per-logo height tuning: stacked logos can be tall, wordmarks need to be shorter
// so the type doesn't dwarf neighbours. Width caps prevent any single logo from
// dominating its cell. Height in px (cell is ~95px tall).
const BRANDS_LIST = [
// Stacked / iconic logos (mostly square-ish)
{ name: "Solen Group", src: "assets/logo-solen-clean.png?v=3", h: 72, type: "stacked" },
{ name: "Akbal Music", src: "assets/brand-akbalmusic-clean.png?v=3", h: 72, type: "stacked" },
{ name: "Hotelshops", src: "assets/brand-hotelshops-clean.png?v=3", h: 72, type: "stacked" },
// Wordmarks / wide logos
{ name: "American Sign", src: "assets/brand-americansign-clean.png?v=3", h: 28, type: "wordmark" },
{ name: "Akbal Collection", src: "assets/logo-akbal-clean.png?v=3", h: 30, type: "wordmark" },
{ name: "Muralia Decoprint", src: "assets/brand-muralia-clean.png?v=3", h: 38, type: "wordmark" },
];
const BrandImg = ({ brand, scale = 1 }) => (
);
const BrandCell = ({ brand, scale }) => (
);
// === Layout: Marquee horizontal infinito ===
const BrandsMarquee = () => {
// duplicate list so it loops seamlessly
const list = [...BRANDS_LIST, ...BRANDS_LIST];
return (
{list.map((b, i) => (
))}
);
};
// === Layout: Carrusel paginado swipeable ===
const BrandsCarousel = () => {
const [page, setPage] = React.useState(0);
const trackRef = React.useRef(null);
const pageSize = 4; // 2x2 per page
const pages = [];
for (let i = 0; i < BRANDS_LIST.length; i += pageSize) {
pages.push(BRANDS_LIST.slice(i, i + pageSize));
}
const onScroll = () => {
const t = trackRef.current;
if (!t) return;
const p = Math.round(t.scrollLeft / t.clientWidth);
setPage(p);
};
const goto = (i) => {
const t = trackRef.current;
if (!t) return;
t.scrollTo({ left: i * t.clientWidth, behavior: "smooth" });
};
return (
{pages.map((group, i) => (
))}
{pages.map((_, i) => (
);
};
// === Layout: Lista vertical apilada ===
const BrandsStack = () => (
{BRANDS_LIST.map((b, i) => (
{b.name}
))}
);
const Brands = ({ t, layout = "compact" }) => {
let body;
if (layout === "marquee") {
body = ;
} else if (layout === "carousel") {
body = ;
} else if (layout === "stack") {
body = ;
} else {
// compact = 4×2 desktop, 3-col mobile
// big = 2-col mobile, larger cells
const gridClass = layout === "big" ? "fa-brands-grid fa-brands-grid--big" : "fa-brands-grid";
const scale = layout === "big" ? 1.15 : 1;
body = (
{BRANDS_LIST.map((b) => (
))}
);
}
return (
{t.brands.eyebrow}
{t.brands.title.split("\n").map((l, i) => (
{l}
))}
{body}
);
};
// ---------- AUDIENCE LAYOUTS ----------
const AudienceSplit = ({ t }) => (
{t.audience.eyebrow}
{t.audience.title}
{t.audience.list.map((l, i) => - {l}
)}
{t.audience.outcome.title}
{t.audience.outcome.items.map((i, idx) => - {i}
)}
);
const AudienceManifesto = ({ t }) => (
{t.audience.eyebrow}
{t.audience.title}
{t.audience.list.map((l, i) => - {l}
)}
{t.audience.outcome.title}
{t.audience.outcome.items.map((i, idx) => (
{i}
))}
);
const AudienceIndex = ({ t }) => (
{t.audience.eyebrow}
{t.audience.title}
{t.audience.list.map((l, i) => (
{String(i + 1).padStart(2, "0")}
{l}
))}
{t.audience.outcome.title}
{t.audience.outcome.items.map((i, idx) => (
{i}
))}
);
const AudienceMatrix = ({ t }) => (
{t.audience.eyebrow}
{t.audience.title}
{t.audience.eyebrow}
{t.audience.list.map((l, i) => (
-
+
{l}
))}
{t.audience.outcome.title}
{t.audience.outcome.items.map((i, idx) => (
-
→
{i}
))}
);
const AudienceRibbon = ({ t }) => (
{t.audience.eyebrow}
{t.audience.title}
{t.audience.list.map((l, i) => (
{l}
))}
{t.audience.outcome.title}
{t.audience.outcome.items.map((i, idx) => (
{i}
))}
);
const AudienceLedger = ({ t }) => (
{t.audience.eyebrow}
{t.audience.title}
| № |
{t.audience.eyebrow} |
{t.audience.outcome.title} |
{t.audience.list.map((l, i) => {
const out = t.audience.outcome.items[i];
return (
| {String(i + 1).padStart(2, "0")} |
{l} |
{out || ""} |
);
})}
{t.audience.outcome.items.length > t.audience.list.length &&
t.audience.outcome.items.slice(t.audience.list.length).map((o, idx) => (
| — |
|
{o} |
))}
);
const Audience = ({ t, layout = "split" }) => {
const variants = {
split: AudienceSplit,
manifesto: AudienceManifesto,
index: AudienceIndex,
matrix: AudienceMatrix,
ribbon: AudienceRibbon,
ledger: AudienceLedger,
};
const View = variants[layout] || AudienceSplit;
return (
);
};
const Contact = ({ t }) => {
const [sent, setSent] = React.useState(false);
const [form, setForm] = React.useState({ name: "", email: "", company: "", service: "" });
const submit = (e) => {
e.preventDefault();
setSent(true);
setTimeout(() => {
setForm({ name: "", email: "", company: "", service: "" });
setSent(false);
}, 4500);
};
return (
);
};
// ---------- FOOTER LAYOUTS ----------
const FooterDefault = ({ t }) => (
{t.footer.tagline}
{t.footer.rights}
);
// 2 · MONUMENTAL — wordmark a sangre, mínimo info abajo
const FooterMonumental = ({ t }) => (
{t.footer.tagline}
{t.footer.rights}
FUENTE·ALQUIMIA
);
// 3 · CTA — última invitación a conversar
const FooterCta = ({ t }) => (
{t.contact.eyebrow}
{t.contact.title.split("\n").map((line, i) => (
{line}
))}
{t.contact.cta}
→
{t.footer.rights}
);
// 4 · COLOFÓN — sigilo + frase + rights, como libro antiguo
const FooterColophon = ({ t }) => (
{t.footer.tagline}
{t.footer.rights}
);
// 5 · FUNCIONAL — 3 columnas equilibradas con iconos sociales
const FooterFunctional = ({ t }) => {
const navLinks = [
{ id: "services", label: t.nav.services },
{ id: "brands", label: t.nav.work },
{ id: "contact", label: t.nav.contact },
];
return (
{/* Col 1 — marca */}

{t.footer.tagline ?
{t.footer.tagline}
: null}
{/* Col 2 — navegación */}
{t.footer.navLabel || "Navegación"}
{/* Col 3 — contacto + redes */}
{t.footer.contactLabel || "Contacto"}
{t.footer.rights}
);
};
// 6 · GRID3 — 3 columnas: marca + navegación + redes (inspiración tiles)
const FooterGrid3 = ({ t }) => {
const go = (id) => {
const el = document.getElementById(id);
if (el) {
const navH = (document.querySelector(".fa-nav")?.offsetHeight || 80) + 8;
window.scrollTo({ top: el.offsetTop - navH, behavior: "smooth" });
}
};
return (
Claridad para decisiones que pesan. Estrategia, marca e IA aplicada.
{t.footer.rights}
);
};
const Footer = ({ t, layout = "default" }) => {
const variants = {
default: FooterDefault,
monumental: FooterMonumental,
cta: FooterCta,
colophon: FooterColophon,
functional: FooterFunctional,
grid3: FooterGrid3,
};
const View = variants[layout] || FooterDefault;
return (
);
};
Object.assign(window, { Nav, Hero, Manifesto, Services, Method, Clients, Brands, Audience, Contact, Footer });