// ride-experience.jsx — Dynamic ride view: map + contextual cards
// Cards appear on a simulated timeline (demo) → in production driven by GPS
// Three card types: curiosity (POI), promo (QR), and seatbelt (start of ride)

const { useState, useEffect, useRef, useCallback } = React;

// ── GPS coordinates for Málaga POIs & promos ─────────────────────────────────
// Trigger radius: 300m for curiosities, 400m for promos
const POI_COORDS = {
  larios:      { lat: 36.7213, lon: -4.4196, type: 'curiosity', radius: 300 },
  catedral:    { lat: 36.7211, lon: -4.4198, type: 'curiosity', radius: 300 },
  atarazanas:  { lat: 36.7218, lon: -4.4244, type: 'curiosity', radius: 300 },
  malagueta:   { lat: 36.7169, lon: -4.4061, type: 'curiosity', radius: 400 },
  pimpi:       { lat: 36.7214, lon: -4.4183, type: 'promo',     radius: 400 },
  hotel_larios:{ lat: 36.7215, lon: -4.4200, type: 'promo',     radius: 400 },
  teatro:      { lat: 36.7218, lon: -4.4238, type: 'promo',     radius: 400 },
};

function haversineMeters(lat1, lon1, lat2, lon2) {
  const R = 6371000;
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLon = (lon2 - lon1) * Math.PI / 180;
  const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLon/2)**2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}

// ── Simulated timeline — used as fallback when GPS not available ──────────────
const RIDE_TIMELINE = [
  // seatbelt removed — handled by SeatbeltScene before the ride starts
  { at:  10, type: 'curiosity', id: 'larios'      },
  { at:  25, type: 'promo',     id: 'pimpi'       },
  { at:  40, type: 'curiosity', id: 'catedral'    },
  { at:  55, type: 'promo',     id: 'hotel_larios'},
  { at:  70, type: 'curiosity', id: 'atarazanas'  },
  { at:  85, type: 'promo',     id: 'teatro'      },
  { at: 100, type: 'curiosity', id: 'malagueta'   },
];

// ── Promo data ────────────────────────────────────────────────────────────────
const PROMOS = {
  pimpi: {
    merchant: 'Bodega El Pimpi',
    photo: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=900&q=80&auto=format&fit=crop',
    zone: 'Centro histórico',
    offer: { es: '−20% en tapas y vino', en: '−20% on tapas & wine', fr: '−20% tapas et vin', de: '−20% Tapas & Wein', it: '−20% tapas e vino', pt: '−20% tapas e vinho', zh: '8折优惠', ru: '−20% тапас и вино' },
    blurb: { es: 'La bodega más famosa de Málaga, a 4 min de tu hotel. Escanea y te guardo mesa.', en: 'The most famous bodega in Málaga, 4 min from your hotel. Scan and I\'ll hold your table.', fr: 'La bodega la plus célèbre de Málaga, à 4 min. Scannez, je garde la table.', de: 'Málagas berühmteste Bodega, 4 Min vom Hotel. Scanne — ich halte den Tisch.', it: 'La bodega più famosa di Málaga, 4 min dall\'hotel. Scansiona e tengo il tavolo.', pt: 'A bodega mais famosa de Málaga, 4 min do hotel. Escaneia e guardo a mesa.', zh: '马拉加最著名的酒吧，距酒店4分钟。扫码我帮您留座位。', ru: 'Самая известная бодега Малаги, 4 мин от отеля. Сканируй — придержу столик.' },
    color: '#8B1A2A',
    emoji: '🍷',
  },
  hotel_larios: {
    merchant: 'Hotel Larios',
    photo: 'https://images.unsplash.com/photo-1542314831-068cd1dbfeeb?w=900&q=80&auto=format&fit=crop',
    zone: 'Calle Larios',
    offer: { es: 'Upgrade gratis si reservas hoy', en: 'Free upgrade if you book today', fr: 'Surclassement gratuit si vous réservez aujourd\'hui', de: 'Kostenloses Upgrade bei Buchung heute', it: 'Upgrade gratuito se prenoti oggi', pt: 'Upgrade gratuito se reservares hoje', zh: '今天预订免费升级', ru: 'Бесплатный апгрейд при бронировании сегодня' },
    blurb: { es: 'En el corazón de Málaga, frente a la calle más elegante. Escanea para reservar con upgrade.', en: 'In the heart of Málaga, facing the most elegant street. Scan to book with free upgrade.', fr: 'Au cœur de Málaga, face à la rue la plus élégante. Scannez pour réserver.', de: 'Im Herzen Málagas. Scanne für eine Buchung mit kostenlosem Upgrade.', it: 'Nel cuore di Málaga. Scansiona per prenotare con upgrade gratuito.', pt: 'No coração de Málaga. Escaneia para reservar com upgrade gratuito.', zh: '马拉加市中心。扫码预订并享受免费升级。', ru: 'В центре Малаги. Сканируй для бронирования с апгрейдом.' },
    color: '#1a3a5c',
    emoji: '🏨',
  },
  teatro: {
    merchant: 'Teatro Cervantes',
    photo: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/a1/Teatro_Cervantes_Malaga.JPG/800px-Teatro_Cervantes_Malaga.JPG',
    zone: 'Centro · Teatro',
    offer: { es: '2x1 en espectáculos este fin de semana', en: '2-for-1 shows this weekend', fr: '2 pour 1 ce week-end', de: '2-für-1 dieses Wochenende', it: '2×1 questo weekend', pt: '2×1 este fim de semana', zh: '本周末买一送一', ru: '2 за 1 в эти выходные' },
    blurb: { es: 'El teatro más emblemático de Málaga. Escanea para 2 entradas al precio de 1.', en: 'Málaga\'s most iconic theatre. Scan for 2 tickets at the price of 1.', fr: 'Le théâtre le plus emblématique de Málaga. Scannez pour 2 billets au prix d\'1.', de: 'Málagas ikonischstes Theater. Scanne für 2 Tickets zum Preis von 1.', it: 'Il teatro più iconico di Málaga. Scansiona per 2 biglietti al prezzo di 1.', pt: 'O teatro mais icónico de Málaga. Escaneia por 2 bilhetes ao preço de 1.', zh: '马拉加最具标志性的剧院。扫码买一送一。', ru: 'Самый известный театр Малаги. Сканируй — 2 билета по цене 1.' },
    color: '#4a1a6b',
    emoji: '🎭',
  },
};

// ── QR generator (same as PromoScene) ─────────────────────────────────────────
function useQR(seed = 919) {
  return React.useMemo(() => {
    const SIZE = 11;
    const arr = Array(SIZE * SIZE).fill(false);
    const set = (x, y) => { arr[y * SIZE + x] = true; };
    const isFinder = (x, y) => ((x < 3 && y < 3) || (x < 3 && y > SIZE - 4) || (x > SIZE - 4 && y < 3));
    [[0,0],[0,SIZE-3],[SIZE-3,0]].forEach(([fx,fy]) => {
      for (let y = 0; y < 3; y++) for (let x = 0; x < 3; x++)
        if (x===0||x===2||y===0||y===2||(x===1&&y===1)) set(fx+x, fy+y);
    });
    let s = seed;
    const rnd = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s; };
    for (let y = 0; y < SIZE; y++) for (let x = 0; x < SIZE; x++) {
      if (isFinder(x, y)) continue;
      if (rnd() % 100 < 48) set(x, y);
    }
    return arr;
  }, [seed]);
}

// ── Map component — follows real GPS position ─────────────────────────────────
function RideMap({ position, centerKey }) {
  const lat = position?.lat || 36.7213;
  const lon = position?.lon || -4.4214;

  // Build Google Maps embed URL centered on current position
  const src = `https://www.google.com/maps/embed?pb=!1m14!1m12!1m3!1d1200!2d${lon}!3d${lat}!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!5e0!3m2!1ses!2ses!4v${centerKey || Date.now()}`;

  return (
    <div style={{ position: 'absolute', inset: 0, background: '#e8e0d5', overflow: 'hidden' }}>
      <iframe
        key={centerKey || Math.floor(Date.now()/30000)}
        title="Mapa del trayecto"
        src={src}
        style={{ width: '100%', height: '100%', border: 0, filter: 'saturate(0.85)' }}
        loading="lazy"
        allowFullScreen
      />
      {/* GPS indicator */}
      {position && (
        <div style={{
          position: 'absolute', bottom: 8, left: 8,
          background: 'rgba(0,0,0,.5)', backdropFilter: 'blur(6px)',
          borderRadius: 20, padding: '4px 10px',
          fontSize: 10, color: 'rgba(255,255,255,.8)', fontFamily: 'monospace',
          display: 'flex', alignItems: 'center', gap: 5,
        }}>
          <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#4ade80', display: 'inline-block' }} />
          GPS {position.lat.toFixed(4)}, {position.lon.toFixed(4)}
          {position.accuracy && ` ±${Math.round(position.accuracy)}m`}
        </div>
      )}
      <div style={{
        position: 'absolute', bottom: 0, left: 0, right: 0, height: '35%',
        background: 'linear-gradient(to top, rgba(0,0,0,.2), transparent)',
        pointerEvents: 'none',
      }} />
    </div>
  );
}

// ── Curiosity card data — real Wikimedia Commons photos + verified facts ───────
const POI_CARD_DATA = {
  larios: {
    id: 'larios',
    icon: '🏛️',
    photo: 'https://upload.wikimedia.org/wikipedia/commons/a/a4/Calle_Marqu%C3%A9s_de_Larios_M%C3%A1laga.jpg',
    gradient: 'linear-gradient(135deg, #c9a96e, #8B6914)',
    kind:  { es: 'CALLE ICÓNICA', en: 'ICONIC STREET', fr: 'RUE EMBLÉMATIQUE', de: 'IKONE DER STRASSEN', it: 'VIA ICONICA', pt: 'RUA ICÓNICA', zh: '标志性街道', ru: 'ЗНАКОВАЯ УЛИЦА' },
    title: { es: 'Calle Larios', en: 'Calle Larios', fr: 'Calle Larios', de: 'Calle Larios', it: 'Calle Larios', pt: 'Calle Larios', zh: '拉里奥斯大街', ru: 'Улица Лариос' },
    blurb: {
      es: 'Inaugurada el 27 de agosto de 1891 e inspirada en los bulevares de París, es la 3ª calle comercial más cara de España. El mármol que ves bajo tus pies fue traído de Macael, Almería. Solo mide 350 metros — pero cada Navidad acoge el mejor espectáculo de luces del país. Por la noche, es pura magia.',
      en: 'Opened on August 27, 1891, inspired by Parisian boulevards — Spain\'s 3rd most expensive shopping street. The marble under your feet was brought from Macael, Almería. It\'s only 350 metres long, yet every Christmas it hosts Spain\'s most spectacular light show. At night, pure magic.',
      fr: 'Inaugurée le 27 août 1891, inspirée des boulevards parisiens — 3e rue commerciale la plus chère d\'Espagne. Le marbre vient de Macael, Almería. Elle ne mesure que 350 m, mais chaque Noël elle accueille le plus beau spectacle lumineux d\'Espagne.',
      de: 'Eröffnet am 27. August 1891, inspiriert von Pariser Boulevards — Spaniens drittteuerste Einkaufsstraße. Der Marmor stammt aus Macael. Nur 350 Meter lang, aber jedes Weihnachten mit Spaniens spektakulärster Lichtshow.',
      it: 'Inaugurata il 27 agosto 1891, ispirata ai boulevard parigini — 3ª strada commerciale più cara di Spagna. Il marmo viene da Macael. Lunga solo 350 metri, ma ogni Natale ospita il più bello spettacolo di luci di Spagna.',
      pt: 'Inaugurada a 27 de agosto de 1891, inspirada nos bulevares parisienses — 3ª rua comercial mais cara de Espanha. O mármore veio de Macael. Tem apenas 350 metros, mas cada Natal acolhe o mais espetacular espetáculo de luzes de Espanha.',
      zh: '1891年8月27日开放，灵感来自巴黎大道——西班牙第三贵的购物街。脚下的大理石来自马卡埃尔，仅350米长，但每年圣诞节都会举办西班牙最壮观的灯光秀。夜晚纯粹的魔法。',
      ru: 'Открылась 27 августа 1891 года, вдохновлённая парижскими бульварами — третья по стоимости торговая улица Испании. Мрамор из Масаэля. Всего 350 метров, но каждое Рождество здесь — самое яркое световое шоу Испании.',
    },
  },
  catedral: {
    id: 'catedral',
    icon: '⛪',
    photo: 'https://upload.wikimedia.org/wikipedia/commons/e/e2/La_manquita_%28catedral_de_malaga%29.JPG',
    gradient: 'linear-gradient(135deg, #6b8fa3, #2d5a72)',
    kind:  { es: 'MONUMENTO', en: 'LANDMARK', fr: 'MONUMENT', de: 'WAHRZEICHEN', it: 'MONUMENTO', pt: 'MONUMENTO', zh: '地标', ru: 'ДОСТОПРИМЕЧАТЕЛЬНОСТЬ' },
    title: { es: 'La Manquita', en: 'La Manquita', fr: 'La Manquita', de: 'La Manquita', it: 'La Manquita', pt: 'La Manquita', zh: '"独臂少女"', ru: 'Ла-Манкита' },
    blurb: {
      es: 'Su construcción duró 254 años (1528-1782) sobre el antiguo alcázar árabe. La torre que falta se llama "La Manquita" — la mancuerna. Según la leyenda, los fondos se desviaron para financiar la independencia de EE. UU. Las bóvedas alcanzan 41 metros, las más altas de Andalucía. Pablo Picasso fue bautizado aquí.',
      en: 'Construction lasted 254 years (1528-1782), built over the old Arab alcázar. The missing tower earned the nickname "La Manquita" — the one-armed lady. Legend says the funds were diverted to finance American independence. Vaults reach 41 metres — tallest in Andalusia. Pablo Picasso was baptised here.',
      fr: 'Construction de 254 ans (1528-1782), sur l\'ancienne alcazaba arabe. La tour manquante lui valut le surnom «La Manquita» — la manchote. La légende dit que les fonds financèrent l\'indépendance américaine. Voûtes de 41 m — les plus hautes d\'Andalousie. Pablo Picasso y fut baptisé.',
      de: 'Bau dauerte 254 Jahre (1528-1782), über der alten arabischen Alcázar. Der fehlende Turm brachte ihr den Namen „La Manquita" — die Einarmige. Der Legende nach flossen die Mittel in die amerikanische Unabhängigkeit. Gewölbe 41 m — höchste Andalusiens. Pablo Picasso wurde hier getauft.',
      it: 'La costruzione durò 254 anni (1528-1782), sull\'antica alcazaba araba. La torre mancante le valse il soprannome «La Manquita» — la monca. La leggenda dice che i fondi finanziarono l\'indipendenza americana. Volte di 41 m — le più alte dell\'Andalusia. Pablo Picasso fu battezzato qui.',
      pt: 'Construção de 254 anos (1528-1782), sobre a antiga alcáçova árabe. A torre que falta deu-lhe o apelido «La Manquita» — a manca. A lenda diz que os fundos financiaram a independência americana. Abóbadas de 41 m — as mais altas da Andaluzia. Pablo Picasso foi batizado aqui.',
      zh: '建造历时254年（1528-1782），建于古阿拉伯城堡之上。缺失的塔楼使其得名"La Manquita"——独臂少女。传说资金被用于资助美国独立。拱顶41米——安达卢西亚最高。毕加索在此受洗。',
      ru: 'Строилась 254 года (1528-1782) на месте старой арабской крепости. Недостроенная башня дала прозвище «Ла-Манкита» — однорукая. По легенде, средства пошли на финансирование независимости США. Своды 41 м — самые высокие в Андалусии. Пикассо был крещён здесь.',
    },
  },
  atarazanas: {
    id: 'atarazanas',
    icon: '🏪',
    photo: 'https://upload.wikimedia.org/wikipedia/commons/f/fd/Malaga_Central_Market_%28Mercado_Central_de_Atarazanas%29.JPG',
    gradient: 'linear-gradient(135deg, #c4784a, #8b3a1a)',
    kind:  { es: 'MERCADO HISTÓRICO', en: 'HISTORIC MARKET', fr: 'MARCHÉ HISTORIQUE', de: 'HISTORISCHER MARKT', it: 'MERCATO STORICO', pt: 'MERCADO HISTÓRICO', zh: '历史市场', ru: 'ИСТОРИЧЕСКИЙ РЫНОК' },
    title: { es: 'Mercado de Atarazanas', en: 'Atarazanas Market', fr: 'Marché Atarazanas', de: 'Atarazanas-Markt', it: 'Mercato Atarazanas', pt: 'Mercado de Atarazanas', zh: '阿塔拉萨纳斯市场', ru: 'Рынок Атарасанас' },
    blurb: {
      es: 'El arco morisco de la entrada tiene inscripciones en árabe: "Solo Dios es el vencedor". Fue un astillero nazarí del siglo XIV — "Atarazanas" viene del árabe "astillero". El arco fue desmontado piedra a piedra en 1876 y reconstruido. El mercado actual es de hierro y cristal, fusión única de arte árabe e industrial. Pide boquerones fritos: son la seña de identidad de Málaga.',
      en: 'The Moorish arch bears Arabic inscriptions: "Only God is the victor." It was a 14th-century Nasrid shipyard — "Atarazanas" comes from Arabic for shipyard. The arch was dismantled stone by stone in 1876 and rebuilt. Today\'s iron-and-glass market is a unique fusion of Arab and industrial art. Order the fried anchovies — they\'re Málaga\'s signature dish.',
      fr: 'L\'arc moresque porte: «Seul Dieu est le vainqueur». C\'était un chantier naval nasride du XIVe siècle. L\'arc fut démonté pierre par pierre en 1876 et reconstruit. Le marché actuel en fer et verre fusionne art arabe et industriel. Commandez les anchois frits — la spécialité de Málaga.',
      de: 'Der maurische Bogen trägt: „Nur Gott ist der Sieger." 14. Jh. Nasridische Werft — „Atarazanas" aus dem Arabischen für Werft. Der Bogen wurde 1876 Stein für Stein abgetragen und neu aufgebaut. Bestell gebratene Sardellen — Málagas Spezialität.',
      it: 'L\'arco reca: «Solo Dio è il vincitore.» Cantiere navale nasride del XIV sec. L\'arco fu smontato pietra per pietra nel 1876 e ricostruito. Il mercato in ferro e vetro è una fusione unica di arte araba e industriale. Ordina le acciughe fritte — il piatto tipico di Málaga.',
      pt: 'O arco morisco tem: «Só Deus é o vencedor.» Estaleiro nasrida do séc. XIV. O arco foi desmontado pedra a pedra em 1876 e reconstruído. O mercado de ferro e vidro é uma fusão única de arte árabe e industrial. Pede as anchovas fritas — especialidade de Málaga.',
      zh: '摩尔式拱门铭文："唯有真主是胜者"。14世纪纳斯里德造船厂。1876年拱门被逐石拆除重建。铁与玻璃的市场是阿拉伯与工业艺术的独特融合。点炸鳀鱼——马拉加的招牌菜。',
      ru: 'Арка несёт надпись: «Лишь Бог — победитель». Верфь Насридов XIV века. Арка разобрана камень за камнем в 1876 году и восстановлена. Рынок из железа и стекла — уникальное слияние арабского и промышленного искусства. Закажи жареные анчоусы — фирменное блюдо Малаги.',
    },
  },
  malagueta: {
    id: 'malagueta',
    icon: '🏖️',
    photo: 'https://upload.wikimedia.org/wikipedia/commons/5/57/Playa_de_la_Malagueta_02.JPG',
    gradient: 'linear-gradient(135deg, #5b9db5, #1a6a8a)',
    kind:  { es: 'PLAYA URBANA', en: 'CITY BEACH', fr: 'PLAGE URBAINE', de: 'STADTSTRAND', it: 'SPIAGGIA URBANA', pt: 'PRAIA URBANA', zh: '城市海滩', ru: 'ГОРОДСКОЙ ПЛЯЖ' },
    title: { es: 'La Malagueta', en: 'La Malagueta', fr: 'La Malagueta', de: 'La Malagueta', it: 'La Malagueta', pt: 'La Malagueta', zh: '马拉格塔海滩', ru: 'Ла-Малагета' },
    blurb: {
      es: 'La arena es del Desierto del Sahara — importada expresamente. 2.500 metros de playa con Bandera Azul a minutos del centro histórico. El faro La Farola lleva más de 200 años iluminando el puerto y es el único faro con nombre femenino de España. Busca un chiringuito y pide espetos: sardinas en caña de bambú sobre brasas. Es el sabor de Málaga.',
      en: 'The sand was imported from the Sahara Desert. 2,500 metres of Blue Flag beach minutes from the historic centre. La Farola lighthouse has guided sailors for over 200 years and is Spain\'s only lighthouse with a feminine name. Find a chiringuito and order espetos: sardines on bamboo skewers over coals. That\'s the taste of Málaga.',
      fr: 'Le sable vient du Sahara. 2 500 m de plage Pavillon Bleu à quelques minutes du centre. Le phare La Farola guide les marins depuis 200 ans — seul phare féminin d\'Espagne. Commandez des espetos: sardines sur bambou à la braise. C\'est le goût de Málaga.',
      de: 'Sand aus der Sahara. 2.500 m Blaue-Flagge-Strand, Minuten vom Zentrum. Der Leuchtturm La Farola leuchtet seit 200+ Jahren — Spaniens einziger weiblicher Leuchtturm. Bestell Espetos: Sardinen am Bambus. Das ist der Geschmack Málagas.',
      it: 'Sabbia dal Sahara. 2.500 m di spiaggia Bandiera Blu a minuti dal centro. Il faro La Farola guida i marinai da oltre 200 anni — unico faro femminile di Spagna. Ordina gli espetos: sardine su bambù alla brace. È il sapore di Málaga.',
      pt: 'Areia do Saara. 2.500 m de praia Bandeira Azul a minutos do centro. O farol La Farola guia os marinheiros há 200 anos — único farol feminino de Espanha. Pede espetos: sardinhas em bambú na brasa. É o sabor de Málaga.',
      zh: '沙子来自撒哈拉。距市中心几分钟的2500米蓝旗海滩。La Farola灯塔已指引水手200多年——西班牙唯一女性名字的灯塔。点espetos：竹签炭烤沙丁鱼。这就是马拉加的味道。',
      ru: 'Песок из Сахары. 2500 м пляжа с Голубым флагом в минутах от центра. Маяк Ла-Фарола светит морякам 200+ лет — единственный маяк с женским именем в Испании. Закажи эспетос: сардины на бамбуке над углями. Это вкус Малаги.',
    },
  },
};

function CuriosityCard({ id, lang, onClose, voiceOn }) {
  const data    = POI_CARD_DATA[id];
  const title   = data?.title?.[lang]  || data?.title?.en  || id;
  const kind    = data?.kind?.[lang]   || data?.kind?.en   || 'CURIOSIDAD';
  const blurb   = data?.blurb?.[lang]  || data?.blurb?.en  || '';
  const [imgOk, setImgOk] = React.useState(true);

  useEffect(() => {
    if (!voiceOn || !blurb) return;
    setTimeout(() => {
      if (window.speak) window.speak(blurb, lang, { volume: 1 });
    }, 400); // small delay to ensure audio context is ready
    return () => window.stopSpeaking && window.stopSpeaking();
  }, []);

  if (!data) return null;

  return (
    <div style={{ padding: '0 14px 14px' }}>
      <div style={{ borderRadius: 20, overflow: 'hidden', boxShadow: '0 8px 32px -8px rgba(0,0,0,.4)' }}>
        {/* Photo header — takes ~60% of card height */}
        <div style={{ position: 'relative', height: 'min(320px, 58vh)' }}>
          {imgOk
            ? <img src={data.photo} alt={title} onError={() => setImgOk(false)}
                style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center 60%', display: 'block' }} />
            : <div style={{ width: '100%', height: '100%', background: data.gradient, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 48 }}>{data.icon}</div>
          }
          {/* Dark gradient overlay for text legibility */}
          <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,.75) 0%, rgba(0,0,0,.1) 60%)' }} />
          {/* Close button */}
          <button onClick={onClose} style={{
            position: 'absolute', top: 10, right: 10,
            background: 'rgba(0,0,0,.45)', border: 'none', color: 'white',
            width: 28, height: 28, borderRadius: '50%', cursor: 'pointer',
            fontSize: 16, display: 'flex', alignItems: 'center', justifyContent: 'center',
            backdropFilter: 'blur(4px)',
          }}>×</button>
          {/* Title overlay on photo */}
          <div style={{ position: 'absolute', bottom: 12, left: 16, right: 48 }}>
            <div style={{ fontSize: 9, color: 'rgba(255,255,255,.7)', letterSpacing: '0.12em', marginBottom: 3 }}>{kind}</div>
            <div style={{ fontSize: 18, fontWeight: 700, color: 'white', fontFamily: 'var(--font-display)', lineHeight: 1.2 }}>{title}</div>
          </div>
          {/* Source badge */}
          <div style={{ position: 'absolute', bottom: 8, right: 10, fontSize: 8, color: 'rgba(255,255,255,.4)' }}>© Wikimedia</div>
        </div>
        {/* Body — compact */}
        <div style={{ background: 'var(--surface)', padding: '10px 14px' }}>
          <p style={{ fontSize: 12, lineHeight: 1.55, color: 'var(--fg-soft)', margin: '0 0 8px' }}>{blurb}</p>
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={() => window.speak && window.speak(blurb, lang, { volume: 1 })}
              style={{ padding: '7px 12px', borderRadius: 8, border: '1px solid var(--line)', background: 'transparent', cursor: 'pointer', fontSize: 12, color: 'var(--fg)' }}>
              🔁
            </button>
            <button onClick={onClose}
              style={{ flex: 1, padding: '7px 12px', borderRadius: 8, border: 'none', background: 'var(--accent)', color: 'white', cursor: 'pointer', fontSize: 13, fontWeight: 700 }}>
              Volver al mapa →
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ── Promo card ─────────────────────────────────────────────────────────────────
function PromoCard({ id, lang, onClose, voiceOn, promoData }) {
  const promo   = promoData || PROMOS[id];
  const merchantUrl = promo?.qrUrl || '';
  const taxiId      = window.__SESSION_ID || 'taxi-demo';
  const sessionId   = window.__rideSession || Date.now().toString(36);
  const base        = location.origin;
  const merchantName = promo?.merchant || id;
  const offerText    = promo?.offer_es || promo?.offer?.es || '';
  const emoji        = promo?.emoji || '🎁';
  const trackUrl    = `${base}/go.html?m=${id}&t=${taxiId}&s=${sessionId}&url=${encodeURIComponent(merchantUrl)}&name=${encodeURIComponent(merchantName)}&offer=${encodeURIComponent(offerText)}&emoji=${encodeURIComponent(emoji)}`;
  const qrSrc       = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(trackUrl)}&margin=8`;

  // Source is always Spanish; translate to passenger's language
  const sourceOffer = promo?.offer_es || promo?.offer?.es || promo?.offer?.en || '';
  const sourceBlurb = promo?.blurb_es || promo?.blurb?.es || promo?.blurb?.en || '';

  const [offer, setOffer] = React.useState(sourceOffer);
  const [blurb, setBlurb] = React.useState(sourceBlurb);
  const [imgOk, setImgOk] = React.useState(true);

  // Auto-translate to passenger language
  React.useEffect(() => {
    if (lang === 'es' || !window.translate) return;
    window.translate(sourceOffer, 'es', lang).then(setOffer);
    window.translate(sourceBlurb, 'es', lang).then(setBlurb);
  }, [lang, sourceOffer, sourceBlurb]);

  useEffect(() => {
    if (!voiceOn || !blurb) return;
    setTimeout(() => { if (window.speak) window.speak(blurb, lang, { volume: 1 }); }, 400);
    return () => window.stopSpeaking && window.stopSpeaking();
  }, []);

  if (!promo) return null;

  return (
    <div style={{ padding: '0 14px 14px' }}>
      <div style={{ borderRadius: 20, overflow: 'hidden', boxShadow: '0 8px 32px -8px rgba(0,0,0,.4)' }}>
        {/* Photo header */}
        <div style={{ position: 'relative', height: 'min(280px, 52vh)' }}>
          {imgOk && promo.photo
            ? <img src={promo.photo} alt={promo.merchant} onError={() => setImgOk(false)}
                style={{ width: '100%', height: '100%', objectFit: 'cover', objectPosition: 'center', display: 'block' }} />
            : <div style={{ width: '100%', height: '100%', background: promo.color, display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 56 }}>{promo.emoji}</div>
          }
          <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,.8) 0%, rgba(0,0,0,.15) 60%)' }} />
          <button onClick={onClose} style={{
            position: 'absolute', top: 10, right: 10,
            background: 'rgba(0,0,0,.45)', border: 'none', color: 'white',
            width: 28, height: 28, borderRadius: '50%', cursor: 'pointer',
            fontSize: 16, display: 'flex', alignItems: 'center', justifyContent: 'center',
            backdropFilter: 'blur(4px)',
          }}>×</button>
          <div style={{ position: 'absolute', top: 10, left: 14 }}>
            <div style={{ background: 'rgba(255,200,0,.9)', color: '#111', fontSize: 9, fontWeight: 700, padding: '3px 8px', borderRadius: 20, letterSpacing: '0.08em' }}>SÓLO PARA TI · AHORA</div>
          </div>
          <div style={{ position: 'absolute', bottom: 12, left: 16 }}>
            <div style={{ fontSize: 20, fontWeight: 700, color: 'white', fontFamily: 'var(--font-display)' }}>{promo.merchant}</div>
            <div style={{ fontSize: 13, color: 'rgba(255,255,255,.9)', marginTop: 2, fontWeight: 600 }}>{offer}</div>
          </div>
        </div>
        {/* Body: text + QR */}
        <div style={{ background: 'var(--surface)', padding: '14px 20px', display: 'flex', gap: 16, alignItems: 'flex-start' }}>
          <div style={{ flex: 1 }}>
            <p style={{ fontSize: 12, lineHeight: 1.55, color: 'var(--fg-soft)', margin: '0 0 10px' }}>{blurb}</p>
            <div style={{ display: 'flex', gap: 8 }}>
              <button onClick={() => window.speak && window.speak(blurb, lang, { volume: 1 })}
                style={{ padding: '6px 12px', borderRadius: 8, border: '1px solid var(--line)', background: 'transparent', cursor: 'pointer', fontSize: 11, color: 'var(--fg)' }}>
                🔁
              </button>
              <button onClick={onClose}
                style={{ flex: 1, padding: '6px 12px', borderRadius: 8, border: 'none', background: 'var(--accent)', color: 'white', cursor: 'pointer', fontSize: 11, fontWeight: 600 }}>
                Volver al mapa
              </button>
            </div>
          </div>
          {/* Real QR from URL */}
          <div style={{ flexShrink: 0, textAlign: 'center' }}>
            <img src={qrSrc} alt="QR" style={{ width: 88, height: 88, borderRadius: 10, background: 'white', padding: 4, display: 'block' }} />
            <div style={{ fontSize: 9, color: 'var(--fg-mute)', marginTop: 4 }}>ESCANEA</div>
          </div>
        </div>
      </div>
    </div>
  );
}

const TRANSLATE_LABEL = {
  es: 'Traducir', en: 'Translate', fr: 'Traduire', de: 'Übersetzen',
  it: 'Tradurre', pt: 'Traduzir', zh: '翻译', ru: 'Перевод',
};

// ── Mic button for in-ride translation ───────────────────────────────────────
function RideMicButton({ lang, voiceOn, onResume }) {
  const [listening,  setListening]  = useState(false);
  const [original,   setOriginal]   = useState('');
  const [translated, setTranslated] = useState('');
  const lastRef = useRef('');

  const toggle = () => {
    if (listening) {
      window.stopListening && window.stopListening();
      setListening(false);
      const text = lastRef.current;
      if (!text) return;
      setOriginal(text);
      const translateFn = window.translate || ((t) => Promise.resolve(t));
      translateFn(text, lang, 'es').then(tr => {
        setTranslated(tr);
        if ('speechSynthesis' in window) {
          window.speechSynthesis.cancel();
          const u = new SpeechSynthesisUtterance(tr);
          u.lang = 'es-ES'; u.rate = 0.92;
          window.speechSynthesis.speak(u);
        }
        if (window.publishTranslation) window.publishTranslation({ role: 'passenger', source: text, target: tr, srcLang: lang, tgtLang: 'es' });
        setTimeout(() => {
          setOriginal('');
          setTranslated('');
          // Resume card narration after translation bubble closes
          if (onResume) onResume();
        }, 6000);
      });
    } else {
      lastRef.current = '';
      setOriginal('');
      setTranslated('');
      setListening(true);
      // Stop Leo speaking so mic picks up passenger clearly
      window.stopSpeaking && window.stopSpeaking();
      window.speechSynthesis && window.speechSynthesis.cancel();
      window.startListening({
        lang,
        onPartial: t => { lastRef.current = t; setOriginal(t); },
        onFinal:   t => { lastRef.current = t; setOriginal(t); },
        onError:   () => setListening(false),
      });
    }
  };

  const showBubble = original || translated;

  return (
    <div style={{ position: 'relative' }}>
      <button onClick={toggle} style={{
        width: 52, height: 52, borderRadius: '50%', border: 'none',
        background: listening ? '#ef4444' : 'var(--accent)',
        color: listening ? 'white' : '#111',
        cursor: 'pointer', fontSize: 20,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        boxShadow: listening ? '0 0 0 8px rgba(239,68,68,.3)' : '0 4px 12px rgba(0,0,0,.3)',
        transition: 'all .2s',
      }}>
        {listening ? '⏹' : '🎙'}
      </button>

      {showBubble && (
        <div style={{
          position: 'absolute', bottom: 64, right: 0, width: 240,
          background: 'rgba(0,0,0,.88)', backdropFilter: 'blur(10px)',
          borderRadius: 14, padding: '12px 14px', color: 'white',
          boxShadow: '0 4px 20px rgba(0,0,0,.5)',
          animation: 'slideUp .3s ease',
        }}>
          {/* Original in passenger language */}
          {original && (
            <div style={{ marginBottom: translated ? 8 : 0 }}>
              <div style={{ fontSize: 9, color: 'rgba(255,255,255,.5)', letterSpacing: '.08em', marginBottom: 3 }}>
                {lang.toUpperCase()} · {listening ? '🔴 Escuchando…' : ''}
              </div>
              <div style={{ fontSize: 13, color: 'rgba(255,255,255,.85)', fontStyle: 'italic' }}>
                "{original}"
              </div>
            </div>
          )}
          {/* Translation in Spanish */}
          {translated && (
            <div style={{ borderTop: '1px solid rgba(255,255,255,.12)', paddingTop: 8 }}>
              <div style={{ fontSize: 9, color: 'rgba(255,255,255,.5)', letterSpacing: '.08em', marginBottom: 3 }}>
                ES · LEO DICE
              </div>
              <div style={{ fontSize: 14, fontWeight: 700, color: '#e8a87c' }}>
                {translated}
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

// ── Volume control for ride scene ────────────────────────────────────────────
function RideVolumeBtn() {
  const [vol,   setVol]   = useState(0.85);
  const [open,  setOpen]  = useState(false);
  const [muted, setMuted] = useState(false);
  const prevVol           = useRef(0.85);

  const applyVol = (v) => { setVol(v); window.__rideVolume = v; };

  const toggleMute = () => {
    if (muted) {
      setMuted(false);
      applyVol(prevVol.current || 0.85);
    } else {
      prevVol.current = vol;
      setMuted(true);
      applyVol(0);
      window.stopSpeaking && window.stopSpeaking();
    }
  };

  const volIcon = vol < 0.01 ? '🔇' : vol < 0.5 ? '🔉' : '🔊';

  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 10, position: 'relative' }}>

      {/* Volume slider popup */}
      {open && (
        <div style={{
          position: 'absolute', bottom: 56, left: 44,
          background: 'rgba(0,0,0,.85)', backdropFilter: 'blur(10px)',
          borderRadius: 14, padding: '10px 16px',
          display: 'flex', alignItems: 'center', gap: 10,
          boxShadow: '0 4px 20px rgba(0,0,0,.5)', zIndex: 30,
          whiteSpace: 'nowrap',
        }}>
          <span style={{ color: 'white', fontSize: 13 }}>🔇</span>
          <input type="range" min="0" max="100" value={Math.round(vol * 100)}
            onChange={(e) => { setMuted(false); applyVol(Number(e.target.value) / 100); }}
            style={{ width: 130, accentColor: '#e8a87c', cursor: 'pointer' }} />
          <span style={{ color: 'white', fontSize: 13 }}>🔊</span>
          <span style={{ color: '#e8a87c', fontSize: 12, fontWeight: 600, minWidth: 32 }}>
            {Math.round(vol * 100)}%
          </span>
        </div>
      )}

      {/* Mute toggle button */}
      <button onClick={toggleMute} style={{
        width: 44, height: 44, borderRadius: '50%', border: 'none',
        background: muted ? 'rgba(248,113,113,.35)' : 'rgba(255,255,255,.15)',
        backdropFilter: 'blur(8px)', color: 'white', cursor: 'pointer', fontSize: 18,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }} title={muted ? 'Activar sonido' : 'Silenciar'}>
        {muted ? '🔇' : '🔊'}
      </button>

      {/* Volume level button — opens slider */}
      <button onClick={() => setOpen(o => !o)} style={{
        height: 28, padding: '0 10px', borderRadius: 14, border: 'none',
        background: 'rgba(255,255,255,.12)', backdropFilter: 'blur(8px)',
        color: 'rgba(255,255,255,.8)', cursor: 'pointer', fontSize: 11, fontWeight: 600,
        display: 'flex', alignItems: 'center', gap: 4,
      }} title="Ajustar volumen">
        {volIcon} {Math.round(vol * 100)}%
      </button>

    </div>
  );
}

// ── Main RideExperience component ─────────────────────────────────────────────
function RideExperience({ active, lang, voiceOn, isSpeaking, onPlay }) {
  const [activeCard,    setActiveCard]    = useState(null);
  const [elapsed,       setElapsed]       = useState(0);
  const [queue,         setQueue]         = useState([]);
  const [livePromos,    setLivePromos]    = useState(null);
  const [liveTimeline,  setLiveTimeline]  = useState(null);
  const [gpsPosition,   setGpsPosition]   = useState(null);
  const [gpsMode,       setGpsMode]       = useState(false);
  const [destination,   setDestination]   = useState(null);
  const [allowedPromos, setAllowedPromos] = useState(null);
  const [mapCenterKey,  setMapCenterKey]  = useState(Date.now());
  const timerRef  = useRef(null);
  const firedRef  = useRef(new Set());
  const watchRef  = useRef(null);

  // Listen for admin updates pushed via PubNub
  useEffect(() => {
    const handler = (e) => {
      const { promos, timeline } = e.detail;
      if (promos)   setLivePromos(promos);
      if (timeline) setLiveTimeline(timeline);
    };
    window.addEventListener('admin_update', handler);
    return () => window.removeEventListener('admin_update', handler);
  }, []);

  // Active promos: merge admin overrides over defaults
  // Filter inactive promos — don't show on tablet
  const activePromos = livePromos ? Object.fromEntries(
    Object.entries(PROMOS).map(([k, def]) => {
      const ov = livePromos[k];
      if (!ov) return [k, def];
      return [k, { ...def, merchant: ov.merchant, zone: ov.zone, photo: ov.photo,
        color: ov.color, emoji: ov.emoji,
        offer: { ...def.offer, es: ov.offer_es, en: ov.offer_en },
        blurb: { ...def.blurb, es: ov.blurb_es, en: ov.blurb_en },
      }];
    })
  ) : PROMOS;

  const activeTimeline = liveTimeline
    ? RIDE_TIMELINE.map(ev => ({ ...ev, at: liveTimeline[ev.id] ?? ev.at }))
    : RIDE_TIMELINE;

  // Start GPS tracking when ride is active
  useEffect(() => {
    if (!active) {
      if (watchRef.current) navigator.geolocation?.clearWatch(watchRef.current);
      setElapsed(0);
      firedRef.current.clear();
      setGpsPosition(null);
      setGpsMode(false);
      return;
    }

    // Try GPS
    if ('geolocation' in navigator) {
      watchRef.current = navigator.geolocation.watchPosition(
        (pos) => {
          const { latitude: lat, longitude: lon, accuracy } = pos.coords;
          setGpsPosition({ lat, lon, accuracy });
          setGpsMode(true);

          // On first GPS fix: calculate allowed promos (1km from origin + destination)
          setAllowedPromos(prev => {
            if (prev !== null) return prev; // already calculated
            const dest = window.__rideDestination;
            const allowed = new Set();
            Object.entries(POI_COORDS).forEach(([id, poi]) => {
              if (poi.type !== 'promo') return; // curiosities always allowed
              const fromOrigin = haversineMeters(lat, lon, poi.lat, poi.lon);
              const fromDest   = dest ? haversineMeters(dest.lat, dest.lon, poi.lat, poi.lon) : Infinity;
              if (fromOrigin <= 1000 || fromDest <= 1000) allowed.add(id);
            });
            console.log('[promos] allowed near origin/dest:', [...allowed]);
            return allowed;
          });

          // Check proximity — curiosities always, promos only if allowed
          Object.entries(POI_COORDS).forEach(([id, poi]) => {
            if (firedRef.current.has(id)) return;
            const dist = haversineMeters(lat, lon, poi.lat, poi.lon);
            if (dist > poi.radius) return;
            if (poi.type === 'promo') return; // promos handled at destination approach
            firedRef.current.add(id);
            setQueue(q => [...q, { type: poi.type, id }]);
          });

          // Show allowed promos when within 1.5km of destination
          const dest = window.__rideDestination;
          if (dest) {
            const distToDest = haversineMeters(lat, lon, dest.lat, dest.lon);
            if (distToDest <= 1500) {
              setAllowedPromos(allowed => {
                if (!allowed) return allowed;
                allowed.forEach(id => {
                  if (!firedRef.current.has(id)) {
                    firedRef.current.add(id);
                    setQueue(q => [...q, { type: 'promo', id }]);
                  }
                });
                return allowed;
              });
            }
          }
        },
        () => setGpsMode(false),
        { enableHighAccuracy: true, maximumAge: 5000, timeout: 10000 }
      );
    }

    // Timer fallback (always runs as backup for demo without GPS)
    timerRef.current = setInterval(() => setElapsed(e => e + 1), 1000);
    return () => {
      clearInterval(timerRef.current);
      if (watchRef.current) navigator.geolocation?.clearWatch(watchRef.current);
    };
  }, [active]);

  // Timer-based events — fallback when GPS not available
  useEffect(() => {
    if (gpsMode) return;
    activeTimeline.forEach(ev => {
      if (elapsed >= ev.at && !firedRef.current.has(ev.id)) {
        firedRef.current.add(ev.id);
        if (ev.type === 'message' && ev.id === 'seatbelt') {
          if (voiceOn && window.speak) setTimeout(() => window.speak(t('seatbelt', lang), lang, { volume: 1 }), 500);
          return;
        }
        if (ev.type === 'promo') {
          const dest = window.__rideDestination;
          if (dest && allowedPromos !== null && !allowedPromos.has(ev.id)) return;
          // Skip promos marked inactive in admin
          const pd = (livePromos && livePromos[ev.id]) || PROMOS[ev.id];
          if (pd?.active === false) return;
        }
        setQueue(q => [...q, ev]);
      }
    });
  }, [elapsed, gpsMode, allowedPromos]);

  // Show next queued card — with 3s pause so passenger sees the map first
  const nextCardRef = useRef(null);
  useEffect(() => {
    if (activeCard || queue.length === 0) return;
    if (nextCardRef.current) clearTimeout(nextCardRef.current);
    nextCardRef.current = setTimeout(() => {
      setActiveCard(queue[0]);
      setQueue(q => q.slice(1));
    }, 3000);
    return () => clearTimeout(nextCardRef.current);
  }, [activeCard, queue]);

  const dismissCard = useCallback(() => {
    window.stopSpeaking && window.stopSpeaking();
    setActiveCard(null);
  }, []);

  // Auto-dismiss card after 20s so the queue keeps flowing
  const autoDismissRef = useRef(null);
  useEffect(() => {
    if (autoDismissRef.current) clearTimeout(autoDismissRef.current);
    if (activeCard) {
      autoDismissRef.current = setTimeout(dismissCard, 20000);
    }
    return () => clearTimeout(autoDismissRef.current);
  }, [activeCard, dismissCard]);

  if (!active) return null;

  return (
    <div style={{ position: 'absolute', inset: 0 }}>
      {/* Map — follows GPS when available */}
      <RideMap position={gpsPosition} centerKey={mapCenterKey} />


      {/* Top status — offset right to avoid Google Maps UI on left */}
      <div style={{
        position: 'absolute', top: 8, right: 12, zIndex: 5,
        display: 'flex', alignItems: 'center', gap: 6,
        background: 'rgba(0,0,0,.5)', backdropFilter: 'blur(6px)',
        borderRadius: 20, padding: '5px 12px',
      }}>
        <div style={{ width: 7, height: 7, borderRadius: '50%', background: '#4ade80', boxShadow: '0 0 5px #4ade80' }} />
        <span style={{ color: 'white', fontSize: 11, fontFamily: 'var(--font-mono)', letterSpacing: '0.05em' }}>
          LEO · {Math.floor(elapsed / 60)}:{String(elapsed % 60).padStart(2, '0')}
        </span>
      </div>

      {/* Leo avatar — only when no card */}
      {!activeCard && (
        <div style={{
          position: 'absolute', bottom: 70, left: '50%', transform: 'translateX(-50%)',
          zIndex: 6, width: 100, height: 120,
          filter: 'drop-shadow(0 4px 16px rgba(0,0,0,.5))',
        }}>
          <Avatar mode={isSpeaking ? 'speaking' : 'idle'} size="sm" name={AVATAR_NAME} role="" />
        </div>
      )}

      {/* Persistent controls — always visible, below any card */}
      <div style={{
        position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 20,
        padding: '10px 20px 14px',
        background: 'rgba(0,0,0,.6)', backdropFilter: 'blur(10px)',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        gap: 12,
      }}>
        {/* Left: mute toggle + volume */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <RideVolumeBtn />
        </div>

        {/* Center: re-center map button */}
        <button
          onClick={() => setMapCenterKey(Date.now())}
          style={{
            height: 44, padding: '0 16px', borderRadius: 22, border: 'none',
            background: gpsPosition ? 'white' : 'rgba(255,255,255,.2)',
            color: gpsPosition ? '#1a73e8' : 'rgba(255,255,255,.7)',
            cursor: 'pointer', fontSize: 14, fontWeight: 600,
            display: 'flex', alignItems: 'center', gap: 6,
            boxShadow: gpsPosition ? '0 2px 8px rgba(0,0,0,.2)' : 'none',
          }}
          title={gpsPosition ? 'Centrar en mi ubicación' : 'GPS no disponible'}>
          📍 {gpsPosition ? 'Centrar' : 'Sin GPS'}
        </button>

        {/* Right: mic for translation */}
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
          <RideMicButton lang={lang} voiceOn={voiceOn} onResume={() => {
            if (!activeCard || !voiceOn) return;
            const data = activeCard.type === 'curiosity'
              ? POI_CARD_DATA[activeCard.id]
              : (activePromos[activeCard.id] || PROMOS[activeCard.id]);
            const blurb = data?.blurb?.[lang] || data?.blurb?.en;
            if (blurb && window.speak) setTimeout(() => window.speak(blurb, lang, { volume: 1 }), 500);
          }} />
          <span style={{ color: 'rgba(255,255,255,.55)', fontSize: 9 }}>
            {TRANSLATE_LABEL[lang] || 'Translate'}
          </span>
        </div>
      </div>

      {/* Design-mode preview controls — hidden in kiosk */}
      {!window.KIOSK && (
        <div style={{
          position: 'absolute', top: 44, left: 0, right: 0, zIndex: 8,
          padding: '6px 10px',
          background: 'rgba(0,0,0,.55)', backdropFilter: 'blur(6px)',
          display: 'flex', gap: 6, flexWrap: 'wrap',
        }}>
          {[
            { id: 'larios',      label: '🏛️ Larios',     type: 'curiosity' },
            { id: 'catedral',    label: '⛪ Manquita',    type: 'curiosity' },
            { id: 'atarazanas',  label: '🏪 Atarazanas',  type: 'curiosity' },
            { id: 'malagueta',   label: '🏖️ Malagueta',   type: 'curiosity' },
            { id: 'pimpi',       label: '🍷 El Pimpi',    type: 'promo' },
            { id: 'hotel_larios',label: '🏨 Hotel',       type: 'promo' },
            { id: 'teatro',      label: '🎭 Teatro',      type: 'promo' },
          ].map(ev => (
            <button key={ev.id} onClick={() => { setActiveCard(ev); window.stopSpeaking && window.stopSpeaking(); }}
              style={{
                padding: '4px 10px', borderRadius: 20, border: 'none',
                background: ev.type === 'promo' ? 'rgba(232,168,124,.85)' : 'rgba(255,255,255,.15)',
                color: ev.type === 'promo' ? '#111' : 'white',
                fontSize: 11, fontWeight: 600, cursor: 'pointer',
              }}>
              {ev.label}
            </button>
          ))}
          <button onClick={() => { setActiveCard(null); window.stopSpeaking && window.stopSpeaking(); }}
            style={{ padding: '4px 10px', borderRadius: 20, border: 'none', background: 'rgba(248,113,113,.7)', color: 'white', fontSize: 11, fontWeight: 600, cursor: 'pointer', marginLeft: 'auto' }}>
            ✕ Cerrar
          </button>
        </div>
      )}

      {/* Queue indicator */}
      {queue.length > 0 && !activeCard && (
        <div style={{
          position: 'absolute', top: 50, right: 14, zIndex: 6,
          background: 'var(--accent)', color: 'white',
          borderRadius: 20, padding: '4px 10px', fontSize: 11, fontWeight: 600,
        }}>
          {queue.length} aviso{queue.length > 1 ? 's' : ''} pendiente{queue.length > 1 ? 's' : ''}
        </div>
      )}

      {/* Active card — covers 72% of screen, map visible above */}
      {activeCard && (
        <div style={{
          position: 'absolute', bottom: 0, left: 0, right: 0,
          height: '72%', zIndex: 10,
          animation: 'slideUp .35s cubic-bezier(0.22,1,0.36,1)',
          display: 'flex', flexDirection: 'column',
        }}>
          {/* Gradient fade from map into card */}
          <div style={{ height: 32, background: 'linear-gradient(to bottom, transparent, rgba(0,0,0,.3))', flexShrink: 0 }} />

          {/* Card content — fills remaining height */}
          <div style={{ flex: 1, minHeight: 0, display: 'flex', padding: '0 12px 12px', gap: 10 }}>
            {/* Leo avatar on left */}
            <div style={{ width: 100, flexShrink: 0, display: 'flex', alignItems: 'flex-end' }}>
              <Avatar mode={isSpeaking ? 'speaking' : 'idle'} size="sm" name={AVATAR_NAME} role="" />
            </div>
            {/* Card on right — fills remaining width */}
            <div style={{ flex: 1, minWidth: 0, overflowY: 'auto', borderRadius: 20 }}>
              {activeCard.type === 'curiosity' && (
                <CuriosityCard id={activeCard.id} lang={lang} voiceOn={voiceOn} onClose={dismissCard} />
              )}
              {activeCard.type === 'promo' && (
                <PromoCard id={activeCard.id} lang={lang} voiceOn={voiceOn} onClose={dismissCard} promoData={activePromos[activeCard.id]} />
              )}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// Unlock browser audio on first user interaction
(function() {
  const unlock = () => {
    const ctx = new (window.AudioContext || window.webkitAudioContext)();
    ctx.resume().then(() => ctx.close());
    const s = new Audio('data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA=');
    s.play().catch(() => {});
    window.__audioUnlocked = true;
    document.removeEventListener('pointerdown', unlock);
  };
  document.addEventListener('pointerdown', unlock, { once: true });
})();

// CSS for slide-up animation
(function() {
  if (document.getElementById('ride-kf')) return;
  const s = document.createElement('style');
  s.id = 'ride-kf';
  s.textContent = `
    @keyframes slideUp {
      from { transform: translateY(100%); opacity: 0; }
      to   { transform: translateY(0);    opacity: 1; }
    }
  `;
  document.head.appendChild(s);
})();

Object.assign(window, { RideExperience });
