(function(){
// v2-app.jsx — Mobile v2: Bottom Sheet + Bottom Nav + Sticky CTA
// Hero cinematográfico con sheet parcialmente abierto al cargar.
// Bottom nav ancla a secciones (Inicio · Servicios · Propiedades · Contacto).
// Sticky CTA flotante de WhatsApp arriba de la nav.
// i18n helper — pick ES/EN based on global window.__amLang.
const T = (es, en) => typeof window !== 'undefined' && window.__amLang === 'en' ? en : es;
const { useState, useRef, useEffect } = React;
// ─── Phone wrapper centered on a moody page background ───────
function PhoneStage({ children }) {
// In production (window.__AM_RESPONSIVE__), render full-viewport without phone frame.
if (typeof window !== 'undefined' && (window.__AM_RESPONSIVE__ || window.__AM_MOBILE_PROD__)) {
return (
{children}
);
}
return (
{/* Side label */}
AM TULUM · MOBILE v2 · D+A HÍBRIDO
390 × 844 · iPhone
{/* iPhone bezel */}
);
}
// ─── App ────────────────────────────────────────────────
function V2App() {
const [lang, setLang] = typeof useLang === 'function' ? useLang() : React.useState('es');
// Swap global COPY based on lang so sub-components reading COPY.* pick up the change.
if (typeof window !== 'undefined') {
window.__amLang = lang;
window.COPY = lang === 'en' ? window.COPY_EN || window.COPY_ES || window.COPY : window.COPY_ES || window.COPY;
}
const scrollRef = useRef(null);
const [activeTab, setActiveTab] = useState('home');
const [scrollY, setScrollY] = useState(0);
const [menuOpen, setMenuOpen] = useState(false);
// Sections refs for scroll-anchor
const sectionRefs = {
home: useRef(null),
services: useRef(null),
properties: useRef(null),
contact: useRef(null)
};
const scrollTo = (id) => {
const el = sectionRefs[id]?.current;
const cont = scrollRef.current;
if (!el || !cont) return;
// Compensate for floating TopChrome height (~64px) so the section title isn't clipped.
const offset = id === 'home' ? 0 : 72;
cont.scrollTo({ top: el.offsetTop - offset, behavior: 'smooth' });
setActiveTab(id);
};
// Scroll listener — for hero parallax + active tab tracking
useEffect(() => {
const cont = scrollRef.current;
if (!cont) return;
const onScroll = () => {
setScrollY(cont.scrollTop);
// Active tab detection
const scrollPos = cont.scrollTop + 100;
let current = 'home';
for (const [id, ref] of Object.entries(sectionRefs)) {
if (ref.current && ref.current.offsetTop <= scrollPos) {
current = id;
}
}
setActiveTab(current);
};
cont.addEventListener('scroll', onScroll);
return () => cont.removeEventListener('scroll', onScroll);
}, []);
return (
{!(typeof window !== 'undefined' && (window.__AM_RESPONSIVE__ || window.__AM_MOBILE_PROD__)) && }
{/* Top floating chrome — appears over hero, transitions on scroll */}
setMenuOpen(true)} onLogoClick={() => scrollTo('home')} lang={lang} setLang={setLang} />
setMenuOpen(false)} onNav={(id) => {setMenuOpen(false);scrollTo(id);}} />
{/* Scrollable content */}
{/* Dark spacer behind the translucent bottom nav — keeps it visually attached to the footer */}
{/* Bottom nav */}
);
}
// ─── Top chrome (logo + menu, glass on hero, solid after) ─────
function TopChrome({ scrollY, onMenuClick, onLogoClick, lang, setLang }) {
const onHero = scrollY < 200;
const t = Math.min(1, Math.max(0, (scrollY - 100) / 100));
const inProd = typeof window !== 'undefined' && (window.__AM_RESPONSIVE__ || window.__AM_MOBILE_PROD__);
const topPad = inProd ? 'max(env(safe-area-inset-top, 0px), 12px)' : 54;
const langBtnColor = onHero ? 'rgba(255,255,255,0.85)' : 'rgba(10,10,10,0.55)';
const langBtnActive = onHero ? '#fff' : AM.ink;
return (
0.3 ? 'blur(20px) saturate(180%)' : 'none',
WebkitBackdropFilter: t > 0.3 ? 'blur(20px) saturate(180%)' : 'none',
borderBottom: t > 0.5 ? '0.5px solid rgba(10,10,10,0.06)' : 'none',
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
zIndex: 50,
transition: 'background 0.2s, border-color 0.2s'
}}>
{/* ES / EN toggle */}
{setLang &&
setLang('es')} aria-label="Español" style={{
background: 'none', border: 'none', cursor: 'pointer',
padding: '4px 6px',
color: lang === 'es' ? AM.gold : langBtnColor,
fontWeight: lang === 'es' ? 600 : 400,
fontFamily: 'inherit', fontSize: 'inherit', letterSpacing: 'inherit',
textShadow: onHero ? '0 1px 4px rgba(0,0,0,0.4)' : 'none'
}}>ES
·
setLang('en')} aria-label="English" style={{
background: 'none', border: 'none', cursor: 'pointer',
padding: '4px 6px',
color: lang === 'en' ? AM.gold : langBtnColor,
fontWeight: lang === 'en' ? 600 : 400,
fontFamily: 'inherit', fontSize: 'inherit', letterSpacing: 'inherit',
textShadow: onHero ? '0 1px 4px rgba(0,0,0,0.4)' : 'none'
}}>EN
}
{Ico.menu(onHero ? '#fff' : AM.ink, 18)}
);
}
// ─── Hero — full-bleed photo with sheet partially open ────
function Hero({ scrollY }) {
return (
{/* Hero copy — eyebrow arriba, título central grande, credenciales abajo */}
{/* Eyebrow — arriba */}
{T('PROPERTY MANAGEMENT · DESDE 2015', 'PROPERTY MANAGEMENT · SINCE 2015')}
{/* Bloque central — título + subtítulo (protagonista) */}
{T(<>Tu propiedad en Tulum, cuidadacomo nuestra .>, <>Your Tulum home, cared for like our own .>)}
{T('Operación integral con estándares Superhost.', 'End-to-end operation, Superhost standard.')}
{/* Credenciales — abajo */}
{/* Chip Superhost — certificación */}
Superhost {T('· desde 2019', '· since 2019')}
{/* Stats card — partially over hero */}
{COPY.stats.map((s, i) =>
)}
);
}
// ─── Value props — Antes/Después interactivo ─────────
function ValueProps() {
const [mode, setMode] = React.useState('after'); // 'before' | 'after'
const ref = React.useRef(null);
const [visible, setVisible] = React.useState(false);
React.useEffect(() => {
if (!ref.current) return;
const obs = new IntersectionObserver(
([e]) => e.isIntersecting && setVisible(true),
{ threshold: 0.2 }
);
obs.observe(ref.current);
return () => obs.disconnect();
}, []);
const items = window.__amLang === 'en' ? [
{ before: 'Guests waking you at 3am', after: '24/7 attention · without interrupting you' },
{ before: 'Inconsistent cleaning', after: 'In-house staff · Superhost standard' },
{ before: '30-45% occupancy', after: '70-85% average occupancy' },
{ before: 'Reviews 4.3★', after: 'Reviews 4.9★ · Highly Satisfied' },
{ before: 'Fixed pricing · lose peak', after: 'Daily dynamic pricing' }] :
[
{ before: 'Te despierta el huésped a las 3am', after: 'Atención 24/7 sin interrumpirte' },
{ before: 'Limpiezas inconsistentes', after: 'Staff propio · estándar Superhost' },
{ before: 'Ocupación 30-45%', after: 'Ocupación 70-85% promedio' },
{ before: 'Reviews 4.3★', after: 'Reviews 4.9★ · Highly Satisfied' },
{ before: 'Pricing fijo · pierdes temporada', after: 'Pricing dinámico diario' }];
return (
{T('▲ Para propietarios', '▲ For owners')}
{T(<>Tu segunda casa no debe sercarga de trabajo .>, <>Your second home shouldn't bea job .>)}
{T('Esto cambia con AM Tulum:', 'This is what changes with AM Tulum:')}
{/* Toggle Antes / Después */}
{[
{ id: 'before', l: T('Sin AM', 'Without AM') },
{ id: 'after', l: T('Con AM Tulum', 'With AM Tulum') }].
map((t) =>
setMode(t.id)} style={{
padding: '8px 16px', borderRadius: 999, border: 0,
background: mode === t.id ? AM.ink : 'transparent',
color: mode === t.id ? AM.cream : 'rgba(10,10,10,0.55)',
fontFamily: 'Inter, sans-serif',
fontSize: 11.5, fontWeight: 600, letterSpacing: 0.5,
cursor: 'pointer', transition: 'all 0.25s'
}}>{t.l}
)}
{/* Lista */}
{items.map((it, i) => {
const isAfter = mode === 'after';
return (
{/* Icon */}
{isAfter ? it.after : it.before}
);
})}
);
}
// ─── Services ────────────────────────────────────────────
// 6 iconos minimal (1px stroke), index alineado con COPY.services
const SVC_ICONS_M = [
(c) => ,
(c) => ,
(c) => ,
(c) => ,
(c) => ,
(c) => ];
function Services() {
const [openIdx, setOpenIdx] = React.useState(null);
const isEs = (window.__amLang || 'es') !== 'en';
// Read services from the active language bag (falls back to ES const).
const svcList = (isEs ? window.COPY_ES || window.COPY : window.COPY_EN || window.COPY).services || COPY.services;
// Mini-stats por servicio
const SVC_STAT = !isEs ? [
'+30% bookings vs listing alone',
'+20% RevPAR vs fixed pricing',
'< 5 min response time',
'100% auditable checklist',
'Full monthly traceability',
'PDF report every 30 days'] :
[
'+30% reservas vs listing solo',
'+20% RevPAR vs precio fijo',
'< 5 min tiempo de respuesta',
'100% checklist auditable',
'Trazabilidad mensual completa',
'Reporte PDF cada 30 días'];
return (
{T('Operación integral', 'Integrated operation')}
{isEs ?
<>Lo que hacemos por ti,mientras tú no estás .> :
<>What we do for you,while you're away .>
}
{T('Seis áreas que cubrimos, sin que tú toques un teléfono. Toca cualquiera para ver el detalle.', 'Six areas we cover — without you touching a phone. Tap any to see the detail.')}
{/* Lista vertical de 6 áreas — full-width rows */}
{svcList.slice(0, 6).map((s, i) => {
const Icon = SVC_ICONS_M[i];
const isOpen = openIdx === i;
return (
{/* Row clickable */}
setOpenIdx(isOpen ? null : i)}
style={{
all: 'unset', cursor: 'pointer',
width: '100%', boxSizing: 'border-box',
padding: '20px 24px',
display: 'flex', alignItems: 'center', gap: 18,
position: 'relative'
}}>
{/* Icon */}
{Icon(isOpen ? AM.gold : 'rgba(245,241,232,0.85)')}
{/* Title + 1-line teaser */}
{/* Chevron */}
{/* Expanded panel */}
{isOpen &&
}
);
})}
{T('· Todo bajo un solo equipo ·', '· All under one team ·')}
);
}
// Panel expandido para Layout B — bullets + mini-stat dorado
function ServiceDetailRow({ item, stat }) {
return (
{/* Bullets */}
{item.bullets && item.bullets.map((b, i) =>
)}
{/* Mini-stat dorado destacado */}
{/* Proof line */}
{item.proof ?
· {item.proof} ·
:
null}
);
}
// ─── Benefits ────────────────────────────────────────────
// 3 iconos minimal para benefits
const BNF_ICONS_M = [
// 01 Asset value — diamante / casa con escudo
(c) => ,
// 02 Income — gráfica ascendente con línea base
(c) => ,
// 03 Free time — reloj con sol/hojas
(c) => ];
function Benefits() {
return (
{T('Por qué AM Tulum', 'Why AM Tulum')}
{T(<>Un modelo que trabaja para ti .>, <>A model that works for you .>)}
{/* Grid 3 — icono + título corto */}
{(window.__amLang === 'en' ? [
{ n: '01', t: 'Asset value' },
{ n: '02', t: 'Income' },
{ n: '03', t: 'Free time' }] :
[
{ n: '01', t: 'Plusvalía' },
{ n: '02', t: 'Ingresos' },
{ n: '03', t: 'Tiempo libre' }]).
map((b, i) => {
const Icon = BNF_ICONS_M[i];
return (
{b.n}
{Icon('rgba(245,241,232,0.9)')}
{b.t}
);
})}
{T('· Tú disfrutas. Nosotros operamos. ·', '· You enjoy. We operate. ·')}
);
}
// ─── Process — timeline animada con scroll progressive reveal ────
function Process() {
const ref = React.useRef(null);
const [progress, setProgress] = React.useState(0);
// Etiquetas de tiempo para dar sentido de progresión
const timeline = window.__amLang === 'en' ? ['Day 1', 'Week 1', 'Week 2', 'Month 1+'] : ['Día 1', 'Semana 1', 'Semana 2', 'Mes 1+'];
const timelineLabels = timeline;
React.useEffect(() => {
if (!ref.current) return;
const onScroll = () => {
const rect = ref.current.getBoundingClientRect();
const vh = window.innerHeight || 800;
// Progress: 0 cuando la sección entra desde abajo, 1 cuando termina de cruzar
const start = vh * 0.85;
const end = vh * 0.2;
const total = rect.height + (start - end);
const passed = start - rect.top;
const p = Math.max(0, Math.min(1, passed / total));
setProgress(p);
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
return () => window.removeEventListener('scroll', onScroll);
}, []);
const N = COPY.steps.length;
// Línea avanza ligeramente antes que los dots para sentir flow
const lineProgress = Math.min(1, progress * 1.05);
return (
{T('El proceso', 'The process')}
{T(<>4 pasos hasta tuprimera reserva .>, <>4 steps to yourfirst booking .>)}
{/* Track de fondo (apagado) */}
{/* Track activo (dorado, crece con scroll) */}
{COPY.steps.map((s, i) => {
// Cada step se "enciende" (fill dorado) cuando progress pasa su threshold.
// Pero el contenido siempre está visible y leíble.
const stepThreshold = (i + 0.5) / N;
const fullyActive = progress >= stepThreshold;
return (
);
})}
);
}
// ─── Properties — carrusel horizontal con snap ──────────
function Properties() {
const scrollRef = React.useRef(null);
const [activeIdx, setActiveIdx] = React.useState(0);
const total = COPY.properties.length;
React.useEffect(() => {
const el = scrollRef.current;
if (!el) return;
const onScroll = () => {
const cardWidth = el.offsetWidth * 0.82 + 12; // ancho card + gap
const idx = Math.round(el.scrollLeft / cardWidth);
setActiveIdx(Math.max(0, Math.min(total - 1, idx)));
};
el.addEventListener('scroll', onScroll, { passive: true });
return () => el.removeEventListener('scroll', onScroll);
}, [total]);
const scrollToIdx = (i) => {
const el = scrollRef.current;
if (!el) return;
const cardWidth = el.offsetWidth * 0.82 + 12;
el.scrollTo({ left: i * cardWidth, behavior: 'smooth' });
};
return (
{T('Portafolio', 'Portfolio')}
{T(<>Hogares que cuidamos .>, <>Homes we care for .>)}
{String(activeIdx + 1).padStart(2, '0')}
/ {String(total).padStart(2, '0')}
{T('Desliza para explorar las propiedades activas.', 'Swipe to explore active properties.')}
{/* Carrusel horizontal con snap */}
{COPY.properties.map((p, i) =>
)}
{/* spacer para que la última card pueda alinear al inicio */}
{/* Indicador de páginas (dots) */}
{COPY.properties.map((_, i) =>
scrollToIdx(i)}
aria-label={T(`Ir a propiedad ${i + 1}`, `Go to property ${i + 1}`)}
style={{
all: 'unset', cursor: 'pointer',
width: i === activeIdx ? 22 : 6, height: 6,
borderRadius: 999,
background: i === activeIdx ? AM.ink : 'rgba(10,10,10,0.2)',
transition: 'width 320ms cubic-bezier(.2,.7,.2,1), background 220ms ease'
}} />
)}
);
}
// ─── KPIs — count-up + barras comparativas ───────────────
// Datos numéricos: am = nuestro valor, bm = benchmark industria, max = escala
const KPI_DATA = [
{ am: 4.8, bm: 4.5, max: 5.0, fmt: (v) => v.toFixed(1), suffix: '', dir: 'higher' },
{ am: 100, bm: 90, max: 100, fmt: (v) => Math.round(v).toString(), suffix: '%', dir: 'higher' },
{ am: 0, bm: 1, max: 5, fmt: (v) => Math.round(v).toString(), suffix: '%', dir: 'lower' },
{ am: 27.7, bm: 0, max: 30, fmt: (v) => v.toFixed(1), suffix: '%', dir: 'higher', prefix: '+' }];
function CountUp({ to, fmt, suffix = '', prefix = '', active, duration = 1200 }) {
const [v, setV] = React.useState(0);
const startedRef = React.useRef(false);
React.useEffect(() => {
if (!active || startedRef.current) return;
startedRef.current = true;
const start = performance.now();
let raf;
const tick = (now) => {
const t = Math.min(1, (now - start) / duration);
// easeOutCubic
const eased = 1 - Math.pow(1 - t, 3);
setV(to * eased);
if (t < 1) raf = requestAnimationFrame(tick);else
setV(to);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [active, to, duration]);
return {prefix}{fmt(v)}{suffix} ;
}
function KPIs() {
const ref = React.useRef(null);
const [active, setActive] = React.useState(false);
React.useEffect(() => {
if (!ref.current) return;
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
setActive(true);
io.disconnect();
}
});
}, { threshold: 0.25 });
io.observe(ref.current);
return () => io.disconnect();
}, []);
return (
{T('Resultados vs Industria', 'Results vs Industry')}
{T(<>Datos verificables.No promesas .>, <>Verifiable data.No promises .>)}
{/* Leyenda */}
{T('Industria', 'Industry')}
{COPY.kpis.map((k, i) => {
const d = KPI_DATA[i];
const amPct = d.am / d.max * 100;
const bmPct = d.bm / d.max * 100;
// Para "lower is better" (cancelaciones), AM se ve verde aún siendo barra corta
return (
{/* Label + valor grande */}
{/* Barras comparativas */}
{/* AM bar */}
{/* Benchmark bar */}
{k.b}
);
})}
{T(<>
93% de huéspedes nos califican
«Altamente Satisfechos» .>, <>
93% of guests rate us
«Highly Satisfied» .>)}
{T('FUENTE: AIRBNB · JUL 2025', 'SOURCE: AIRBNB · JUL 2025')}
);
}
// ─── Decade strip ───────────────────────────────────────
function DecadeStrip() {
return (
{T('Una década en números', 'A decade in numbers')}
{COPY.decade.map((s, i) =>
)}
);
}
// ─── Contact ─────────────────────────────────────────────
function Contact() {
return (
{T('Habla con un asesor', 'Talk to an advisor')}
{T(<>Cuéntanos detu propiedad .>, <>Tell us aboutyour property .>)}
{T(<>Respuesta en menos de 30 minutos. Sin compromiso, auditoría gratuita.>, <>Response in under 30 minutes. No commitment, free audit.>)}
{/* Audit checklist */}
{T('Qué incluye la auditoría', "What's included in the audit")}
{(window.__amLang === 'en' ? [
'Visit and improvement proposal',
'Inventory and furnishing',
'Professional photography',
'30-day launch plan'] :
[
'Visita y propuesta de mejoras',
'Inventario y equipamiento',
'Fotografías profesionales',
'Plan de lanzamiento en 30 días']).
map((it, i) =>
·
{it}
)}
);
}
// ─── Footer ──────────────────────────────────────────────
function Footer() {
return (
{/* Airbnb CTA — botón largo cream arriba de los íconos sociales */}
{T('Reservar en Airbnb', 'Book on Airbnb')}
{/* Social icons — Opción C: cream sutil + WA verde destacado */}
{[
{
href: COPY.contact.instagramHref, label: 'Instagram', external: true,
icon:
,
highlight: false
},
{
href: COPY.contact.facebookHref, label: 'Facebook', external: true,
icon:
,
highlight: false
},
{
href: `mailto:${COPY.contact.email}`, label: 'Email', external: false,
icon:
,
highlight: false
},
{
href: COPY.contact.waHref, label: 'WhatsApp', external: true,
icon:
,
highlight: true
}
].map(b =>
{b.icon}
{b.label}
)}
);
}
// ─── Nav Drawer (hamburger menu) ────────────────────────
function NavDrawer({ open, onClose, onNav }) {
const items = [
{ id: 'home', label: T('Inicio', 'Home') },
{ id: 'services', label: T('Servicios', 'Services') },
{ id: 'properties', label: T('Propiedades', 'Properties') },
{ id: 'contact', label: T('Contacto', 'Contact') }];
return (
<>
{items.map((it, i) =>
onNav(it.id)} style={{
textAlign: 'left', padding: '14px 0',
background: 'none', border: 'none',
borderBottom: i === items.length - 1 ? 'none' : '0.5px solid rgba(245,241,232,0.1)',
color: AM.cream, cursor: 'pointer',
fontFamily: 'Inter, sans-serif',
fontSize: 11.5, fontWeight: 600,
letterSpacing: 1, textTransform: 'uppercase'
}}>
{it.label}
)}
{T('Reservar en Airbnb', 'Book on Airbnb')}
{T('Hablar por WhatsApp', 'Chat on WhatsApp')}
>);
}
// ─── Sticky WhatsApp CTA — floats above bottom nav ────────
function StickyCTA() {
return (
);
}
// ─── Bottom Nav ──────────────────────────────────────────
function BottomNav({ active, onChange }) {
const items = [
{ id: 'home', icon: Ico.home, label: T('Inicio', 'Home') },
{ id: 'services', icon: Ico.compass, label: T('Servicios', 'Services') },
{ id: 'properties', icon: Ico.star, label: T('Propiedades', 'Properties') },
{ id: 'contact', icon: Ico.user, label: T('Contacto', 'Contact') }];
return (
{items.map((it) => {
const isActive = active === it.id;
const c = isActive ? AM.gold : 'rgba(245,241,232,0.5)';
return (
onChange(it.id)}
style={{
background: 'none', border: 'none',
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
cursor: 'pointer', padding: '6px 8px',
flex: 1
}}>
{it.icon(c, 21)}
{it.label}
);
})}
);
}
// ─── Component (mounted by responsive-app or by Mobile v2.html standalone) ────
window.MobileApp = V2App;
window.V2App = V2App;
if (!window.__AM_RESPONSIVE__) {
ReactDOM.createRoot(document.getElementById('root')).render( );
}
})();