class CalendarApp {
  constructor(){
    this.userId = (window.__CALENDAR_USER_ID__ || 1);
    this.view = 'month';
    this.current = new Date();
    this.events = [];
    this.dataConfig = { sseUrl: (window.CALENDAR_SSE_URL || null), dataUrl: (window.CALENDAR_DATA_URL || null) };
    // Filtros de calendarios (persistencia en localStorage)
    try {
      const saved = JSON.parse(localStorage.getItem('calendarFilters')||'{}');
      this.filters = {
        desk: saved.desk!==undefined ? !!saved.desk : true,
        work: saved.work!==undefined ? !!saved.work : true,
        tasks: saved.tasks!==undefined ? !!saved.tasks : true,
      };
    } catch(e) {
      this.filters = { desk: true, work: true, tasks: true };
    }
  }

  init(){
    this.loadEvents();
    this.bindToolbar();
    this.render();
    this.remindersEnabled = true; // estado inicial de la campana de recordatorios
    this.reminderOffsetMin = 15;
    this.notifiedKeys = new Set();
    this.startReminderScheduler();
    this.startNowLineUpdater();
    this.initDataLoading();
    this.toast('Calendario listo');
  }

  bindToolbar(){
    const prev = document.getElementById('calPrev');
    const next = document.getElementById('calNext');
    const today = document.getElementById('calToday');
    const addBtn = document.getElementById('calAddEvent');
    const addBtnHeader = document.getElementById('calAddEventHeader');
    const addBtnSidebar = document.getElementById('calAddEventSidebar');
    const search = document.getElementById('calSearch');
    const bell = document.getElementById('calNotifyBell');
    const bellCount = document.getElementById('calNotifyCount');
    const offsetSel = document.getElementById('calNotifyOffset');
    const toggleSidebar = document.getElementById('calToggleSidebar');
    const densityToggle = document.getElementById('calDensityToggle');
    const fullWidthToggle = document.getElementById('calFullWidthToggle');
    const cbDesk = document.getElementById('calCalendarDesk');
    const cbWork = document.getElementById('calCalendarWork');
    const cbTasks = document.getElementById('calCalendarTasks');

    if (prev) prev.addEventListener('click', ()=>{ this.shift(-1); });
    if (next) next.addEventListener('click', ()=>{ this.shift(1); });
    if (today) today.addEventListener('click', ()=>{ this.current = new Date(); this.render(); });
    if (addBtn) addBtn.addEventListener('click', ()=> this.quickCreate());
    if (addBtnHeader) addBtnHeader.addEventListener('click', ()=> this.quickCreate());
    if (addBtnSidebar) addBtnSidebar.addEventListener('click', ()=> this.quickCreate());
    let deb;
    search?.addEventListener('input', ()=>{ clearTimeout(deb); deb=setTimeout(()=> this.render(), 120); });

    // Campana de recordatorios (activar/desactivar)
    if (bell) {
      bell.addEventListener('click', ()=>{
        this.remindersEnabled = !this.remindersEnabled;
        this.updateBellUI();
        this.toast(this.remindersEnabled ? 'Recordatorios activados' : 'Recordatorios desactivados');
      });
      // Estado inicial
      this.updateBellUI();
    }

    // Selector de tiempo de aviso
    if (offsetSel) {
      try { offsetSel.value = String(this.reminderOffsetMin); } catch(e){}
      offsetSel.addEventListener('change', ()=>{
        const v = parseInt(offsetSel.value, 10) || 15;
        this.reminderOffsetMin = v;
        this.notifiedKeys.clear();
        this.updateBellUI();
        this.toast(`Aviso ${v} min antes`);
      });
    }

    // Toggle del panel lateral y colapso automático en pantallas pequeñas
    if (toggleSidebar) {
      const layout = document.querySelector('.calendar-layout');
      const mq = window.matchMedia('(max-width: 960px)');
      const applyByMQ = ()=>{
        if (!layout) return;
        if (mq.matches) layout.classList.add('sidebar-collapsed');
        else layout.classList.remove('sidebar-collapsed');
      };
      applyByMQ();
      try { mq.addEventListener('change', applyByMQ); } catch(e) { mq.addListener(applyByMQ); }
      toggleSidebar.addEventListener('click', ()=>{ layout?.classList.toggle('sidebar-collapsed'); });
    }

    // Alternar densidad (compacta vs cómoda)
    if (densityToggle) {
      densityToggle.addEventListener('click', ()=>{
        const cal = document.getElementById('calendar');
        if (!cal) return; cal.classList.toggle('dense');
      });
    }

    // Alternar modo de ancho completo del módulo (oculta sidebar y expande layout)
    if (fullWidthToggle) {
      fullWidthToggle.addEventListener('click', ()=>{
        const mod = document.getElementById('calendar-module');
        mod?.classList.toggle('full-width');
        // Al activar ancho completo, también colapsar visualmente el sidebar
        const layout = document.querySelector('.calendar-layout');
        layout?.classList.toggle('sidebar-collapsed', mod?.classList.contains('full-width'));
      });
    }

    // Filtros del sidebar: sincronizar estado inicial y escuchar cambios
    if (cbDesk) cbDesk.checked = !!this.filters.desk;
    if (cbWork) cbWork.checked = !!this.filters.work;
    if (cbTasks) cbTasks.checked = !!this.filters.tasks;
    const applyFilter = ()=>{
      if (cbDesk) this.filters.desk = !!cbDesk.checked;
      if (cbWork) this.filters.work = !!cbWork.checked;
      if (cbTasks) this.filters.tasks = !!cbTasks.checked;
      try { localStorage.setItem('calendarFilters', JSON.stringify(this.filters)); } catch(e){}
      this.render();
    };
    cbDesk?.addEventListener('change', applyFilter);
    cbWork?.addEventListener('change', applyFilter);
    cbTasks?.addEventListener('change', applyFilter);

    // Nuevo header - Event listeners
    const menuToggle = document.getElementById('calMenuToggle');
    const todayBtn = document.getElementById('calTodayBtn');
    const prevBtn = document.getElementById('calPrevBtn');
    const nextBtn = document.getElementById('calNextBtn');
    const searchInput = document.getElementById('calSearchInput');
    const settingsBtn = document.getElementById('calSettingsBtn');
    const fabCreate = document.getElementById('calFabCreate');

    // Navegación
    todayBtn?.addEventListener('click', ()=>{
      this.current = new Date();
      this.render();
    });
    prevBtn?.addEventListener('click', ()=>{
      this.shift(-1);
    });
    nextBtn?.addEventListener('click', ()=>{
      this.shift(1);
    });

    // Búsqueda
    let searchTimeout;
    searchInput?.addEventListener('input', (e)=>{
      clearTimeout(searchTimeout);
      searchTimeout = setTimeout(()=>{
        this.searchTerm = e.target.value.toLowerCase();
        this.render();
      }, 300);
    });

    // Botón flotante crear
    fabCreate?.addEventListener('click', ()=>{
      this.quickCreate();
    });

    // Menu toggle (para futura funcionalidad)
    menuToggle?.addEventListener('click', ()=>{
      this.toast('Menú en desarrollo');
    });

    // Settings
    settingsBtn?.addEventListener('click', ()=>{
      this.toast('Configuración en desarrollo');
    });
  }

  setView(v){
    this.view = 'month';
    this.render();
  }

  shift(delta){
    this.current.setMonth(this.current.getMonth() + delta);
    this.render();
  }

  loadEvents(){
    const key = `calendarEvents:${this.userId}`;
    try{
      const raw = localStorage.getItem(key);
      if (raw){ 
        this.events = JSON.parse(raw)||[]; 
        // Normalizar eventos antiguos para usar calendario 'desk'
        this.events.forEach(ev=>{ if (!ev.calendar) ev.calendar = 'desk'; });
        if (!this.events.length){ this.ensureSampleWeek(); localStorage.setItem(key, JSON.stringify(this.events)); }
      }
      else {
        // Sembrar semana de ejemplo similar al screenshot
        this.ensureSampleWeek();
        localStorage.setItem(key, JSON.stringify(this.events));
      }
    }catch(e){ this.events=[]; }
  }

  saveEvents(){
    const key = `calendarEvents:${this.userId}`;
    try{ localStorage.setItem(key, JSON.stringify(this.events)); }catch(e){}
  }

  // Colores por calendario
  getCalendarColor(cal){
    switch(cal){
      case 'desk': return '#f97316'; // naranja
      case 'work': return '#3b82f6'; // azul
      case 'tasks': return '#6366f1'; // índigo
      default: return 'var(--primary)';
    }
  }

  // Genera una semana de eventos de muestra (estilo Google) si no hay datos
  ensureSampleWeek(){
    const base = new Date(this.current.getTime());
    // Encontrar lunes de la semana actual
    const day = base.getDay(); // 0=Dom
    const diffToMonday = (day===0)? -6 : (1-day);
    const monday = new Date(base.getFullYear(), base.getMonth(), base.getDate()+diffToMonday);
    monday.setHours(0,0,0,0);
    const add = (d, sh, sm, eh, em, title, cal)=>{
      const s = new Date(d.getFullYear(), d.getMonth(), d.getDate(), sh, sm, 0, 0);
      const e = new Date(d.getFullYear(), d.getMonth(), d.getDate(), eh, em, 0, 0);
      this.events.push({ id: Date.now()+Math.floor(Math.random()*100000)+this.events.length, userId:this.userId, title, startISO:s.toISOString(), endISO:e.toISOString(), allDay:false, calendar:cal });
    };
    // Reunión diaria (Lun–Vie) 9:00–9:30 (work)
    for(let i=0;i<5;i++){ const d = new Date(monday.getTime()); d.setDate(monday.getDate()+i); add(d, 9, 0, 9, 30, 'Daily Office Meeting', 'work'); }
    // Lunch (Lun–Vie) 13:00–14:00 (desk)
    for(let i=0;i<5;i++){ const d = new Date(monday.getTime()); d.setDate(monday.getDate()+i); add(d, 13, 0, 14, 0, 'Lunch', 'desk'); }
    // Martes 11:00–12:00 (desk)
    { const t = new Date(monday.getTime()); t.setDate(monday.getDate()+1); add(t, 11, 0, 12, 0, 'Michael/Dwight 1:1', 'desk'); }
    // Lunes 17:00–18:00 (work)
    { const d = new Date(monday.getTime()); add(d, 17, 0, 18, 0, 'Screenwriting', 'work'); }
    // Jueves 17:00–18:00 (work)
    { const th = new Date(monday.getTime()); th.setDate(monday.getDate()+3); add(th, 17, 0, 18, 0, 'Dentist Appointment', 'work'); }
    // Martes 19:00–20:00 (work)
    { const t = new Date(monday.getTime()); t.setDate(monday.getDate()+1); add(t, 19, 0, 20, 0, 'Secret Company Work', 'work'); }
    // Viernes 14:15–15:15 (work)
    { const f = new Date(monday.getTime()); f.setDate(monday.getDate()+4); add(f, 14, 15, 15, 15, 'Improv Class', 'work'); }
    // Sábado 16:30–19:30 (work)
    { const s = new Date(monday.getTime()); s.setDate(monday.getDate()+5); add(s, 16, 30, 19, 30, 'More Improv', 'work'); }
  }

  quickCreate(){
    const base = new Date(this.current.getTime());
    base.setHours(10,0,0,0);
    const end = new Date(base.getTime()); end.setMinutes(end.getMinutes()+30);
    const id = Date.now();
    this.events.push({ id, userId:this.userId, title:'Nuevo evento', startISO:base.toISOString(), endISO:end.toISOString(), allDay:false, calendar:'desk' });
    this.saveEvents();
    this.render();
    this.toast('Evento creado');
  }

  render(){
    const cal = document.getElementById('calendar');
    if (!cal) return;
    cal.innerHTML='';
    const range = this.getRange();
    const label = this.formatRangeLabel(range.start, range.end);
    const rl = document.getElementById('calRangeLabel');
    if (rl) { rl.setAttribute('aria-label', `Rango: ${label}`); rl.textContent = label; }
    const sr = document.getElementById('cal-status-range');
    if (sr) sr.textContent = label;
    
    // Actualizar mes actual en el header
    const currentMonthEl = document.getElementById('calCurrentMonth');
    if (currentMonthEl) {
      const monthNames = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
      currentMonthEl.textContent = `${monthNames[this.current.getMonth()]} ${this.current.getFullYear()}`;
    }
    
    this.renderMonth(cal, range);
    this.updateKpis(range);
    this.enhanceDynamics();
    this.renderMiniMonth();
    this.updateSidebarWidgets(range);
    // Animación suave y actualización del contador de campana
    cal.classList.add('anim-fade');
    setTimeout(()=> cal.classList.remove('anim-fade'), 220);
    this.updateBellUI();
  }

  getRange(){
    const d = new Date(this.current.getTime());
    const start = new Date(d.getFullYear(), d.getMonth(), 1, 0,0,0,0);
    const end = new Date(d.getFullYear(), d.getMonth()+1, 0, 23,59,59,999);
    return { start, end };
  }

  updateKpis(range){
    try{
      const todayCountEl = document.getElementById('kpiTodayCount');
      const todaySubEl = document.getElementById('kpiTodaySub');
      const todayTrendEl = document.getElementById('kpiTodayTrend');
      const weekCountEl = document.getElementById('kpiWeekCount');
      const nextTitleEl = document.getElementById('kpiNextTitle');
      const nextTimeEl = document.getElementById('kpiNextTime');
      const capPctEl = document.getElementById('kpiCapacityPct');
      const capSubEl = document.getElementById('kpiCapacitySub');
      const capBarEl = document.getElementById('kpiCapacityBar');
      const todaySparkEl = document.getElementById('kpiTodaySpark');
      const weekSparkEl = document.getElementById('kpiWeekSpark');
      const now = new Date();

      // Hoy
      const todayList = this.eventsOnDay(now);
      this.animateNumber(todayCountEl, todayList.length);
      if (todaySubEl) todaySubEl.textContent = todayList.length === 1 ? 'Evento programado' : 'Eventos programados';
      // Tendencia vs ayer
      const yesterday = new Date(now.getTime()); yesterday.setDate(now.getDate()-1);
      const yesterdayList = this.eventsOnDay(yesterday);
      const diff = todayList.length - yesterdayList.length;
      if (todayTrendEl){
        todayTrendEl.textContent = (diff===0) ? '±0' : (diff>0 ? `↑ +${diff}` : `↓ ${diff}`);
        todayTrendEl.className = 'kpi-trend ' + (diff>0 ? 'up':'down');
      }

      // Semana visible (rango actual)
      const weekList = this.events.filter(ev=>{
        const s = new Date(ev.startISO);
        const cal = (ev.calendar||'desk');
        return s >= range.start && s <= range.end && (!!this.filters[cal]);
      });
      this.animateNumber(weekCountEl, weekList.length);

      // Próximo evento
      const upcoming = this.events
        .filter(ev=>{ const cal=(ev.calendar||'desk'); return new Date(ev.startISO) >= now && (!!this.filters[cal]); })
        .sort((a,b)=> new Date(a.startISO).getTime() - new Date(b.startISO).getTime())[0];
      if (upcoming){
        const s = new Date(upcoming.startISO);
        if (nextTitleEl) nextTitleEl.textContent = (upcoming.title||'Evento');
        if (nextTimeEl) nextTimeEl.textContent = `${this.formatTime(s)}`;
      } else {
        if (nextTitleEl) nextTitleEl.textContent = '—';
        if (nextTimeEl) nextTimeEl.textContent = '—';
      }

      // Ocupación hoy (08–21) en minutos
      const startRef = this.dayStartMin, endRef = this.dayEndMin; const span = endRef - startRef;
      let usedMin = 0;
      todayList.forEach(ev=>{
        const s = new Date(ev.startISO); const e = new Date(ev.endISO);
        let sm = s.getHours()*60 + s.getMinutes();
        let em = e.getHours()*60 + e.getMinutes();
        sm = Math.max(sm, startRef); em = Math.min(em, endRef);
        if (em>sm) usedMin += (em-sm);
      });
      const pct = Math.max(0, Math.min(100, Math.round((usedMin/span)*100)));
      if (capPctEl) capPctEl.textContent = `${pct}%`;
      if (capSubEl) capSubEl.textContent = 'Hoy (08–21)';
      if (capBarEl) capBarEl.style.width = `${pct}%`;

      // Sparkline de HOY: distribución por hora (08–21)
      if (todaySparkEl){
        const hours = Math.round((endRef-startRef)/60);
        const buckets = new Array(hours).fill(0);
        todayList.forEach(ev=>{
          const s = new Date(ev.startISO); const e = new Date(ev.endISO);
          let sm = Math.max(s.getHours()*60 + s.getMinutes(), startRef);
          let em = Math.min(e.getHours()*60 + e.getMinutes(), endRef);
          for(let i=0;i<hours;i++){
            const bStart = startRef + i*60; const bEnd = bStart+60;
            const overlap = Math.max(0, Math.min(em, bEnd) - Math.max(sm, bStart));
            buckets[i] += overlap;
          }
        });
        const max = Math.max(...buckets, 1);
        this.drawSparkline(todaySparkEl, buckets.map(v=> v/max));
      }

      // Sparkline de SEMANA: eventos por día en rango
      if (weekSparkEl){
        const days = Math.round((range.end - range.start)/86400000)+1;
        const counts = [];
        for(let i=0;i<days;i++){
          const d = new Date(range.start.getTime()); d.setDate(range.start.getDate()+i);
          counts.push(this.eventsOnDay(d).length);
        }
        const max = Math.max(...counts, 1);
        this.drawSparkline(weekSparkEl, counts.map(v=> v/max));
      }
    }catch(e){ /* noop */ }
  }

  drawSparkline(svgEl, values){
    try{
      const w = 100, h = 28; const n = values.length;
      const points = values.map((v,i)=>{
        const x = (i/(n-1))*w; const y = h - (v*h);
        return `${x},${y}`;
      }).join(' ');
      svgEl.innerHTML = `<polyline points="${points}"/>`;
    }catch(e){ /* noop */ }
  }

  animateNumber(el, target){
    if (!el) return;
    const current = parseInt(el.textContent||'0',10)||0;
    const duration = 300; const start = performance.now();
    const step = (ts)=>{
      const p = Math.min(1, (ts-start)/duration);
      const val = Math.round(current + (target-current)*p);
      el.textContent = String(val);
      if (p<1) requestAnimationFrame(step);
    };
    requestAnimationFrame(step);
  }

  enhanceDynamics(){
    // refuerzo sutil: nada costoso, sólo señales visuales rápidas
    // Marcar columna actual un poco más
    const cols = document.querySelectorAll('.day-col.today');
    cols.forEach(col=>{ col.style.boxShadow = 'inset 0 0 0 1px rgba(46,166,255,.15)'; });
  }



  getTimezoneLabel(){
    const offsetMin = -new Date().getTimezoneOffset();
    const sign = offsetMin>=0?'+':'-';
    const abs = Math.abs(offsetMin);
    const hh = String(Math.floor(abs/60)).padStart(2,'0');
    const mm = abs%60 ? ':'+String(abs%60).padStart(2,'0') : '';
    return `GMT${sign}${hh}${mm}`;
  }



  renderMonth(root, range){
    const startMonth = new Date(this.current.getFullYear(), this.current.getMonth(), 1);
    const endMonth = new Date(this.current.getFullYear(), this.current.getMonth()+1, 0);
    // iniciar en lunes de la primera semana mostrada
    const smDay = startMonth.getDay();
    const diffToMonday = (smDay===0)? -6 : (1-smDay);
    const gridStart = new Date(startMonth.getTime()); gridStart.setDate(startMonth.getDate()+diffToMonday); gridStart.setHours(0,0,0,0);

    const header = document.createElement('div'); header.className='month-header';
    const wdNames = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'];
    wdNames.forEach((n, idx)=>{ const d=document.createElement('div'); d.className='day'; d.textContent=n; const todayIndex = ((new Date().getDay()===0)?6:(new Date().getDay()-1)); if (idx===todayIndex) d.classList.add('today'); header.appendChild(d); });
    root.appendChild(header);

    const grid = document.createElement('div'); grid.className='month-grid';
    for(let i=0;i<42;i++){
      const d = new Date(gridStart.getTime()); d.setDate(gridStart.getDate()+i);
      const cell = document.createElement('div'); cell.className='day-cell';
      const inMonth = d.getMonth()===this.current.getMonth();
      if (!inMonth) cell.classList.add('out-month');
      const dow = d.getDay(); if (dow===0 || dow===6) cell.classList.add('weekend');
      const now = new Date(); if (d.getFullYear()===now.getFullYear() && d.getMonth()===now.getMonth() && d.getDate()===now.getDate()) cell.classList.add('today');
      const head = document.createElement('div'); head.className='date'; head.textContent = String(d.getDate());
      cell.appendChild(head);
      const list = this.eventsOnDay(d);
      const maxShow = 3;
      list.slice(0, maxShow).forEach(ev=>{
        const me = document.createElement('span'); me.className='mini-event'; me.textContent = ev.title || 'Evento';
        if (ev.color) me.style.setProperty('--event-color', ev.color);
        const sEv = new Date(ev.startISO); const eEv = new Date(ev.endISO);
        me.title = `${this.formatTime(sEv)} – ${this.formatTime(eEv)}${ev.description?`\n${ev.description}`:''}`;
        me.addEventListener('click', ()=> this.openModal(ev.id));
        cell.appendChild(me);
      });
      if (list.length > maxShow){
        const more = document.createElement('span'); more.className='mini-event more'; more.textContent = `+${list.length-maxShow} más`;
        more.addEventListener('click', ()=>{ this.toast('Vista diaria deshabilitada'); });
        cell.appendChild(more);
      }
      cell.addEventListener('dblclick', ()=>{
        const base = new Date(d.getTime()); base.setHours(10,0,0,0); const end = new Date(base.getTime()); end.setMinutes(end.getMinutes()+30);
        const id = Date.now(); this.events.push({ id, userId:this.userId, title:'Nuevo evento', startISO:base.toISOString(), endISO:end.toISOString(), allDay:false, calendar:'desk' });
        this.saveEvents(); this.render(); this.toast('Evento creado');
      });
      grid.appendChild(cell);
    }
    root.appendChild(grid);
  }

  eventsOnDay(date){
    const y=date.getFullYear(), m=date.getMonth(), d=date.getDate();
    const q = this.getSearchQuery();
    return this.events.filter(ev=>{
      const s = new Date(ev.startISO); const e = new Date(ev.endISO);
      const sameDay = s.getFullYear()===y && s.getMonth()===m && s.getDate()===d;
      if (!sameDay) return false;
      // Filtrar por calendarios seleccionados
      const cal = (ev.calendar || 'desk');
      if (!this.filters[cal]) return false;
      if (!q) return true;
      const hay = (ev.title||'') + ' ' + (ev.description||'');
      return hay.toLowerCase().includes(q);
    });
  }

  getSearchQuery(){
    const el = document.getElementById('calSearchInput');
    const v = (el?.value || this.searchTerm || '').trim().toLowerCase();
    return v;
  }

  buildEventBlock(ev, layout){
    const s = new Date(ev.startISO); const e = new Date(ev.endISO);
    const startMin = s.getHours()*60 + s.getMinutes();
    const endMin = e.getHours()*60 + e.getMinutes();
    const startRef = 8*60; const endRef = 18*60; const span = endRef - startRef;
    const topPct = ((startMin - startRef) / span) * 100; const heightPct = ((endMin - startMin) / span) * 100;
    const el = document.createElement('div'); el.className='event'; el.style.top = `${Math.max(0, topPct)}%`; el.style.height = `${Math.max(4, heightPct)}%`;
    const idx = layout?.index ?? 0; const cnt = layout?.count ?? 1;
    el.style.setProperty('--col-index', String(idx));
    el.style.setProperty('--col-count', String(cnt));
    el.dataset.id = String(ev.id);
    if (ev.color) el.style.setProperty('--event-color', ev.color);
    el.innerHTML = `<div class="title">${ev.title || 'Evento'}</div><div class="meta">${this.formatTime(s)} – ${this.formatTime(e)}</div><div class="resize-handle" aria-hidden="true"></div>`;
    el.addEventListener('click', ()=> this.openModal(ev.id));
    this.attachDragHandlers(el, ev);
    return el;
  }

  computeDayLayouts(list){
    const items = list.map(ev=>{
      const s = new Date(ev.startISO); const e = new Date(ev.endISO);
      return { id: ev.id, ev, start: s.getHours()*60 + s.getMinutes(), end: e.getHours()*60 + e.getMinutes(), col: -1 };
    }).sort((a,b)=> a.start===b.start ? a.end-b.end : a.start-b.start);
    const layouts = {}; const groups = []; let active = []; let currentGroup = { events: [], maxCols: 0, active: [] };
    const freeCol = (used)=>{ let i=0; while(used.has(i)) i++; return i; };
    items.forEach(it=>{
      // Mantener solo los que siguen solapando
      currentGroup.active = currentGroup.active.filter(a=> a.end > it.start);
      // Si ya había eventos y se vació el activo, cerramos grupo anterior
      if (currentGroup.events.length && currentGroup.active.length===0){
        groups.push({ events: currentGroup.events.slice(), maxCols: currentGroup.maxCols });
        currentGroup = { events: [], maxCols: 0, active: [] };
      }
      const used = new Set(currentGroup.active.map(a=> a.col));
      const colIdx = freeCol(used);
      it.col = colIdx; currentGroup.events.push(it);
      currentGroup.maxCols = Math.max(currentGroup.maxCols, colIdx+1);
      currentGroup.active.push(it);
    });
    if (currentGroup.events.length) groups.push({ events: currentGroup.events.slice(), maxCols: currentGroup.maxCols });
    groups.forEach(g=>{ g.events.forEach(it=>{ layouts[it.id] = { index: it.col, count: g.maxCols }; }); });
    return layouts;
  }

  startNowLineUpdater(){
    try { if (this.nowLineTimer) clearInterval(this.nowLineTimer); } catch(e){}
    this.nowLineTimer = setInterval(()=>{ if (this.view!=='month') this.render(); }, 60000);
  }

  // ===== Carga dinámica de datos (SSE / Polling) =====
  initDataLoading(){
    // Si hay SSE configurado, intentamos conectarnos; si falla, hacemos polling
    if (this.dataConfig.sseUrl && typeof EventSource !== 'undefined'){
      this.startSSE();
    } else if (this.dataConfig.dataUrl){
      this.startPolling(30000);
    }
  }

  startSSE(){
    try{
      if (this.es) { try{ this.es.close(); }catch(e){} }
      const es = new EventSource(this.dataConfig.sseUrl);
      this.es = es;
      es.onmessage = (evt)=>{
        try{
          const payload = JSON.parse(evt.data);
          const list = Array.isArray(payload) ? payload : (Array.isArray(payload.events) ? payload.events : (payload.event ? [payload.event] : []));
          if (list && list.length){ this.applyServerEvents(list); }
        }catch(e){ /* noop */ }
      };
      es.onerror = ()=>{
        try{ es.close(); }catch(e){}
        this.es = null;
        if (this.dataConfig.dataUrl){ this.startPolling(30000); }
      };
    }catch(e){ if (this.dataConfig.dataUrl){ this.startPolling(30000); } }
  }

  startPolling(intervalMs){
    try { if (this.pollTimer) clearInterval(this.pollTimer); } catch(e){}
    const tick = async ()=>{ const range = this.getRange(); await this.pullRange(range); };
    this.pollTimer = setInterval(tick, Math.max(5000, intervalMs||30000));
    tick();
  }

  async pullRange(range){
    const list = await this.fetchServerEvents(range).catch(()=> null);
    if (list && list.length){ this.applyServerEvents(list); }
  }

  async fetchServerEvents(range){
    if (!this.dataConfig.dataUrl) return null;
    const startISO = range.start.toISOString();
    const endISO = range.end.toISOString();
    const url = `${this.dataConfig.dataUrl}?start=${encodeURIComponent(startISO)}&end=${encodeURIComponent(endISO)}&user_id=${encodeURIComponent(this.userId)}`;
    const resp = await fetch(url, { headers: { 'Accept': 'application/json' } });
    if (!resp.ok) return null;
    const json = await resp.json();
    let raw = null;
    if (Array.isArray(json)) raw = json;
    else if (Array.isArray(json.events)) raw = json.events;
    else return null;
    return this.normalizeServerEvents(raw);
  }

  normalizeServerEvents(raw){
    return raw.map(ev=>{
      const s = ev.startISO || ev.start || ev.start_time;
      const e = ev.endISO || ev.end || ev.end_time;
      const sISO = (typeof s==='string') ? s : (s ? new Date(s).toISOString() : new Date().toISOString());
      const eISO = (typeof e==='string') ? e : (e ? new Date(e).toISOString() : new Date().toISOString());
      return {
        id: ev.id ?? Date.now()+Math.random(),
        userId: ev.userId ?? this.userId,
        title: ev.title || 'Evento',
        description: ev.description || '',
        startISO: sISO,
        endISO: eISO,
        allDay: !!ev.allDay,
        color: ev.color || '#2ea6ff',
        calendar: ev.calendar || 'desk'
      };
    });
  }

  applyServerEvents(list){
    this.events = list;
    this.saveEvents();
    this.render();
    this.toast('Eventos actualizados');
  }

  // ===== Mini-calendario y widgets de barra lateral =====
  renderMiniMonth(){
    const cont = document.getElementById('calMiniMonth');
    if (!cont) return;
    cont.innerHTML = '';
    const cur = new Date(this.current.getFullYear(), this.current.getMonth(), 1);
    const smDay = cur.getDay();
    const diffToMonday = (smDay===0)? -6 : (1-smDay);
    const gridStart = new Date(cur.getTime()); gridStart.setDate(cur.getDate()+diffToMonday); gridStart.setHours(0,0,0,0);
    for(let i=0;i<42;i++){
      const d = new Date(gridStart.getTime()); d.setDate(gridStart.getDate()+i);
      const cell = document.createElement('div'); cell.className='mm-cell';
      cell.textContent = String(d.getDate());
      if (d.getMonth()!==this.current.getMonth()) cell.classList.add('out-month');
      const now = new Date();
      if (d.getFullYear()===now.getFullYear() && d.getMonth()===now.getMonth() && d.getDate()===now.getDate()) cell.classList.add('today');
      if (d.getFullYear()===this.current.getFullYear() && d.getMonth()===this.current.getMonth() && d.getDate()===this.current.getDate()) cell.classList.add('selected');
      cell.addEventListener('click', ()=>{ this.current = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0,0,0,0); this.setView('day'); });
      cont.appendChild(cell);
    }
  }

  updateSidebarWidgets(range){
    const upcomingEl = document.getElementById('calUpcomingList');
    const remindersEl = document.getElementById('calReminders');
    if (upcomingEl){
      upcomingEl.innerHTML='';
      const now = new Date();
      const cutoff = new Date(now.getTime()); cutoff.setDate(cutoff.getDate()+7);
      const items = this.events
        .filter(ev=>{ const cal=(ev.calendar||'desk'); return (new Date(ev.startISO))>now && (new Date(ev.startISO))<=cutoff && (!!this.filters[cal]); })
        .sort((a,b)=> new Date(a.startISO)-new Date(b.startISO))
        .slice(0,8);
      if (!items.length){ const empty=document.createElement('div'); empty.className='empty-state'; empty.textContent='Sin próximos'; upcomingEl.appendChild(empty); }
      items.forEach(ev=>{
        const d = new Date(ev.startISO); const timeLabel = this.formatTime(d);
        const it = document.createElement('div'); it.className='mini-event'; it.textContent = `${timeLabel} · ${ev.title||'Evento'}`;
        if (ev.color) it.style.setProperty('--event-color', ev.color);
        it.title = ev.description||'';
        it.addEventListener('click', ()=> this.openModal(ev.id));
        upcomingEl.appendChild(it);
      });
    }
    if (remindersEl){
      remindersEl.innerHTML='';
      const now = new Date(); const soon = new Date(now.getTime()+60*60000);
      const items = this.events
        .filter(ev=>{ const s=new Date(ev.startISO); const cal=(ev.calendar||'desk'); return s>=now && s<=soon && (!!this.filters[cal]); })
        .sort((a,b)=> new Date(a.startISO)-new Date(b.startISO));
      if (!items.length){ const empty=document.createElement('div'); empty.className='empty-state'; empty.textContent='Sin recordatorios'; remindersEl.appendChild(empty); }
      items.forEach(ev=>{
        const d = new Date(ev.startISO); const timeLabel = this.formatTime(d);
        const it = document.createElement('div'); it.className='mini-event'; it.textContent = `${timeLabel} · ${ev.title||'Evento'}`;
        if (ev.color) it.style.setProperty('--event-color', ev.color);
        it.title = ev.description||'';
        it.addEventListener('click', ()=> this.openModal(ev.id));
        remindersEl.appendChild(it);
      });
    }
  }

  editEvent(id){
    const ev = this.events.find(x=>x.id===id); if (!ev) return;
    const title = prompt('Editar título del evento', ev.title||'');
    if (title===null) return;
    ev.title = title.trim() || ev.title;
    this.saveEvents(); this.render(); this.toast('Evento actualizado');
  }

  formatDayHeader(d){
    const wd = ['Lun','Mar','Mié','Jue','Vie','Sáb','Dom'];
    const idx = (d.getDay()===0)?6:(d.getDay()-1);
    return `${wd[idx]} ${String(d.getDate()).padStart(2,'0')}/${String(d.getMonth()+1).padStart(2,'0')}`;
  }

  formatTime(d){
    return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
  }

  formatRangeLabel(s,e){
    const opts = { day:'2-digit', month:'2-digit' };
    return `${s.toLocaleDateString('es-ES', opts)} – ${e.toLocaleDateString('es-ES', opts)}`;
  }

  toast(msg){
    const t = document.createElement('div'); t.className='toast'; t.textContent = msg; document.body.appendChild(t);
    setTimeout(()=>{ t.remove(); }, 1600);
  }

  // Sonido suave para notificaciones
  playNotifySound(){
    try{
      const AC = window.AudioContext || window.webkitAudioContext; if (!AC) return;
      const ctx = new AC();
      const osc = ctx.createOscillator(); const gain = ctx.createGain();
      osc.type = 'sine'; osc.frequency.value = 880; // A5 suave
      osc.connect(gain); gain.connect(ctx.destination);
      gain.gain.setValueAtTime(0.0001, ctx.currentTime);
      gain.gain.exponentialRampToValueAtTime(0.05, ctx.currentTime+0.02);
      osc.start();
      gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime+0.15);
      osc.stop(ctx.currentTime+0.15);
    }catch(e){ /* noop */ }
  }

  updateBellUI(){
    const bell = document.getElementById('calNotifyBell');
    const bellCount = document.getElementById('calNotifyCount');
    if (!bell) return;
    bell.classList.toggle('active', !!this.remindersEnabled);
    if (bellCount){
      const now = Date.now();
      const soonMs = this.reminderOffsetMin * 60 * 1000;
      const count = this.events.filter(ev=>{
        const ms = new Date(ev.startISO).getTime();
        return ms>now && ms<=now+soonMs;
      }).length;
      bellCount.textContent = String(count);
      bellCount.hidden = count===0;
    }
  }

  attachDragHandlers(el, ev){
    const onMoveDrag = (mmEvt, startY, origStart)=>{
      const delta = mmEvt.clientY - startY; // px
      // cada hora ~48px (ver CSS); media hora ~24px; total 10h -> 480px
      const minutesPerPx = (10*60) / 480; // 1.25 min/px
      const deltaMin = Math.round(delta * minutesPerPx);
      const newStart = new Date(origStart.getTime()); newStart.setMinutes(newStart.getMinutes()+deltaMin);
      const dur = (new Date(ev.endISO).getTime() - origStart.getTime());
      const newEnd = new Date(newStart.getTime()+dur);
      ev.startISO = newStart.toISOString(); ev.endISO = newEnd.toISOString();
      this.saveEvents(); this.render();
    };
    const onMoveResize = (mmEvt, startY, origEnd)=>{
      const delta = mmEvt.clientY - startY;
      const minutesPerPx = (10*60) / 480;
      const deltaMin = Math.round(delta * minutesPerPx);
      const newEnd = new Date(origEnd.getTime()); newEnd.setMinutes(newEnd.getMinutes()+deltaMin);
      if (newEnd > new Date(ev.startISO)){
        ev.endISO = newEnd.toISOString(); this.saveEvents(); this.render();
      }
    };
    el.addEventListener('mousedown', (e)=>{
      if (e.target && e.target.classList.contains('resize-handle')) return; // resize se maneja aparte
      const origStart = new Date(ev.startISO); const startY = e.clientY;
      const move = (mm)=> onMoveDrag(mm, startY, origStart);
      const up = ()=>{ document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
      document.addEventListener('mousemove', move); document.addEventListener('mouseup', up);
    });
    const rh = el.querySelector('.resize-handle');
    if (rh){
      rh.addEventListener('mousedown', (e)=>{
        e.stopPropagation(); const origEnd = new Date(ev.endISO); const startY = e.clientY;
        const move = (mm)=> onMoveResize(mm, startY, origEnd);
        const up = ()=>{ document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); };
        document.addEventListener('mousemove', move); document.addEventListener('mouseup', up);
      });
    }
  }

  attachSelectionCreate(col, date){
    let selecting = false; let startY = 0; let overlay = null;
    const startRef = 8*60; const endRef = 18*60; const minutesPerPx = (10*60) / 480; // 1.25 min/px
    const getTimeFromY = (y)=>{
      const rect = col.getBoundingClientRect();
      const offset = Math.max(0, y - rect.top);
      const mins = Math.round(offset * minutesPerPx) + startRef;
      return Math.max(startRef, Math.min(endRef, mins));
    };
    col.addEventListener('mousedown', (e)=>{
      // evitar conflicto al arrastrar eventos; sólo fondo
      const tgt = e.target;
      if (tgt && tgt instanceof HTMLElement && tgt.classList && tgt.classList.contains('event')) return;
      selecting = true; startY = e.clientY;
      overlay = document.createElement('div'); overlay.className='selection';
      overlay.style.top = `${((getTimeFromY(startY)-startRef)/(endRef-startRef))*100}%`;
      overlay.style.height = '0%';
      col.appendChild(overlay);
    });
    const onMove = (e)=>{
      if (!selecting || !overlay) return;
      const sMin = getTimeFromY(startY); const cMin = getTimeFromY(e.clientY);
      const topMin = Math.min(sMin, cMin); const bottomMin = Math.max(sMin, cMin);
      const topPct = ((topMin-startRef)/(endRef-startRef))*100;
      const hPct = ((bottomMin-topMin)/(endRef-startRef))*100;
      overlay.style.top = `${topPct}%`; overlay.style.height = `${Math.max(1, hPct)}%`;
    };
    const onUp = (e)=>{
      if (!selecting || !overlay) return;
      selecting = false;
      const sMin = getTimeFromY(startY); const eMin = getTimeFromY(e.clientY);
      const startMin = Math.min(sMin, eMin); const endMin = Math.max(sMin, eMin);
      const s = new Date(date.getFullYear(), date.getMonth(), date.getDate(), Math.floor(startMin/60), startMin%60);
      const f = new Date(date.getFullYear(), date.getMonth(), date.getDate(), Math.floor(endMin/60), endMin%60);
      const id = Date.now();
      this.events.push({ id, userId:this.userId, title:'Nuevo evento', startISO:s.toISOString(), endISO:f.toISOString(), allDay:false, calendar:'desk' });
      this.saveEvents(); this.render(); this.toast('Evento creado');
      overlay.remove(); overlay = null;
    };
    document.addEventListener('mousemove', onMove);
    document.addEventListener('mouseup', onUp);
  }

  openModal(id){
    const ev = this.events.find(x=>x.id===id); if (!ev) return;
    const modal = document.getElementById('calModal'); if (!modal) return;
    const dialog = modal.querySelector('.cal-modal-dialog');
    if (dialog){ const accent = ev.color || '#2ea6ff'; dialog.style.setProperty('--accent', accent); }
    const colorDot = document.getElementById('calColorDot'); if (colorDot) colorDot.style.background = ev.color || '#2ea6ff';
    const title = document.getElementById('calTitle');
    const desc = document.getElementById('calDesc');
    const start = document.getElementById('calStart');
    const end = document.getElementById('calEnd');
    const allDay = document.getElementById('calAllDay');
    const color = document.getElementById('calColor');
    const calSel = document.getElementById('calCalendar');
    const durationEl = document.getElementById('calDuration');
    const rangeMiniEl = document.getElementById('calRangeMini');
    const notes = document.getElementById('calNotes');
    const roBanner = document.getElementById('calReadOnlyBanner');
    // Lead UI elements
    const leadNameLink = document.getElementById('calLeadNameLink');
    const leadAvatar = document.getElementById('calLeadAvatar');
    const leadSub = document.getElementById('calLeadSub');
    const leadEmail = document.getElementById('calLeadEmail');
    const leadPhone = document.getElementById('calLeadPhone');
    const leadDesk = document.getElementById('calLeadDesk');
    const leadAssigned = document.getElementById('calLeadAssigned');
    const leadIdInput = document.getElementById('calLeadId');
    const loadLeadBtn = document.getElementById('calLoadLeadBtn');
    title.value = ev.title || '';
    desc.value = ev.description || '';
    if (notes) notes.value = ev.notes || '';
    const s = new Date(ev.startISO); const e = new Date(ev.endISO);
    start.value = this.toLocalInputValue(s);
    end.value = this.toLocalInputValue(e);
    allDay.checked = !!ev.allDay;
    color.value = ev.color || '#2ea6ff';
    modal.classList.add('open');
    if (dialog) dialog.classList.add('read-only');
    if (roBanner) roBanner.hidden = false;
    // Mostrar rango corto y duración
    if (rangeMiniEl) rangeMiniEl.textContent = `${this.formatTime(s)} – ${this.formatTime(e)}`;
    if (durationEl){
      const mins = Math.round((e.getTime()-s.getTime())/60000);
      if (ev.allDay) durationEl.textContent = 'Todo el día';
      else if (mins<60) durationEl.textContent = `${mins} min`;
      else { const h=Math.floor(mins/60), m=mins%60; durationEl.textContent = `${h} h${m?` ${m} min`:''}`; }
    }

    // Poblar sección Lead
    const resetLeadUI = ()=>{
      if (leadNameLink) { leadNameLink.textContent = 'Sin lead'; leadNameLink.href = '#'; leadNameLink.onclick = null; }
      if (leadAvatar) { leadAvatar.textContent = '—'; }
      if (leadSub) { leadSub.textContent = '—'; }
      if (leadEmail) { leadEmail.textContent = '—'; }
      if (leadPhone) { leadPhone.textContent = '—'; }
      if (leadDesk) { leadDesk.textContent = '—'; }
      if (leadAssigned) { leadAssigned.textContent = '—'; }
      if (leadIdInput) { leadIdInput.value = ev.leadId ? String(ev.leadId) : ''; }
    };
    resetLeadUI();

    const computeProfileUrl = (id)=>{
      const base = (window.APP_BASE_PATH || '');
      const b = (typeof base === 'string' && base.endsWith('/')) ? base.slice(0, -1) : base;
      return b ? (b + '/index.php?module=leads&action=view&id=' + encodeURIComponent(id))
               : ('index.php?module=leads&action=view&id=' + encodeURIComponent(id));
    };

    const populateLead = (data)=>{
      const full = (data.full_name || '').trim();
      const initials = full ? full.split(' ').map(s=>s[0]).slice(0,2).join('').toUpperCase() : '—';
      if (leadAvatar) leadAvatar.textContent = initials || '—';
      if (leadNameLink) {
        leadNameLink.textContent = full || ('Lead ' + (data.id||''));
        const url = computeProfileUrl(data.id);
        leadNameLink.href = url;
        leadNameLink.onclick = (e)=>{ e.preventDefault(); if (typeof goToLeadProfile === 'function') { goToLeadProfile(data.id); } else { window.location.href = url; } };
      }
      if (leadSub) leadSub.textContent = data.country ? data.country : '';
      if (leadEmail) leadEmail.textContent = data.email || '—';
      if (leadPhone) leadPhone.textContent = data.phone || '—';
      if (leadDesk) leadDesk.textContent = data.desk_name || '—';
      if (leadAssigned) leadAssigned.textContent = data.assigned_name || '—';
    };

    const fetchLead = async (leadId)=>{
      if (!leadId) return;
      try{
        const resp = await fetch(`/modules/leads/get_lead.php?lead_id=${encodeURIComponent(leadId)}`);
        const json = await resp.json();
        if (json && json.success && json.lead){ populateLead(json.lead); }
      }catch(e){ /* noop */ }
    };

    if (ev.leadId) { fetchLead(ev.leadId); }
    const close = ()=>{ modal.classList.remove('open'); if (dialog) dialog.classList.add('read-only'); if (roBanner) roBanner.hidden = false; };
    const closeBtn = document.getElementById('calModalClose'); const cancelBtn = document.getElementById('calCancel');
    closeBtn.onclick = cancelBtn.onclick = close;
    const overlay = document.getElementById('calModalOverlay'); if (overlay) overlay.onclick = close;
    const delBtn = document.getElementById('calDelete');
    delBtn.onclick = ()=>{ this.events = this.events.filter(x=>x.id!==id); this.saveEvents(); this.render(); close(); this.toast('Evento eliminado'); };
    const saveBtn = document.getElementById('calSave');
    const editBtn = document.getElementById('calEdit');

    // Modo lectura por defecto
    [title, desc, start, end, allDay, color, calSel, leadIdInput, loadLeadBtn, notes].forEach(el=>{ if (el) el && (el.disabled = true); });
    if (saveBtn) saveBtn.disabled = true;
    if (editBtn) editBtn.disabled = false;

    // Activar edición cuando se presiona "Editar"
    if (editBtn) editBtn.onclick = ()=>{
      [title, desc, start, end, allDay, color, calSel, leadIdInput, loadLeadBtn, notes].forEach(el=>{ if (el) el && (el.disabled = false); });
      if (saveBtn) saveBtn.disabled = false;
      if (dialog) dialog.classList.remove('read-only');
      if (roBanner) roBanner.hidden = true;
      this.toast('Edición habilitada');
    };

    if (loadLeadBtn){
      loadLeadBtn.onclick = ()=>{
        const val = (leadIdInput && leadIdInput.value) ? parseInt(leadIdInput.value, 10) : 0;
        if (val>0){ ev.leadId = val; fetchLead(val); this.toast('Lead asociado'); }
      };
    }

    saveBtn.onclick = ()=>{
      ev.title = title.value.trim() || ev.title;
      ev.description = desc.value.trim();
      ev.notes = notes ? notes.value.trim() : ev.notes;
      ev.startISO = new Date(start.value).toISOString();
      ev.endISO = new Date(end.value).toISOString();
      ev.allDay = !!allDay.checked;
      ev.color = color.value;
      ev.calendar = 'desk';
      // Persistir leadId si fue ingresado
      if (leadIdInput && leadIdInput.value){
        const val = parseInt(leadIdInput.value, 10);
        ev.leadId = (val>0) ? val : undefined;
      }
      this.saveEvents(); this.render(); close(); this.toast('Evento guardado');
    };

    // Tabs del modal
    const tabEvent = document.getElementById('calTabEvent');
    const tabLead = document.getElementById('calTabLead');
    const tabNotes = document.getElementById('calTabNotes');
    const panelEvent = document.getElementById('calTabPanelEvent');
    const panelLead = document.getElementById('calTabPanelLead');
    const panelNotes = document.getElementById('calTabPanelNotes');
    const setActive = (name)=>{
      [tabEvent, tabLead, tabNotes].forEach(btn=> btn?.classList.remove('active'));
      [panelEvent, panelLead, panelNotes].forEach(p=> p?.classList.remove('active'));
      if (name==='event'){ tabEvent?.classList.add('active'); panelEvent?.classList.add('active'); }
      else if (name==='lead'){ tabLead?.classList.add('active'); panelLead?.classList.add('active'); }
      else { tabNotes?.classList.add('active'); panelNotes?.classList.add('active'); }
    };
    tabEvent?.addEventListener('click', ()=> setActive('event'));
    tabLead?.addEventListener('click', ()=> setActive('lead'));
    tabNotes?.addEventListener('click', ()=> setActive('notes'));
    setActive('event');
  }

  toLocalInputValue(d){
    const pad = (n)=> String(n).padStart(2,'0');
    const yyyy = d.getFullYear(); const mm = pad(d.getMonth()+1); const dd = pad(d.getDate());
    const hh = pad(d.getHours()); const mi = pad(d.getMinutes());
    return `${yyyy}-${mm}-${dd}T${hh}:${mi}`;
  }

  startReminderScheduler(){
    const check = ()=>{
      if (!this.remindersEnabled){ this.updateBellUI(); return; }
      const now = Date.now();
      const offsetMs = this.reminderOffsetMin * 60 * 1000;
      this.events.forEach(ev=>{
        const key = `rem:${ev.id}:${ev.startISO}`;
        const startMs = new Date(ev.startISO).getTime();
        if (startMs - now <= offsetMs && startMs - now > 0 && !this.notifiedKeys.has(key)){
          this.notifiedKeys.add(key);
          this.toast(`Recordatorio: ${ev.title || 'Evento'} a las ${this.formatTime(new Date(ev.startISO))}`);
          this.playNotifySound();
        }
      });
      this.updateBellUI();
    };
    check();
    this.reminderTimer = setInterval(check, 30000);
  }
}

document.addEventListener('DOMContentLoaded', ()=>{
  const app = new CalendarApp();
  app.init();
});
