(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 */}
{children}
); } // ─── 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 &&
·
}
); } // ─── 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,
cuidada
como 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) =>
{s.v}
{s.l}
)}
); } // ─── 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 ser
carga de trabajo., <>Your second home
shouldn't be
a 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) => )}
{/* Lista */}
{items.map((it, i) => { const isAfter = mode === 'after'; return (
{/* Icon */}
{isAfter ? : }
{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 */} {/* 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) =>
{b}
)}
{/* Mini-stat dorado destacado */}
{stat}
{/* 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 tu
primera reserva., <>4 steps to your
first 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 (
{s.n}
{s.t}
{timeline[i]}
{s.d}
); })}
); } // ─── 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) =>
{p.name}
)} {/* spacer para que la última card pueda alinear al inicio */}
{/* Indicador de páginas (dots) */}
{COPY.properties.map((_, i) =>
); } // ─── 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 */}
AM Tulum
{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 */}
{k.l}
{/* Barras comparativas */}
{/* AM bar */}
AM
{/* Benchmark bar */}
IND
{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) =>
{s.v}
{s.l}
)}
); } // ─── Contact ───────────────────────────────────────────── function Contact() { return (
{T('Habla con un asesor', 'Talk to an advisor')}
{T(<>Cuéntanos de
tu propiedad., <>Tell us about
your 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) => )}
); } // ─── 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 ( ); })}
); } // ─── 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(); } })();